Compare commits

...

35 Commits

Author SHA1 Message Date
mikkeli e47468fc98 UPS: ups_pct_source and ups_charge_ah (counted charge); host.ndjson in capture sessions
/ crosscheck (push) Successful in 1s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Successful in 22s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-16 00:17:34 +09:00
mikkeli 9452708e80 Checklist v5: step 32, temperature dial; step 14 notes the overnight soak
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-15 23:17:48 +09:00
mikkeli cff32a7190 Signals: cam_up and vision_up (orin-status), for the app's status bar
/ crosscheck (push) Successful in 1s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Successful in 21s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:01:03 +09:00
mikkeli 15c7d6ce84 Checklist v4: step 31, each wheel by hand on stands; DECODE-3: 449 bytes 0, 1, 3 as climate-control sensor candidates
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-15 22:30:40 +09:00
mikkeli c31af2d5e3 Z27AG-DECODE-3: 570 bytes 2-3 slosh with motion, so fuel (not temperature); odometer agrees with the cluster
/ crosscheck (push) Successful in 2s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-15 22:23:47 +09:00
mikkeli e8ba581985 Z27AG decode pass 3: wheel positions from corners, odometer (570, 24-bit); layouts can be 24 bits
/ crosscheck (push) Successful in 1s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Successful in 25s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-15 22:13:34 +09:00
mikkeli 642be2a5e1 Steering angle (0C0, signed 0.1°), main beam, front fogs; layouts can be signed; dipped beam doubted
/ crosscheck (push) Successful in 2s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Successful in 22s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-15 21:21:48 +09:00
mikkeli 0af2c956e4 Z27AG decode pass 2: vehicle speed, coolant, front wiper; wheel speeds little-endian; IMU checked against GNSS
/ crosscheck (push) Successful in 1s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Successful in 22s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-15 20:15:07 +09:00
mikkeli b52a7dcedb Checklist v3: IMU calibration (level twice, straight pull-aways and stops); yukart/raw/imu
/ crosscheck (push) Successful in 1s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Successful in 19s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-14 13:28:40 +09:00
mikkeli 4cbac36fbe IMU signals in the car's frame (attitude, gravity-free g, yaw rate) on yukart/imu/<signal>
/ crosscheck (push) Successful in 0s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Successful in 17s
The WT61's publisher owns mounting and gravity; displays get car-frame values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 23:23:31 +09:00
mikkeli 5d186bfd5d Captures plan: OBD-II through the ELM on the K-line, obd_* names, what each poller state means
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 22:57:31 +09:00
mikkeli 229d796cfd OBD-II signals renamed obd_*, plus timing, trims, O2; GNSS signals; elm_connected; reboot command
/ crosscheck (push) Successful in 1s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Failing after 6s
Decision 30: CAN feeds displays, OBD-II (K-line via the ELM) calibrates, so every
obd2 signal carries the obd_ prefix and the plain names are left for decoded ones.
yukart/gnss/<signal> gives displays the fix as single values; gnss/fix stays for
navigation. Boundary: reboot beside poweroff, same guards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 22:50:03 +09:00
mikkeli 5feba8b354 Decode report: clutch and transmission not on CAN, stated once; no camera trigger plan
/ crosscheck (push) Successful in 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 21:48:07 +09:00
mikkeli 0eab0e660e Decode report: reverse rechecked, gear inference dropped for a recommended gear
/ crosscheck (push) Successful in 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 21:44:52 +09:00
mikkeli 34140cc666 Decoded Z27AG signals: layouts in signals.json can blocks, the only source
/ crosscheck (push) Successful in 1s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Failing after 6s
engine_rpm moves to source z27ag (no OBD-II on this car's CAN). Ten more:
accelerator, brake, handbrake, indicators, side lights, dipped beam, doors,
A/C compressor. Guideline 6: no service writes a CAN ID of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 21:31:51 +09:00
mikkeli 77d6b79c6b Z27AG first decoding pass: rpm, pedal, brake, handbrake, indicators, lights, doors, wheel speeds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 20:47:20 +09:00
mikkeli 140ad0d3ba CI: build every Rust service that compiles contracts in, on schema changes (dev Orin)
/ crosscheck (push) Successful in 0s
Rust consumers (L4T) / Clippy, test, build, glibc for every service that compiles contracts in (push) Successful in 13s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 20:00:43 +09:00
mikkeli e6a1d7f79f Boundary: poweroff command (confirm + boot_id, sent once); hello carries boot_id
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 18:35:06 +09:00
mikkeli b79820dfce Host signals can_up, can_bus_state, can_rx_fps: the recorder's CAN interface
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 17:58:40 +09:00
mikkeli aff9ae3f9f Checklist z27ag-captures v2: jack-stands section (23-26), conditions from the first run
/ crosscheck (push) Successful in 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 17:55:29 +09:00
mikkeli 75fec3b4c2 Host signal net_wifi_ssid: the network the Orin's WiFi is on
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 16:39:00 +09:00
mikkeli 635e2d1304 Z27AG capture steps as data, for the app's one-button page
/ crosscheck (push) Successful in 1s
checklists/z27ag-captures.json is now the source of the steps; the plan
describes the Captures page and the mark texts it records.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 11:12:43 +09:00
mikkeli f7c7a3533a Z27AG captures: reverse as the reverse-light bit
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 10:05:10 +09:00
mikkeli a372f16ebd Z27AG prior art: no sibling capture sought
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 09:59:05 +09:00
mikkeli ac09363b04 Z27AG prior art: forfour Brabus is the CZT sibling
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 09:50:13 +09:00
mikkeli 7dd6a4c467 Z27AG prior art: forfour 454 sibling, AiM Colt DBC, automated decoding
/ crosscheck (push) Successful in 0s
The forfour 454 notes (SmartCruiseControl) share our body and chassis
frames; the AiM "Colt" DBC is the EU Colt/forfour engine-side set and does
not match the Z27.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 09:39:19 +09:00
mikkeli 0862eb5423 CI: crosscheck runs on the halogen label
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 01:01:19 +09:00
mikkeli f07e8071e4 CI: crosscheck caller names its runner
/ crosscheck (push) Successful in 1s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 00:37:59 +09:00
mikkeli 4a69d1506a CI: cross-repo consistency check on push (yukart/tool-crosscheck)
/ crosscheck (push) Successful in 0s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-13 00:36:53 +09:00
mikkeli dbdaa901cc Z27AG capture checklist; obd-poller publishes yukart/vehicle/*
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-12 23:25:03 +09:00
mikkeli 1f91b7ee16 Boundary: source bus, and how svc-gw judges stale
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-12 16:37:10 +09:00
mikkeli d9d942745b Host signals: the Orin's health on yukart/host/<signal> (source host)
Temperatures, CPU/GPU load, memory, module power, fan, WiFi/LTE/tablet
links and the UPS battery, published by svc-core's orin-status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-12 16:33:15 +09:00
mikkeli 7653cb609c Boundary: svc-gw paces its socket toward the tablet (decision 28, proposed)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
2026-10-11 17:21:53 +09:00
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
15 changed files with 2102 additions and 39 deletions
+17
View File
@@ -0,0 +1,17 @@
# Cross-repo consistency (yukart/tool-crosscheck): decisions, topics, doc references,
# service lists and front matter checked against their source in every yukart repo.
on:
push:
branches: [ master ]
workflow_dispatch:
jobs:
crosscheck:
# Gitea dispatches a reusable-workflow call to a runner chosen by the CALLER's runs-on;
# without one, any runner may take it (the instance's pi5-runner did, and failed).
runs-on: halogen
uses: yukart/tool-crosscheck/.gitea/workflows/crosscheck.yml@master
with:
repo: ${{ github.event.repository.name }}
sha: ${{ github.sha }}
secrets: inherit
+75
View File
@@ -0,0 +1,75 @@
name: Rust consumers (L4T)
# A contracts change can break a service that compiles contracts in (include_str! of
# ../contracts/...), and that service's own CI only runs when the service changes.
# So: on a change to what services compile in, build every one of them, at its master,
# against the pushed contracts. README, docs and checklists are not compiled into any
# service (checklists reach the app as a synced copy, which crosscheck watches).
on:
push:
branches: [ master ]
paths:
- 'schema/**'
- '.gitea/workflows/rust-consumers.yml'
workflow_dispatch:
jobs:
consumers:
name: Clippy, test, build, glibc for every service that compiles contracts in
# The development Orin (host mode): the same checks as each service's rust.yml.
runs-on: l4t-rust
steps:
- name: Checkout contracts at the push, and every consumer at master
env:
SERVER: ${{ gitea.server_url }}
SHA: ${{ github.sha }}
run: |
set -euo pipefail
rm -rf ws && mkdir ws
git init -q ws/contracts
git -C ws/contracts fetch -q --depth 1 "$SERVER/yukart/contracts.git" "$SHA"
git -C ws/contracts checkout -q FETCH_HEAD
repos=$(curl -fsS "$SERVER/api/v1/orgs/yukart/repos?limit=50" \
| python3 -c 'import json,sys; print(" ".join(r["name"] for r in json.load(sys.stdin) if not r["archived"]))')
consumers=""
for r in $repos; do
[ "$r" = contracts ] && continue
# Only Rust repos are cloned in full; the API says whether Cargo.toml exists.
curl -fsS -o /dev/null "$SERVER/api/v1/repos/yukart/$r/contents/Cargo.toml" 2>/dev/null || continue
git clone -q --depth 1 "$SERVER/yukart/$r.git" "ws/$r"
if grep -rqsF '../contracts/' "ws/$r/src" "ws/$r/build.rs"; then
consumers="$consumers $r"
else
rm -rf "ws/$r"
fi
done
[ -n "$consumers" ] || { echo "no Rust repo compiles contracts in"; exit 1; }
echo "CONSUMERS=$consumers" >> "$GITHUB_ENV"
echo "contracts $(git -C ws/contracts log --oneline -1); consumers:$consumers"
rustc --version; head -1 /etc/nv_tegra_release
- name: Build each consumer
run: |
set -euo pipefail
failed=""
for r in $CONSUMERS; do
echo "::group::$r $(git -C ws/$r log --oneline -1)"
# Each repo's own cache, shared with its rust.yml: the runner runs one job at a time.
export CARGO_TARGET_DIR="$HOME/target/$r"
if ( cd "ws/$r" \
&& cargo clippy --release --all-targets --locked -- -D warnings \
&& cargo test --release --locked \
&& cargo build --release --locked \
&& for b in $(cargo metadata --format-version 1 --no-deps | python3 -c '
import json, sys
for p in json.load(sys.stdin)["packages"]:
for t in p["targets"]:
if "bin" in t["kind"]: print(t["name"])'); do
scripts/check-glibc.sh "$CARGO_TARGET_DIR/release/$b" || exit 1
done ); then
echo "::endgroup::"; echo "$r: ok"
else
echo "::endgroup::"; echo "$r: FAILED"; failed="$failed $r"
fi
done
[ -z "$failed" ] || { echo "failed:$failed"; exit 1; }
+7 -2
View File
@@ -17,9 +17,14 @@ definitions from here.
|---|---| |---|---|
| [`schema/topics.json`](schema/topics.json) | Topic namespace: payload kind, retention, rate, publisher | | [`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/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 | | [`schema/signals.json`](schema/signals.json) | Signal registry — the single source for name, unit, type and range, and for where each decoded Z27AG signal sits in its CAN frame (`can` block) |
| [`docs/BOUNDARY.md`](docs/BOUNDARY.md) | Display boundary protocol: what crosses between `svc-gw` and the app |
| [`checklists/`](checklists/) | Recording checklists as data; the app bundles exact copies (its **Captures** page) |
The Z27AG DBC lands here too, once Phase 2 produces one. Decoded Z27AG signals (source `z27ag`) carry their frame layout in `signals.json` itself,
so name, unit and bits cannot drift apart. A DBC for tools like SavvyCAN or cantools,
if one is wanted, is generated from it, never edited by hand. Findings behind each
layout: [`docs/impl/reports/`](docs/impl/reports/).
- For implementation practice guidelines, refer to the [guidelines](docs/GUIDELINES.md); - For implementation practice guidelines, refer to the [guidelines](docs/GUIDELINES.md);
- For implementation plans and history, refer to [them](docs/impl/); - For implementation plans and history, refer to [them](docs/impl/);
+548
View File
@@ -0,0 +1,548 @@
{
"id": "z27ag-captures",
"version": 5,
"title": "Z27AG decoding captures",
"about": "docs/impl/plans/Z27AG-CAPTURES.md",
"mark_format": "<step> <action>[ #<n>] start|end for a span, <step> <action>[ #<n>] for a point",
"sections": [
{
"title": "Parked, ignition on, engine off, handbrake on",
"steps": [
{
"id": "1",
"title": "Baseline",
"do": "Touch nothing.",
"finds": "the resting value of everything",
"actions": [
{
"label": "baseline",
"hold_s": 20
}
]
},
{
"id": "2",
"title": "Accelerator",
"do": "Slowly to the floor over 5 s, hold 3 s, slowly back. Start as the foot moves, end when it is off.",
"finds": "pedal position, with no rpm to confuse it",
"actions": [
{
"label": "accel"
}
]
},
{
"id": "3",
"title": "Brake",
"do": "Start as you press, hold 3 s, end as you release.",
"finds": "brake switch, maybe pressure (engine off: the pedal goes hard after a press or two, so pressure fades over the repeats)",
"actions": [
{
"label": "brake",
"hold_s": 3,
"repeat": 3
}
]
},
{
"id": "4",
"title": "Clutch",
"do": "Start as you press, hold 3 s, end as you release.",
"finds": "clutch switch",
"actions": [
{
"label": "clutch",
"hold_s": 3,
"repeat": 3
}
]
},
{
"id": "5",
"title": "Steering",
"do": "Centre, full left, full right, back to centre, slowly. Engine off is cleaner but heavy without power steering: step 23 on the jack stands does it properly.",
"finds": "steering angle",
"actions": [
{
"label": "steer"
}
]
},
{
"id": "6",
"title": "Gears",
"do": "Engine off, clutch down throughout. Start when the gear is in, end as you take it out.",
"finds": "reverse (likely as the reverse light); no other gearbox signal expected",
"actions": [
{
"label": "gear 1",
"hold_s": 3
},
{
"label": "gear 2",
"hold_s": 3
},
{
"label": "gear 3",
"hold_s": 3
},
{
"label": "gear 4",
"hold_s": 3
},
{
"label": "gear 5",
"hold_s": 3
},
{
"label": "gear R",
"hold_s": 3
},
{
"label": "neutral",
"hold_s": 3
}
]
},
{
"id": "7",
"title": "Handbrake",
"do": "Hold the foot brake. Start as you release the handbrake, end as you pull it back up.",
"finds": "handbrake switch",
"actions": [
{
"label": "handbrake",
"hold_s": 3,
"repeat": 2
}
]
},
{
"id": "8",
"title": "Indicators",
"do": "Start when switched on, end when off.",
"finds": "indicator states",
"actions": [
{
"label": "left",
"hold_s": 5
},
{
"label": "right",
"hold_s": 5
},
{
"label": "hazard",
"hold_s": 5
}
]
},
{
"id": "9",
"title": "Lights",
"do": "Start when switched on, end when off.",
"finds": "light states",
"actions": [
{
"label": "side lights",
"hold_s": 3
},
{
"label": "dipped",
"hold_s": 3
},
{
"label": "main beam",
"hold_s": 3
},
{
"label": "flash",
"point": true
},
{
"label": "front fogs",
"hold_s": 3
},
{
"label": "rear fog",
"hold_s": 3
}
]
},
{
"id": "10",
"title": "Doors",
"do": "Start as it opens, end when it is shut. Skip what the car does not have.",
"finds": "door switches",
"actions": [
{
"label": "driver door"
},
{
"label": "passenger door"
},
{
"label": "rear left door"
},
{
"label": "rear right door"
},
{
"label": "boot"
},
{
"label": "bonnet"
}
]
},
{
"id": "11",
"title": "Wipers",
"do": "Start when switched on, end when off.",
"finds": "wiper states",
"actions": [
{
"label": "wipers int",
"hold_s": 5
},
{
"label": "wipers low",
"hold_s": 5
},
{
"label": "wipers high",
"hold_s": 5
},
{
"label": "washer"
}
]
},
{
"id": "12",
"title": "A/C and blower",
"do": "A/C: start on, end off. Blower: start as you turn it up, end when it is back down.",
"finds": "A/C request, blower",
"actions": [
{
"label": "ac",
"hold_s": 5
},
{
"label": "blower"
}
]
},
{
"id": "13",
"title": "Seatbelt",
"do": "Start as it clicks in, end as it comes out.",
"finds": "belt switch",
"actions": [
{
"label": "belt",
"hold_s": 3
}
]
}
]
},
{
"title": "Engine running, parked",
"steps": [
{
"id": "14",
"title": "Engine start",
"do": "Cold start if possible: mark as the starter turns, then idle 60 s.",
"finds": "starting, coolant and oil warming, idle control; after standing overnight, every temperature sensor reads the outside temperature at the first frames, which scales 449 bytes 0, 1, 3 against OBD-II intake and coolant",
"actions": [
{
"label": "engine start",
"point": true
},
{
"label": "idle",
"hold_s": 60
}
]
},
{
"id": "15",
"title": "Revs",
"do": "Neutral, clutch up. Start when the revs are steady at the target, end as you leave it.",
"finds": "engine speed against OBD rpm, throttle against pedal",
"actions": [
{
"label": "rpm 1500",
"hold_s": 5
},
{
"label": "rpm 2000",
"hold_s": 5
},
{
"label": "rpm 3000",
"hold_s": 5
}
]
},
{
"id": "16",
"title": "Blips",
"do": "Three sharp blips of the throttle.",
"finds": "response timing of throttle, rpm, pressure",
"actions": [
{
"label": "blips"
}
]
},
{
"id": "17",
"title": "A/C under load",
"do": "A/C on for 20 s, then off.",
"finds": "compressor clutch, idle-up",
"actions": [
{
"label": "ac",
"hold_s": 20
}
]
},
{
"id": "18",
"title": "Radiator fan",
"do": "Idle until the fan cuts in, if it will; mark when you hear it.",
"finds": "fan state, coolant threshold",
"actions": [
{
"label": "fan",
"point": true
}
]
},
{
"id": "32",
"title": "Temperature dial",
"do": "Engine warm (the heater needs it), blower on, A/C as it is. Sweep: from coldest to hottest a click or two at a time, a few seconds at each, then back; start at coldest, end back at coldest. Then hold hottest 5 min, and coldest 5 min, windows shut.",
"finds": "the dial setting on CAN (a byte stepping with each click); whether 449 byte 0 is the cabin temperature (it should drift up over the hot hold, down over the cold one)",
"actions": [
{
"label": "dial sweep"
},
{
"label": "hold hottest",
"hold_s": 300
},
{
"label": "hold coldest",
"hold_s": 300
}
]
}
]
},
{
"title": "Driving (a passenger presses, or skip the marks)",
"steps": [
{
"id": "19",
"title": "Steady speeds",
"do": "Where safe, hold each speed 10 s. Or do step 24 on the jack stands instead.",
"finds": "road and wheel speeds against OBD and GNSS speed",
"actions": [
{
"label": "steady 20",
"hold_s": 10
},
{
"label": "steady 40",
"hold_s": 10
},
{
"label": "steady 60",
"hold_s": 10
}
]
},
{
"id": "20",
"title": "Stops",
"do": "Start as you brake, end when stopped.",
"finds": "brake pressure, deceleration",
"actions": [
{
"label": "gentle stop"
},
{
"label": "firm stop"
}
]
},
{
"id": "21",
"title": "Corners",
"do": "A few corners each way.",
"finds": "yaw, lateral g, wheel speed differences",
"actions": [
{
"label": "corners"
}
]
},
{
"id": "22",
"title": "Through the box",
"do": "Each gear in turn.",
"finds": "gear from rpm / speed",
"actions": [
{
"label": "gears"
}
]
}
]
},
{
"title": "On jack stands (front wheels up, rears chocked, handbrake off)",
"steps": [
{
"id": "23",
"title": "Steering, unpowered",
"do": "Engine off, wheels in the air. Centre, full left, full right, back to centre, slowly.",
"finds": "steering angle with nothing else moving",
"actions": [
{
"label": "steer"
}
]
},
{
"id": "24",
"title": "Wheel speeds",
"do": "Engine running. Hold each speedometer reading 10 s; start when it is steady, end as you leave it.",
"finds": "vehicle and wheel speeds (fronts turning, rears still), speedometer against CAN, rpm against gear. The ABS light may come on; that is a find too.",
"actions": [
{
"label": "2nd 20",
"hold_s": 10
},
{
"label": "2nd 40",
"hold_s": 10
},
{
"label": "3rd 40",
"hold_s": 10
},
{
"label": "3rd 60",
"hold_s": 10
}
]
},
{
"id": "25",
"title": "Brake from speed",
"do": "3rd at 40 on the speedometer, clutch down, brake to a stop. Start as you brake, end when the wheels stop.",
"finds": "brake with the engine running (vacuum assist), wheel deceleration",
"actions": [
{
"label": "brake from 40",
"repeat": 2
}
]
},
{
"id": "26",
"title": "Reverse",
"do": "Reverse gear, wheels turning gently, 10 s.",
"finds": "reverse light, wheel direction (if the speeds are signed)",
"actions": [
{
"label": "reverse",
"hold_s": 10
}
]
},
{
"id": "31",
"title": "One wheel at a time, by hand",
"do": "Ignition on, engine off, neutral. A front: hold the other front still (open differential: in neutral the turned wheel spins the gearbox, not its partner) and spin this one briskly by hand, forward, about one turn a second; start as it turns, end as you stop. A rear, if you raise it: rears turn freely, no holding needed. Fronts are enough if the rears are not up.",
"finds": "which word of 235 is which wheel, one at a time (the drives already point to bytes 0-1 front right, 2-3 front left, 4-5 rear right, 6-7 rear left); also the slowest speed the ABS reports",
"actions": [
{
"label": "front left by hand",
"hold_s": 10
},
{
"label": "front right by hand",
"hold_s": 10
},
{
"label": "rear left by hand",
"hold_s": 10
},
{
"label": "rear right by hand",
"hold_s": 10
}
]
}
]
},
{
"title": "IMU calibration (module fixed in its final place)",
"steps": [
{
"id": "27",
"title": "IMU level, facing one way",
"do": "Parked on the flattest ground at hand, engine off or idling, nobody moving inside. Start, keep still 15 s, end.",
"finds": "which way is down for the IMU (with step 28, cancelling the ground's slope)",
"actions": [
{
"label": "imu level A",
"hold_s": 15
}
]
},
{
"id": "28",
"title": "IMU level, turned round",
"do": "The same spot, nose the other way: a rough 180° will do. Still again, 15 s.",
"finds": "the second half of the level: the slope's tilt reverses, the mounting's does not",
"actions": [
{
"label": "imu level B",
"hold_s": 15
}
]
},
{
"id": "29",
"title": "IMU pull-away, straight",
"do": "A straight, flat section. From standstill, a firm pull-away in 1st to about 30. Start as the clutch bites, end as you lift.",
"finds": "which way is forward for the IMU",
"actions": [
{
"label": "imu pull away",
"repeat": 3
}
]
},
{
"id": "30",
"title": "IMU braking, straight",
"do": "The same section. From about 40, a firm (not emergency) stop. Start as you brake, end when stopped.",
"finds": "forward again, from the other side; also braking g",
"actions": [
{
"label": "imu brake",
"repeat": 3
}
]
}
]
}
]
}
+85
View File
@@ -0,0 +1,85 @@
---
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).
svc-gw caps its socket's send rate (`SO_MAX_PACING_RATE`, ~5 MB/s; project-yukart
decision 28, proposed). Unpaced bursts toward the tablet stall the USB link for
seconds; paced traffic does not (app-flutter `docs/impl/reports/LINK-TEST-2026-10-11.md`).
State messages are far below the cap; it matters for utterances and anything bulky.
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`, `boot_id` (the host's, changes every boot), `source` (`bus` \| `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) |
With `source: bus`, `signals` is the newest envelope of every signal svc-gw has
seen on `yukart/vehicle/+`, `yukart/host/+`, `yukart/gnss/+` and `yukart/imu/+`. svc-gw marks one
`quality: "stale"` when it has not been refreshed for 5 s, or is known only from
the broker's retained copy (which may predate a reboot). A signal once seen is
never removed.
`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 |
| `poweroff` | `confirm: "poweroff"`, `boot_id` (from this connection's `hello`) | acks, then shuts the host down cleanly (`systemctl poweroff`) about 2 s later. Refused unless svc-gw runs with `--allow-power` (the Orin's unit only), and when `boot_id` is not the current boot's |
| `reboot` | `confirm: "reboot"`, `boot_id` | the same, with `systemctl reboot`; the same flag and refusals |
A repeated `id` returns the original `ack` (last 256 kept, shared across
connections) and never re-runs the command.
`poweroff` and `reboot` are the exception to retry-until-acked: the ack cache does
not survive the restart they cause, so a resend could shut the next boot down (or
reboot it again). The client sends them once and does not resend; `boot_id` makes
svc-gw refuse one from a previous boot regardless.
## Not yet
- TTS utterances (id, chunk sequence, terminator, expiry) — when there is a TTS.
- `gnss/fix` as one object (navigation's input): the envelope's `value` does not allow
one yet. Displays get the fix as single values on `yukart/gnss/<signal>` instead.
- Any version beyond `proto: 0`. A breaking change bumps it; the client checks it.
+12 -2
View File
@@ -9,7 +9,7 @@ type: guidelines
- ⚠ **The unit appears in both the topic name and the payload, so they can - ⚠ **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 contradict each other.** Generating both from one entry is what stops that; a
hand-written `"unit"` reintroduces the failure it was meant to prevent. 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 - Signal names carry their unit (`gnss_speed_kph`, `obd_coolant_temp_c`), so a unit
mismatch is visible at the call site rather than one registry lookup away. mismatch is visible at the call site rather than one registry lookup away.
## 2. Two clocks, and they answer different questions ## 2. Two clocks, and they answer different questions
@@ -33,4 +33,14 @@ type: guidelines
- There is no version in any topic or payload. A recorded session names the - 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. contracts tag it was captured under, which is what keeps old captures readable.
## 6. To be added ## 6. A decoded signal's layout lives only in its registry entry
- Every `source: z27ag` signal has a `can` block in `signals.json`: frame ID, byte,
bit or length, byte order, scale and offset. The decoder reads those blocks and
nothing else, so a correction lands in one file and every consumer sees it.
- ⚠ **No CAN ID or bit position is written in any service's code.** A layout
hard-coded downstream is a second source that silently disagrees the day the
registry is corrected.
- Each block names the report with its evidence (`evidence`). A layout without
evidence is a guess and does not go in.
## 7. To be added
+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 `system/<service>` have their fields named in `topics.json` but no JSON Schema
yet. They get one when a publisher exists to constrain. yet. They get one when a publisher exists to constrain.
**The app ↔ `svc-gw` boundary protocol.** Bus topics are defined here, but the **The app ↔ `svc-gw` boundary protocol** is at v0 in [BOUNDARY.md](../../BOUNDARY.md):
two channels across the boundary (latest-wins state down, acknowledged commands heartbeat both ways, idempotency key on every command, mock flagged per message.
up) have no wire format yet. Project-yukart decision 24 (proposed) already Still missing from it: the TTS utterance shape (id, chunk sequence, terminator,
fixes what it must carry, because the USB link drops for ~3 s at every power expiry), defined when a TTS exists to send one.
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.
## Order ## Order
+123
View File
@@ -0,0 +1,123 @@
---
type: plan
status: ready
updated: 2026-10-15
---
# Z27AG decoding captures: what to do in the car
Recordings that let the broadcast frames be decoded: one thing changes at a time,
each change is marked from the app's **Captures** page, and OBD-II mode 01 values are recorded alongside as the
ground truth. Prior art and first candidates: [the prior-art report](../reports/Z27AG-CAN-PRIOR-ART.md).
## What records what
| In the session directory | From | Use |
|---|---|---|
| `can-*.log(.zst)` | the PEAK, every frame, kernel timestamps | what gets decoded (nothing transmits on CAN: OBD-II is on the K-line) |
| `vehicle.ndjson` | `decoder` and `obd-poller` via the bus | decoded CAN signals, and the `obd_*` references from the K-line through the ELM327 (rpm, speed, throttle, load, MAF, timing, O2 every round; coolant, intake air, trims every 5 s); `env.timestamp` is the reply's arrival at the Orin |
| `events.ndjson` | marks from the app | when each step happened |
| `gnss.ndjson` | gpsd (SIM7600) | speed and position for the drive |
| `host.ndjson` | `orin-status` via the bus (`yukart/host/*`) | the Orin's own state beside the drive: temperatures, load, power, links, UPS (since 2026-10-16) |
| `imu.bin` (`imu.ndjson`) | `imu` via the bus (`yukart/raw/imu`) | every IMU sample, packed (svc-core `imu --export` prints it as JSON lines; sessions before 2026-10-16 and anything that does not pack are in `imu.ndjson`), raw module axes, Orin receive time: calibration (27–30) and, once calibrated, motion for the drive |
## Operating it from the app
- **Sessions start by themselves** when the PEAK sees traffic (ignition on) and end
15 s after it stops. There is no start/stop button; nothing to do.
- **Captures** (top bar) opens the checklist. Each step shows what to do and one big
button: **Start** as the action begins, **End** as it ends (a running timer shows
the suggested hold); a single moment is one **Mark**. When a step's actions are done
the button says **Next step**: a natural pause, about 3 s, before the next.
**Redo step** and **Skip step** say so in the recording; the step list (left, or
the list icon on a narrow screen) jumps anywhere. Leaving a step mid-action marks
that action aborted.
- **Check it landed:** under the button, the last mark and `✓`, or `✗ error: no
session` when there is no traffic yet (ignition on, PEAK plugged). A mark sent with
the link down waits (`unacked`) and goes when it is back, with its delay noted.
- The **Mark** button on the main screen is still there for anything off-plan.
- **OBD is working** when `obd_engine_rpm`, `obd_coolant_temp_c` and the rest appear
in the signal list and move (the ELM on the OBD port, through the Y harness beside
the PEAK). If they never appear, see *If OBD stays silent*.
## The steps
They are data: [`checklists/z27ag-captures.json`](../../../checklists/z27ag-captures.json),
bundled into the app as an exact copy (app-flutter `tool/sync-contracts.sh`;
crosscheck fails if the copy drifts). Edit them there; bump `version` when steps
change meaning, since every recording names the version it was made with. Before
starting: PEAK on the OBD port, Orin up, tablet connected, and `can_pcan` UP (the
recorder refuses marks with "no session" otherwise); engine off, doors shut.
- **Parked, ignition on, engine off, handbrake on** (1–13): baseline, pedals, steering, gears
(reverse likely shows as the reverse light: the diesel forfour carries it in
`423` byte 1 bit 3; no other gearbox signal expected), handbrake, indicators,
lights, doors, wipers, A/C, belt.
- **Engine running, parked** (14–18): start and idle, held revs in neutral (no OBD
on this car's CAN: the revs are approximate, read off the tacho), blips, A/C load, radiator fan.
- **Driving** (19–22): steady speeds, stops, corners, each gear. A passenger presses,
or skip the marks: GNSS speed is recorded continuously. The gear is not on CAN and
is not inferred (decode report).
- **On jack stands** (23–26, since v2): front wheels up, rears chocked, handbrake
off. Unpowered steering; speedometer readings held in 2nd and 3rd (fronts turn,
rears stay still, which tells the wheels apart); braking from speed with vacuum
assist; reverse. Stands replace the driving speeds where a drive is not practical. Step 31
(since v4, kept in this section, numbered after the IMU steps so earlier numbers stay): each
wheel spun by hand, the other front held still; the rears only if they are raised too.
- **IMU calibration** (27–30, since v3): once the IMU is fixed in its final place. Level
twice on the same spot, facing one way and then turned round (a rough 180° is fine:
the ground's slope reverses and cancels, the mounting's tilt does not); then three
firm pull-aways and three firm stops on a straight, flat section (forward, signed by
which kind of stretch it is). Any session holds raw IMU samples (`imu.bin`), so
these can go in their own short session or with anything else.
Version 5 (2026-10-15) adds step 32, the temperature dial (engine running section), and the overnight soak to step 14. Version 4 (2026-10-15) adds step 31, one wheel at a time by hand. Version 3 (2026-10-14) adds the IMU calibration section. Version 2 (2026-10-13) adds the stands section and writes down the conditions the
first run showed matter: engine off and handbrake on for 1–13, foot brake held for
the handbrake step, neutral with the clutch up for the revs.
The parked steps can be split over several sessions; the drive is the important part.
## Marks, as recorded
`events.ndjson` holds each mark's text, stamped on arrival at the recorder:
| Text | Meaning |
|---|---|
| `checklist z27ag-captures v1` | before the first mark from the page: which steps these are |
| `3 brake #2 start` / `3 brake #2 end` | a span; `#n` only on repeated actions |
| `14 engine start` | a point |
| `3 brake #2 abort` | a span left open (redo, skip, another step) |
| `3 redo`, `3 skip` | the step is being done again / was skipped: use the last attempt |
## If OBD stays silent
`yukart/system/obd-poller` says why (visible in `mosquitto_sub -v -t 'yukart/system/#'`
on the Orin), and the signal list's `elm_connected` shows the Bluetooth link:
- `ELM …: … (unplugged, asleep or out of range)`: no Bluetooth link. The ELM in the
port? Its light on? A phone app holding it (it takes one connection)? Retried
every minute.
- `ECU not answering (…): ignition off?`: the ELM is there, the car is not. The
adapter's own words are in the brackets (`BUS INIT: ...ERROR`, `NO DATA`).
- `ECU silent for 60 s: adapter released`: ignition off for a minute; the poller let
go so the ELM can sleep, and tries again every minute.
- To see what answers, on the Orin: `sudo systemctl stop yukart-obd-poller`,
`yukart-obd-poller --elm $(. /etc/default/yukart-elm; echo $ELM_ADDR) --once`
(adapter, protocol, supported PIDs and one value each, with reply times), then
`sudo systemctl start yukart-obd-poller`.
## Afterwards
**IMU calibration**, on the car Orin, from the session that holds steps 27–30:
```bash
yukart-imu --calibrate /var/lib/yukart/recordings/<session> # print: down, forward, slope, spread
yukart-imu --calibrate /var/lib/yukart/recordings/<session> --write # and save /var/lib/yukart/imu/mount.json
```
The service picks the new mounting up within seconds; roll and pitch should then read
about 0 parked on level ground. Steps 27–28 alone also work: forward then stays the
`--forward` axis, laid flat, until a session with 29–30 is calibrated.
Sessions are in `/var/lib/yukart/recordings/` on the car Orin. Copy one off with
`rsync -a mikkeli-orin-nano:/var/lib/yukart/recordings/<session>/ …` into
`yukart-data`, with a line in its README of what was done.
+123
View File
@@ -0,0 +1,123 @@
---
type: report
date: 2026-10-09
updated: 2026-10-13
---
# 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.
- **The smart forfour 454 is the useful sibling.** A sniffed 2006 forfour diesel
shares our body/chassis frames (`0C0`, `119`, `312`, …) but not the engine ones;
its notes are the first hypothesis for every shared ID.
- **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) |
| [billyjack2/CAN-DBC-Collection](https://github.com/billyjack2/CAN-DBC-Collection) `MITSUBISHI/COLT/can1v3.dbc` | AiM logger driver "Mitsubishi Colt" as a DBC: wheel speeds `200`/`208`, pedal `210` byte 2, brake switch `300`, rpm `308`, speed and fuel `412`, battery `416`, coolant `608` | The EU Colt / forfour engine-side set (it matches the forfour notes below), **not the Z27's**: of its IDs only `210` and `300` exist here, and neither fits — `210` byte 2 stays `08` through a throttle blip while bytes 4–5 follow rpm; `300` is 4 bytes, not 8 |
| [I2000C/SmartCruiseControl](https://github.com/I2000C/SmartCruiseControl) `doc/CAN bus reverse engineering.md` (MIT) | Smart forfour 454, 2006, **1.5 CDI diesel**, sniffed: ~30 IDs with lengths and byte notes. Decoded: `09E` odometer (bytes 4–6, 0.1 km), `200` handbrake + three speeds (×0.05 km/h, big-endian), `208` brake intensity + speeds, `210` pedal (byte 2), `212` pedal sensor 2, `308` rpm (bytes 1–2), `408` fuel litres, `40C`/`412` dash speed, `416` battery (0.1 V), `423` ignition / lights / indicators / doors bitfield, `608` coolant (−40) and fuel rate | **The closest relative found.** Body and chassis frames match ours: `0C0` (5 bytes, byte 3 always `07`, byte 4 counter; ours `6DFF0007xx`), `119` (byte 5 `08`), `312` (four 16-bit words), and `2F1`, `300`, `443`, `423` exist on both. The engine-side frames do not (`308`, `608`, `200`/`208` absent here; `210`, `423` other lengths and layouts) — plausibly because that car's engine ECU is Mercedes' diesel one and ours is Mitsubishi's 4G15T |
The Z27 Colt is a 2002 platform shared with the smart forfour (W454); the CZT /
Ralliart's 4G15T is also the forfour Brabus engine, and parts listings show the
sharing reaches the electrics (Z30 engine ECUs carry Mercedes-style `A135…` part
numbers; the steering rack is the 454's). The forfour notes bear that out for the
body bus. Their layouts are the first hypothesis for every shared ID, and their
engine-side IDs a hint of what a petrol forfour or EU Colt would add.
**The forfour Brabus is the sibling to look for.** EcuTek's [Colt CZT tuning
manual](https://ecutek.zendesk.com/hc/en-gb/article_attachments/204299089/Mitsubishi_Colt_CZT_-_Tuning.pdf)
(2005-on "CAN ECUs"): "The Smart ForFour Brabus edition is basically a Colt CZT
with the fibreglass body"; one ProECU package tunes both. Brabus owners run CZT
ECUs (MH8304F; ECUFlash definitions exist for CZT ROM 47110032 and Brabus
49100033, [EvolutionM](https://www.evolutionm.net/forums/ecu-flash/767782-xml-file-smart-brabus-w454.html)).
So a Brabus's engine-ECU frames should be ours (if the JDM Ralliart's ECU
matches the EU CZT's), while its body frames come from smart's SAM module: on the
454 "the SAM … sits at the awkward edge of Mitsubishi engine management and
Mercedes body electronics" ([smartcarmanuals](https://smartcarmanuals.com/models/454-forfour/)).
Expect, from a Brabus capture: engine frames equal, chassis frames (ABS, the shared
electric steering) probably equal, body frames different. No Brabus or CZT capture
is public, and none is being sought: the Z27's own recordings are the source. The same manual notes that the **Euro CZT has no MAP sensor**, so OBD PID
`0B` may be unsupported on the Z27 (`obd-poller` discovers that by itself). The smart
fortwo 451 ([hackaday.io](https://hackaday.io/project/19931-smart-fortwo-can-bus-reverse-engineering))
shares `423` as the cluster status frame, little else.
### 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.
## Automated decoding, in the literature
Published tools do what the capture checklist sets up: record known values next to
the raw frames and search the frames for them.
[LibreCAN](https://rtcl.eecs.umich.edu/rtclweb/assets/publications/2019/fv075-peseA.pdf)
fits OBD-II replies for powertrain signals and phone sensors for body ones (GNSS
plays that part here); [CAN-D](https://www.ornl.gov/publication/can-d-modular-four-step-pipeline-comprehensively-decoding-controller-area-network-data)
and [ByCAN](https://arxiv.org/html/2408.09265v1) find signal boundaries from bit
flip rates, then byte order and signedness, then match. None reads our logs; the
method — classify bits (counter, constant, live), cut signals, try both
endiannesses, fit against OBD values at matching timestamps, rank — is the
analysis tool to write.
## 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.
+70
View File
@@ -0,0 +1,70 @@
---
type: report
date: 2026-10-13
---
# Z27AG CAN: first decoding pass
Session `z27ag-20261013-170050-checklist` (yukart-data `can-captures/`), checklist v1,
117 marks. Method: list every transition of every rarely-changing bit and read it
against the marks; for blinking signals, count toggles inside each marked range
against the seconds before it; for values, dump bytes across the marked range; for
speed, correlate every 16-bit word with GNSS speed over the crawls. Marks lag the
action by a fraction of a second to a few seconds (the operator presses after doing),
so a change shortly *before* a start mark is normal.
Bit numbering: `ID.byte.bit`, byte 0 first, bit 7 the MSB.
## Found
| Signal | Where | Evidence | Confidence |
|---|---|---|---|
| Engine speed | `210` bytes 4–5, BE, 1 rpm | idle 740–870; holds at ~1410 and ~1900 for "1500" / "2000" (operator: revs not exact); blip peak 2887 | High |
| Engine speed, finer | `13D` bytes 0–1, LE, ≈ rpm × 2.048 | moves in lockstep with `210` | High |
| Accelerator pedal | `210` byte 0, 0x00 released … 0xFA full (÷2.5 → %) | engine-off sweep: 0 → 0xFA → 0 | High |
| Accelerator sensors 1 and 2 | `139` bytes 0 and 1 (raw, 0x33 rest … 0xEE full, track each other) | same sweep | High |
| Throttle-related | `139` byte 2 (0x21 … 0xD5) | same sweep; plate or target | Medium |
| Pedal-pressed flag | `210` byte 3 bit 7 | set exactly while the pedal is off rest | Medium |
| Brake pedal switch | `231.4.1` (1 = pressed) | 3/3 presses, plus the foot-brake holds around the handbrake step and the crawls | High |
| Handbrake | `239.0.2` (1 = applied) | set through every engaged stretch, clear during the crawls and the handbrake releases | High |
| Left indicator | `423.2.6` blinking | 2/2 | High |
| Right indicator | `423.2.5` blinking | 2/2 | High |
| Hazards | `423.2.6` only blinks, 2/2 | odd: `2.5` stays still. Maybe `2.6` is "left or hazard", maybe the hazard has its own bit elsewhere at a rate this method missed | Low |
| Side lights | `423.2.2` and `570.7.3` (1 = on) | on/off and during dipped | High |
| Dipped beam | `423.3.1` (1 = on) | 1/1 | Medium (one sample) |
| Any door open | `423.2.1` | driver and passenger | Medium |
| Driver door | `423.0.4` | driver only, not passenger | Medium |
| Driver belt unbuckled | `573.5.7` (1 = unbuckled) | set at ignition on, cleared at buckle; no release seen | Low |
| A/C request / blower | `443` bytes 1–2 and `711` bytes 5–6, multi-bit | change with A/C and blower; not separated yet | Low |
| A/C compressor engaged | `13D.6.7` | engine running, A/C on; set ~0.5 s after the request, cleared with A/C off | Medium |
| Four wheel speeds | `235` four 16-bit words (bytes 0–1, 2–3, 4–5, 6–7) | zero parked, all four move together in the crawls | High (signal), **scale open** |
| Vehicle speed | `231` byte 6 (and probably byte 7) | tracks the wheel words | Medium |
**Wheel-speed scale.** Crawls topped out at ~6 km/h, and so far only the first byte of each word moved
(0x4C at GNSS 3.5 km/h). Read as little-endian ×0.05 km/h (the forfour's `200`
scale), that is 3.8 km/h, which is plausible given GNSS lag. Big-endian is not ruled out. The
jack-stands session and a GNSS-matched drive settle both the byte order and the scale.
## Not on this bus (or not seen)
- **Clutch and transmission are not on this bus.** Nothing changed for the clutch
(3 presses), any of the 5 gears, neutral or reverse: nothing on the car consumes
them, so no frame carries them. Reverse was rechecked against both reverse
stretches (engine off and engine on) versus every other position: no bit
separates them.
- **Gear is not inferred from rpm ÷ wheel speed** (owner's call, 2026-10-13): the
clutch is invisible, so the ratio lies whenever it is down or slipping, and the
gear matters most at a standstill, where the ratio says nothing. A *recommended*
gear from speed and rpm is the useful form, if any.
- **Steering angle:** not analysed yet (step 5 was done with the engine on, at the end).
## Not analysed yet
- `312` (four noisy 16-bit words that follow the throttle: MAP/load/air candidates)
- `449` temperatures (coolant candidate), `710`–`713` slow body/meter frames
- `0C0`, `119`, `149`, `156` (counters/checksums; `149` inertial candidate)
## Scripts
Scratch analysis for now: a session loader, a transition timeline, a blink counter and a
GNSS correlator, in Python with numpy. They become a tool once a DBC exists to check
against.
+78
View File
@@ -0,0 +1,78 @@
---
type: report
date: 2026-10-15
---
# Z27AG CAN: second pass, against OBD-II and GNSS
Sessions `000007-20261015T192938` and `000008-20261015T200202` (car Orin): doors, tailgate,
bonnet and wipers (checklist step 10–11), then a short drive with GNSS (3D fix, helix antenna)
and OBD-II through the ELM327 on the K-line, all on the Orin's clock. The first time CAN, OBD-II
and GNSS were recorded together. About 30 minutes; 1190 OBD-II samples of each fast value.
## Confirmed
| Signal | Where | Evidence | Registered as |
|---|---|---|---|
| Engine speed | `210` bytes 4–5, BE, 1 rpm (pass 1) | equal to OBD-II `0C`: median ratio 1.0000, median difference 4 rpm, 1190 samples | `engine_rpm` (unchanged) |
| Coolant temperature | `449` byte 2, −40 °C | equal to OBD-II `05` within ±1 °C (median 0.0) over 86–97 °C; no other byte tracks it | `coolant_temp_c` |
| Vehicle speed | `231` bytes 6–7, **little-endian**, **0.0577 km/h per count** | steady cruising (> 15 km/h, GNSS change < 1 km/h over 2 s, 117 fixes): spread 2.6 % against GNSS | `vehicle_speed_kph` |
| Four wheel speeds | `235` four words, **little-endian** (pass 1 left the byte order open) | big-endian misses by 28 km/h rms; little-endian fits. Words 0, 1: 0.0578; words 2, 3: 0.0581 km/h per count against GNSS | not yet: which word is which wheel waits for the jack-stands run (fronts turn, rears stay) |
Scales are against **GNSS**, so they include this car's tyres today. **OBD-II speed reads 3 % low**
against GNSS (0.9706): the ECU's figure is no better a reference than the ABS frames. The two
word pairs of `235` differ by 0.5 %, the usual size of a front/rear tyre difference.
## Doors, tailgate, bonnet, wipers
| Thing | Bit | Notes |
|---|---|---|
| Driver's door | `423.0.4` | its own bit (pass 1); also sets any-door |
| Passenger, rear left, rear right | only `423.2.1` (any door) | no bit of their own |
| Tailgate | only `423.2.1` | with the driver's door open, `423.0.4` cleared on closing it while `423.2.1` stayed set until the tailgate shut |
| Bonnet | nothing | a mechanical latch with no switch, as expected |
| Front wiper running | `423.3.3` | set through "low" and "high"; nothing in a 4 s "interval" window. Medium confidence; also explains pass 1's unexplained `423.3.3` stretch |
## IMU calibration, checked against GNSS
Level from session `000006` (two still stretches, the ground's 1.9° slope cancelled), forward from
`000008` (three stops, one launch; 3512 samples, mean spread 12.7°). The module sits upside down,
about 24° off level, turned so its X axis points forward. Calibrated, the marked stretches read:
| Stretch | IMU longitudinal (0.25 s mean, peak) | GNSS |
|---|---|---|
| Stop, 48 → 0 km/h | −0.35 g | −0.28 g |
| Stop, 45 → 0 | −0.17 g | −0.17 g |
| Stop, 40 → 0 | −0.21 g | −0.18 g |
| Launch | +0.32 g | +0.36 g |
Lateral stays at about 0 in all four: forward is straight ahead. GNSS reports about once a second,
which smooths short peaks.
## Session 000009: lights at night, corners
`z27ag-20261015-203117-lights-corners-drive`: step 9 at night, then a 35-minute errand drive
(no OBD-II: the obd-poller was off after the freeze). References: GNSS and the calibrated IMU.
| Signal | Where | Evidence | Registered as |
|---|---|---|---|
| Main beam | `423.2.7` | set exactly through the marked stretch; also fired once with the other lights off: flash-to-pass | `main_beam` |
| Front fog lamps | `423.2.4` | set exactly through the marked stretch (lights on) | `front_fogs` |
| **Steering wheel angle** | `0C0` bytes 0–1, little-endian, **signed**, **0.1° per count**, positive left | correlation **+0.995** with how sharply the car turns (IMU yaw rate ÷ speed), 1021 s of driving. Lock to lock −479…+471°; with a 2.5 m wheelbase that is a 16.3:1 steering ratio, typical. Straight driving reads −2.7° (camber or alignment) | `steering_angle_deg` (the layout format gained `signed`) |
| Dipped beam? | `423.3.1` | clean in daylight on the 13th; at night it flickers on for 0.1–0.2 s at each switch movement. The cluster has **no dipped-beam lamp** (only side lights, main beam and fogs, no rear fogs), and `423` matches the cluster's lamps, so this bit is probably something else that moves with the lights (illumination, dimming). A walk through Off → Auto → Dim → Main settles it | `dipped_beam` kept, description says so |
**IMU, lateral axis:** lateral g against speed × yaw rate gives a slope of **−0.998** (correlation
−0.91; the sign is right: yaw counts a left turn positive, lateral g counts right positive). With the
stops above, both horizontal axes of the calibration are confirmed.
`149` bytes 1–2 is not a yaw-rate sensor (correlation 0.02 with the IMU's yaw rate).
## Not found
- **Intake air temperature:** no byte of `449` tracks OBD-II `0F` (29–51 °C here).
- `449` bytes 0, 1, 3 move like temperatures but match nothing on OBD-II.
## Notes on the recording
- **The Orin froze mid-drive** (19:59; hardware watchdog reset at 20:01:47), splitting the drive
over two sessions. orin-nano-env-setup has the investigation.
- OBD-II through the ELM gave each fast value about every 2 s (K-line, ~250 ms per request).
+98
View File
@@ -0,0 +1,98 @@
---
type: report
date: 2026-10-15
---
# Z27AG CAN: third pass, the drive back
Sessions `000011-20261015T212904` and `000013-20261015T214544` (the drive back, about 13 minutes,
no marks) together with pass 2's `000008` and `000009`: 20 km of driving with GNSS and the calibrated
IMU, no OBD-II. Nothing was marked, so this pass uses what any drive has: corners and distance.
## Confirmed
| Signal | Where | Evidence | Registered as |
|---|---|---|---|
| Wheel speeds | `235` four words, little-endian (pass 2): bytes 0–1 **front right**, 2–3 **front left**, 4–5 **rear right**, 6–7 **rear left** | from corners, below | `wheel_speed_fr_kph`, `_fl_`, `_rr_`, `_rl_` |
| Odometer | `570` bytes 4–6, little-endian, **1 km** | steps once per kilometre: 12 steps over 11.9 km (`000009`), 3 over 3.0 km (`000011`); 167966 on the 13th, 167988 at the end of the drive back; valid with the engine off too. Agrees with the cluster's (digital) odometer | `odometer_km` (the layout format gained 24-bit fields) |
| Distance counter | `14D` bytes 6–7, little-endian, **about 0.40 m** per count | tracks distance from the wheel speeds with correlation 1.0000 in both drives (103.2 and 103.6 m per step of byte 7); wraps every ~26 km | not registered. The cluster's two trip meters count 0.1 km and may run from this; it is also the finest distance on the bus (slow creeping, short manoeuvres) |
### Which word is which wheel
In a corner the outer wheels run faster than the inner ones, by about half the track width times the
curvature; and the fronts run on a wider arc than the rears, by about half the wheelbase squared times
the curvature squared. Fitting each word's share of the four (its speed ÷ their mean) against the
curvature from the steering angle (`steering_angle_deg` + 2.7° for the straight-ahead offset, ÷ 16.3,
over a 2.5 m wheelbase), 74 476 frames above 15 km/h:
| Word | Linear term (track) | Square term (wheelbase) | So |
|---|---|---|---|
| 0 | +0.72 m | +1.4 m² | right, front |
| 1 | −0.72 m | +1.4 m² | left, front |
| 2 | +0.75 m | −0.9 m² | right, rear |
| 3 | −0.75 m | −1.9 m² | left, rear |
Curvature is positive turning left, so a positive linear term is a wheel on the right (outside in a left
turn). The size, ±0.72–0.75 m, is half the Colt's track (1.45–1.46 m). The square terms split the words
into a pair above the mean and a pair below it, ±1.5 m² against an expected ±1.6 m² (2.5² ÷ 4). Medians
by curvature agree: in left turns tighter than 20 m (`κ > 0.05`) the words read 1.068, 0.956, 1.044,
0.932 of their mean.
Straight ahead the fronts read 0.6 % more counts than the rears, matching pass 2's per-word scales
(0.0578 against 0.0581 km/h per count). Driven wheels or tyre sizes, either is ordinary; each pair is
registered with its own scale, so all four read GNSS speed when cruising.
Jack stands (checklist 23–26) would still confirm this, but are no longer needed to find it.
## Candidates
- **`570` bytes 2 and 3: fuel, most likely.** Both fall slowly: byte 2 read 87 on the 13th and 76
at the end of the drive back, 22 km later. Both read `7F` with the engine off. Outside temperature
was the other reading, but the car shows it nowhere, and the bytes **slosh**. Take away their own
60 s average, and what is left follows the car's motion in all three drives:
| Byte | Lateral g | Longitudinal g | Lag |
|---|---|---|---|
| 3 | +0.70 to +0.81 | −0.42 to −0.48 | none |
| 2 | +0.64 to +0.75 | −0.48 to −0.50 | about 3 s |
Lateral g here is from the wheel speeds and the steering angle, longitudinal from the change in
wheel speed. A temperature would not do this; liquid in a tank would. Byte 3 looks like the sender
as it is, and byte 2 a damped copy (a gauge needle is damped the same way). The "rise while parked"
(78 → 81) was the same thing: the drive before ended at 80, parked in the same spot (the same
IMU tilt to three decimals). It read 76 a minute earlier, while still moving. The reading moves about
±4 with tilt and motion, so it needs averaging over minutes.
Scale unknown: full is unlikely to be 100 or 255. A refuel gives two points (before and after,
parked level, with the litres put in). Not registered until then.
- **`449` bytes 0, 1, 3: climate-control temperatures, most likely raw sensor readings.** Pass 2 looked
for a byte that equals OBD-II intake air temperature less 40 and found none. Read the other way,
bytes 1 and 3 run *against* it (correlation −0.96 and −0.98 in `000007`), the way a thermistor's raw
reading falls as it warms. They follow it over tens of minutes, not second to second, so they are
other sensors that warm and cool with the air around the engine and car:
- **Byte 3: the A/C evaporator.** At each first switch-on of the compressor (`ac_compressor`) it climbs
from 115–145 to about 185 within 30–60 s, then stays there while the compressor cycles on and off
every 10–20 s. That is the evaporator being held just above freezing, the job its sensor exists
for. It keeps climbing a few counts after each switch-off, as an evaporator does while the last
refrigerant boils off. With the A/C off it drifts with the air (the `000007` correlation above).
Higher count, colder.
- **Byte 1: outside air, probably.** Slow; 89–91 on the afternoon of the 13th, 99–117 in the evening
of the 15th (higher count, colder, as byte 3). It fell from 114 to 99 over 30 minutes parked and
idling, and rose from 112 to 117 over 8 minutes of driving. A sensor behind the grille does this:
radiator heat soaks it while standing, and the airflow cools it once moving. That is why climate
controls filter it.
- **Byte 0: unclear.** 119–134, slow, unmoved by the engine starting or stopping (so not the
charging voltage), and its sign against the other two is not consistent. A cabin sensor is one
guess.
An auto climate control has these three: evaporator, outside air, cabin. The way to scale them is
an overnight soak: a cold start after the car has stood all night gives one reading of each with
everything at the outside temperature (OBD-II intake and coolant say what that is). With a
thermometer and the A/C steps (12, 17), it gives a second. A mid-session frame with byte 6 = 24 instead
of 51 (end of `000009`, engine stopping) has every byte halved or changed; left out of the above.
## Notes on the recording
- `000012` is a 25-second parked session in between; `000013` ends with the PEAK unplugged.
- The recorder kept IMU samples as JSON lines (34 MB for `000009`). From this pass on it packs them
(`imu.bin`, svc-core `imu/packed.rs`): the same samples are 8.5 MB, and all of `000009`'s samples fit
the packed form exactly.
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://yukart/schema/envelope.json", "$id": "https://yukart/schema/envelope.json",
"title": "Signal envelope", "title": "Signal envelope",
"description": "Every decoded signal published on yukart/vehicle/* and yukart/gnss/*. Raw CAN uses the binary frame encoding instead.", "description": "Every decoded signal published on yukart/vehicle/*, yukart/gnss/* and yukart/host/*. Raw CAN uses the binary frame encoding instead.",
"type": "object", "type": "object",
"required": [ "required": [
"timestamp", "timestamp",
+832 -19
View File
@@ -1,17 +1,7 @@
{ {
"$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.", "$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 (OBD-II mode 01 over the K-line, through the ELM327): calibration references, every name prefixed obd_ (project-yukart decision 30); source=host is the Orin itself (orin-status), not the car. source=gnss is the receiver (svc-core gnss), on yukart/gnss/<signal>. source=imu is the WT61 IMU, already turned into the car's frame (mounting and gravity handled by its publisher), on yukart/imu/<signal>. source=z27ag is reverse-engineered from the car's own broadcast frames, and its 'can' block is the only place its layout is written down: id (11-bit, hex string), byte (first data byte, 0-7), length in bits (1, 8, 16 or 24), bit (length 1 only: 0 = LSB of that byte), order (lengths 16 and 24: big = byte first is most significant, little = the reverse), signed (lengths 8 and up: two's complement; default unsigned), scale and offset (value = raw * scale + offset; booleans are raw != 0). 'evidence' names the contracts report that found it.",
"signals": { "signals": {
"engine_rpm": { "obd_vehicle_speed_kph": {
"unit": "rpm",
"type": "number",
"source": "obd2",
"description": "Engine speed",
"range": [
0,
8000
]
},
"vehicle_speed_kph": {
"unit": "kph", "unit": "kph",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -21,7 +11,7 @@
255 255
] ]
}, },
"coolant_temp_c": { "obd_coolant_temp_c": {
"unit": "celsius", "unit": "celsius",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -31,7 +21,7 @@
215 215
] ]
}, },
"intake_air_temp_c": { "obd_intake_air_temp_c": {
"unit": "celsius", "unit": "celsius",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -41,7 +31,7 @@
215 215
] ]
}, },
"throttle_pct": { "obd_throttle_pct": {
"unit": "percent", "unit": "percent",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -51,7 +41,7 @@
100 100
] ]
}, },
"engine_load_pct": { "obd_engine_load_pct": {
"unit": "percent", "unit": "percent",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -61,7 +51,7 @@
100 100
] ]
}, },
"fuel_level_pct": { "obd_fuel_level_pct": {
"unit": "percent", "unit": "percent",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -71,7 +61,7 @@
100 100
] ]
}, },
"intake_pressure_kpa": { "obd_intake_pressure_kpa": {
"unit": "kpa", "unit": "kpa",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -81,7 +71,7 @@
255 255
] ]
}, },
"maf_gps": { "obd_maf_gps": {
"unit": "g/s", "unit": "g/s",
"type": "number", "type": "number",
"source": "obd2", "source": "obd2",
@@ -90,6 +80,829 @@
0, 0,
656 656
] ]
},
"obd_engine_rpm": {
"unit": "rpm",
"type": "number",
"source": "obd2",
"description": "Engine speed (OBD-II 0C): the reference for the decoded engine_rpm",
"range": [
0,
8000
]
},
"obd_timing_advance_deg": {
"unit": "degree",
"type": "number",
"source": "obd2",
"description": "Ignition timing advance before TDC, cylinder 1",
"range": [
-64,
64
]
},
"obd_stft_pct": {
"unit": "percent",
"type": "number",
"source": "obd2",
"description": "Short-term fuel trim, bank 1: positive adds fuel",
"range": [
-100,
100
]
},
"obd_ltft_pct": {
"unit": "percent",
"type": "number",
"source": "obd2",
"description": "Long-term fuel trim, bank 1: positive adds fuel",
"range": [
-100,
100
]
},
"obd_o2_b1s1_v": {
"unit": "V",
"type": "number",
"source": "obd2",
"description": "Upstream oxygen sensor voltage (narrowband: ~0.1 lean … ~0.9 rich)",
"range": [
0,
1.275
]
},
"engine_rpm": {
"unit": "rpm",
"type": "number",
"source": "z27ag",
"description": "Engine speed",
"range": [
0,
8000
],
"can": {
"id": "0x210",
"byte": 4,
"length": 16,
"order": "big",
"scale": 1,
"offset": 0,
"evidence": "Z27AG-DECODE-1"
}
},
"accel_pedal_pct": {
"unit": "percent",
"type": "number",
"source": "z27ag",
"description": "Accelerator pedal position (0x00 at rest, 0xFA flat)",
"range": [
0,
100
],
"can": {
"id": "0x210",
"byte": 0,
"length": 8,
"scale": 0.4,
"offset": 0,
"evidence": "Z27AG-DECODE-1"
}
},
"steering_angle_deg": {
"unit": "degree",
"type": "number",
"source": "z27ag",
"description": "Steering wheel angle: positive to the left. About -2.7° driving straight; lock to lock about ±475° (ratio about 16:1)",
"range": [
-600,
600
],
"can": {
"id": "0x0C0",
"byte": 0,
"length": 16,
"order": "little",
"signed": true,
"scale": 0.1,
"offset": 0,
"evidence": "Z27AG-DECODE-2"
}
},
"brake_pedal": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Brake pedal switch: pressed",
"can": {
"id": "0x231",
"byte": 4,
"bit": 1,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"handbrake": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Handbrake applied",
"can": {
"id": "0x239",
"byte": 0,
"bit": 2,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"indicator_left": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Left indicator lamp, as it flashes (on and off at the flasher's rate)",
"can": {
"id": "0x423",
"byte": 2,
"bit": 6,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"indicator_right": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Right indicator lamp, as it flashes. Hazards were seen on the left bit only: open",
"can": {
"id": "0x423",
"byte": 2,
"bit": 5,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"side_lights": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Side (position) lights on; also on with dipped beam",
"can": {
"id": "0x423",
"byte": 2,
"bit": 2,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"dipped_beam": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Probably not dipped beam: clean in daylight (2026-10-13) but flickering at each switch movement at night (2026-10-15), and the Z27AG's cluster has no dipped-beam lamp (side lights, main beam and fogs only), so frame 423, the cluster's status, has little reason to carry it. Kept under this name until a light-switch walk shows what it is",
"can": {
"id": "0x423",
"byte": 3,
"bit": 1,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"main_beam": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Main beam lamp on, flash-to-pass included",
"can": {
"id": "0x423",
"byte": 2,
"bit": 7,
"length": 1,
"evidence": "Z27AG-DECODE-2"
}
},
"front_fogs": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Front fog lamps on (they need side lights or dipped beam)",
"can": {
"id": "0x423",
"byte": 2,
"bit": 4,
"length": 1,
"evidence": "Z27AG-DECODE-2"
}
},
"door_open": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Any door open, the tailgate included (the bonnet has no switch)",
"can": {
"id": "0x423",
"byte": 2,
"bit": 1,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"driver_door_open": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Driver's door open: the only door with its own bit",
"can": {
"id": "0x423",
"byte": 0,
"bit": 4,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"ac_compressor": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "A/C compressor engaged (engine running)",
"can": {
"id": "0x13D",
"byte": 6,
"bit": 7,
"length": 1,
"evidence": "Z27AG-DECODE-1"
}
},
"vehicle_speed_kph": {
"unit": "kph",
"type": "number",
"source": "z27ag",
"description": "Road speed from the ABS (CAN), scaled against GNSS on this car's tyres: OBD-II speed reads 3 % lower",
"range": [
0,
250
],
"can": {
"id": "0x231",
"byte": 6,
"length": 16,
"order": "little",
"scale": 0.0577,
"offset": 0,
"evidence": "Z27AG-DECODE-2"
}
},
"wheel_speed_fr_kph": {
"unit": "kph",
"type": "number",
"source": "z27ag",
"description": "Front right wheel speed from the ABS (CAN), scaled against GNSS while cruising. Which word is which wheel: from cornering (outer wheels faster, fronts on a wider arc than rears), not yet checked on jack stands",
"range": [
0,
250
],
"can": {
"id": "0x235",
"byte": 0,
"length": 16,
"order": "little",
"scale": 0.0578,
"offset": 0,
"evidence": "Z27AG-DECODE-3"
}
},
"wheel_speed_fl_kph": {
"unit": "kph",
"type": "number",
"source": "z27ag",
"description": "Front left wheel speed from the ABS (CAN), scaled against GNSS while cruising. Which word is which wheel: from cornering (outer wheels faster, fronts on a wider arc than rears), not yet checked on jack stands",
"range": [
0,
250
],
"can": {
"id": "0x235",
"byte": 2,
"length": 16,
"order": "little",
"scale": 0.0578,
"offset": 0,
"evidence": "Z27AG-DECODE-3"
}
},
"wheel_speed_rr_kph": {
"unit": "kph",
"type": "number",
"source": "z27ag",
"description": "Rear right wheel speed from the ABS (CAN), scaled against GNSS while cruising. Which word is which wheel: from cornering (outer wheels faster, fronts on a wider arc than rears), not yet checked on jack stands",
"range": [
0,
250
],
"can": {
"id": "0x235",
"byte": 4,
"length": 16,
"order": "little",
"scale": 0.0581,
"offset": 0,
"evidence": "Z27AG-DECODE-3"
}
},
"wheel_speed_rl_kph": {
"unit": "kph",
"type": "number",
"source": "z27ag",
"description": "Rear left wheel speed from the ABS (CAN), scaled against GNSS while cruising. Which word is which wheel: from cornering (outer wheels faster, fronts on a wider arc than rears), not yet checked on jack stands",
"range": [
0,
250
],
"can": {
"id": "0x235",
"byte": 6,
"length": 16,
"order": "little",
"scale": 0.0581,
"offset": 0,
"evidence": "Z27AG-DECODE-3"
}
},
"odometer_km": {
"unit": "km",
"type": "number",
"source": "z27ag",
"description": "Total distance (CAN), whole kilometres: steps once per kilometre driven (12 steps over 11.9 km). Not yet compared with the cluster's display",
"range": [
0,
16777215
],
"can": {
"id": "0x570",
"byte": 4,
"length": 24,
"order": "little",
"scale": 1,
"offset": 0,
"evidence": "Z27AG-DECODE-3"
}
},
"coolant_temp_c": {
"unit": "celsius",
"type": "number",
"source": "z27ag",
"description": "Engine coolant temperature (CAN): equal to OBD-II's within 1 °C over 86-97 °C",
"range": [
-40,
215
],
"can": {
"id": "0x449",
"byte": 2,
"length": 8,
"scale": 1,
"offset": -40,
"evidence": "Z27AG-DECODE-2"
}
},
"front_wiper": {
"unit": "",
"type": "boolean",
"source": "z27ag",
"description": "Front wiper running (low or high; interval not seen in a 4 s window). Medium confidence",
"can": {
"id": "0x423",
"byte": 3,
"bit": 3,
"length": 1,
"evidence": "Z27AG-DECODE-2"
}
},
"orin_cpu_temp_c": {
"unit": "celsius",
"type": "number",
"source": "host",
"description": "Orin CPU cluster temperature",
"range": [
-40,
110
]
},
"orin_gpu_temp_c": {
"unit": "celsius",
"type": "number",
"source": "host",
"description": "Orin GPU temperature",
"range": [
-40,
110
]
},
"orin_soc_temp_c": {
"unit": "celsius",
"type": "number",
"source": "host",
"description": "Orin SoC temperature, hottest of its sensors",
"range": [
-40,
110
]
},
"orin_tj_temp_c": {
"unit": "celsius",
"type": "number",
"source": "host",
"description": "Orin junction temperature: the hottest point on the die, which throttling acts on",
"range": [
-40,
110
]
},
"orin_cpu_pct": {
"unit": "percent",
"type": "number",
"source": "host",
"description": "CPU utilisation, all cores",
"range": [
0,
100
]
},
"orin_gpu_pct": {
"unit": "percent",
"type": "number",
"source": "host",
"description": "GPU utilisation",
"range": [
0,
100
]
},
"orin_ram_used_mb": {
"unit": "MB",
"type": "number",
"source": "host",
"description": "Memory in use, CPU and GPU together (one shared pool): total minus available",
"range": [
0,
8192
]
},
"orin_ram_used_pct": {
"unit": "percent",
"type": "number",
"source": "host",
"description": "Memory in use as a share of the total",
"range": [
0,
100
]
},
"orin_swap_used_mb": {
"unit": "MB",
"type": "number",
"source": "host",
"description": "Swap in use",
"range": [
0,
16384
]
},
"orin_power_w": {
"unit": "W",
"type": "number",
"source": "host",
"description": "Orin module input power (VDD_IN), from the onboard INA3221",
"range": [
0,
30
]
},
"orin_fan_rpm": {
"unit": "rpm",
"type": "number",
"source": "host",
"description": "Cooling fan speed",
"range": [
0,
10000
]
},
"net_wifi_up": {
"unit": "",
"type": "boolean",
"source": "host",
"description": "WiFi associated and up"
},
"net_wifi_ssid": {
"unit": "",
"type": "string",
"source": "host",
"description": "Network name (SSID) of the WiFi the Orin is associated with; not sent while WiFi is down"
},
"net_wifi_signal_dbm": {
"unit": "dBm",
"type": "number",
"source": "host",
"description": "WiFi signal level",
"range": [
-100,
0
]
},
"net_wifi_rx_kbps": {
"unit": "kbit/s",
"type": "number",
"source": "host",
"description": "WiFi receive rate (into the Orin)",
"range": [
0,
1000000
]
},
"net_wifi_tx_kbps": {
"unit": "kbit/s",
"type": "number",
"source": "host",
"description": "WiFi transmit rate (out of the Orin)",
"range": [
0,
1000000
]
},
"net_lte_state": {
"unit": "",
"type": "string",
"source": "host",
"description": "Modem state as ModemManager reports it (connected, registered, searching, disabled, …), or absent with no modem"
},
"net_lte_tech": {
"unit": "",
"type": "string",
"source": "host",
"description": "Access technology in use (lte, umts, …); empty when not registered"
},
"net_lte_signal_pct": {
"unit": "percent",
"type": "number",
"source": "host",
"description": "Modem signal quality",
"range": [
0,
100
]
},
"net_lte_rx_kbps": {
"unit": "kbit/s",
"type": "number",
"source": "host",
"description": "LTE receive rate (into the Orin)",
"range": [
0,
150000
]
},
"net_lte_tx_kbps": {
"unit": "kbit/s",
"type": "number",
"source": "host",
"description": "LTE transmit rate (out of the Orin)",
"range": [
0,
50000
]
},
"net_tablet_up": {
"unit": "",
"type": "boolean",
"source": "host",
"description": "USB link to the tablet has carrier"
},
"net_tablet_rx_kbps": {
"unit": "kbit/s",
"type": "number",
"source": "host",
"description": "USB tablet link, tablet to Orin",
"range": [
0,
480000
]
},
"net_tablet_tx_kbps": {
"unit": "kbit/s",
"type": "number",
"source": "host",
"description": "USB tablet link, Orin to tablet",
"range": [
0,
480000
]
},
"can_up": {
"unit": "",
"type": "boolean",
"source": "host",
"description": "The recorder's CAN interface (the PEAK, can_pcan) is present and up; false when absent or down"
},
"can_bus_state": {
"unit": "",
"type": "string",
"source": "host",
"description": "CAN controller state: ERROR-ACTIVE (normal), ERROR-WARNING, ERROR-PASSIVE, BUS-OFF, STOPPED (down), or absent with no adapter"
},
"can_rx_fps": {
"unit": "frames/s",
"type": "number",
"source": "host",
"description": "CAN frames received per second on that interface",
"range": [
0,
10000
]
},
"elm_connected": {
"unit": "",
"type": "boolean",
"source": "host",
"description": "Bluetooth link to the OBD-II adapter (the ELM327 on the K-line) is up: obd-poller holds it while the ECU answers"
},
"cam_up": {
"unit": "",
"type": "boolean",
"source": "host",
"description": "A USB (UVC) camera is present for svc-vision: any V4L2 device on USB"
},
"vision_up": {
"unit": "",
"type": "boolean",
"source": "host",
"description": "yukart-vision (the camera's live view) is running"
},
"ups_battery_v": {
"unit": "V",
"type": "number",
"source": "host",
"description": "UPS battery pack voltage (3S Li-ion, 9.0–12.6 V)",
"range": [
0,
16
]
},
"ups_current_a": {
"unit": "A",
"type": "number",
"source": "host",
"description": "UPS battery current: positive charging, negative discharging",
"range": [
-5,
5
]
},
"ups_battery_pct": {
"unit": "percent",
"type": "number",
"source": "host",
"description": "UPS charge: counted from the current since the charger last stopped at full when ups_pct_source is 'counted'; otherwise the estimate from cell voltage, corrected for load, steadiest at rest",
"range": [
0,
100
]
},
"ups_pct_source": {
"unit": "",
"type": "string",
"source": "host",
"description": "Where ups_battery_pct comes from: 'counted' (current integrated since the last full charge, against the configured capacity) or 'voltage' (no full charge seen since the count was lost: pack out, Orin off a while, capacity changed)"
},
"ups_charge_ah": {
"unit": "Ah",
"type": "number",
"source": "host",
"description": "UPS charge counted since the last full charge; absent until there has been one",
"range": [
0,
20
]
},
"ups_state": {
"unit": "",
"type": "string",
"source": "host",
"description": "charging, discharging (running on battery) or idle (on input power, not charging)"
},
"gnss_fix": {
"unit": "",
"type": "string",
"source": "gnss",
"description": "Fix quality: none, 2d or 3d (gpsd TPV mode)"
},
"gnss_speed_kph": {
"unit": "kph",
"type": "number",
"source": "gnss",
"description": "Speed over ground; null without a fix",
"range": [
0,
300
]
},
"gnss_heading_deg": {
"unit": "degree",
"type": "number",
"source": "gnss",
"description": "Course over ground, true north; null without a fix or at a standstill",
"range": [
0,
360
]
},
"gnss_lat": {
"unit": "degree",
"type": "number",
"source": "gnss",
"description": "Latitude, WGS84",
"range": [
-90,
90
]
},
"gnss_lon": {
"unit": "degree",
"type": "number",
"source": "gnss",
"description": "Longitude, WGS84",
"range": [
-180,
180
]
},
"gnss_alt_m": {
"unit": "m",
"type": "number",
"source": "gnss",
"description": "Altitude above mean sea level; null without a 3D fix",
"range": [
-500,
9000
]
},
"gnss_satellites": {
"unit": "",
"type": "number",
"source": "gnss",
"description": "Satellites used in the fix",
"range": [
0,
64
]
},
"imu_roll_deg": {
"unit": "degree",
"type": "number",
"source": "imu",
"description": "Body roll in the car's frame: positive leaning right (right side down)",
"range": [
-180,
180
]
},
"imu_pitch_deg": {
"unit": "degree",
"type": "number",
"source": "imu",
"description": "Body pitch in the car's frame: positive nose up",
"range": [
-90,
90
]
},
"imu_accel_lon_g": {
"unit": "g",
"type": "number",
"source": "imu",
"description": "Longitudinal acceleration in the car's frame, gravity removed: positive forward (speeding up), negative braking",
"range": [
-4,
4
]
},
"imu_accel_lat_g": {
"unit": "g",
"type": "number",
"source": "imu",
"description": "Lateral acceleration in the car's frame, gravity removed: positive to the right (a right turn reads positive)",
"range": [
-4,
4
]
},
"imu_yaw_rate_dps": {
"unit": "degree/s",
"type": "number",
"source": "imu",
"description": "Yaw rate: positive turning left (counter-clockwise from above)",
"range": [
-500,
500
]
} }
} }
} }
+29 -1
View File
@@ -12,7 +12,7 @@
"payload": "envelope", "payload": "envelope",
"retained": true, "retained": true,
"rate": "1-50/s", "rate": "1-50/s",
"publisher": "decoder", "publisher": "decoder (source z27ag), obd-poller (source obd2)",
"description": "One topic per signal, named exactly as in signals.json." "description": "One topic per signal, named exactly as in signals.json."
}, },
"yukart/gnss/fix": { "yukart/gnss/fix": {
@@ -22,6 +22,34 @@
"publisher": "gnss", "publisher": "gnss",
"description": "value is an object: lat, lon, alt_m, speed_kph, heading_deg, fix, satellites." "description": "value is an object: lat, lon, alt_m, speed_kph, heading_deg, fix, satellites."
}, },
"yukart/gnss/<signal>": {
"payload": "envelope",
"retained": true,
"rate": "1-10/s",
"publisher": "gnss",
"description": "The receiver's fix as single values, one topic per signal, named exactly as in signals.json (source=gnss). For displays; yukart/gnss/fix stays the composite for navigation."
},
"yukart/imu/<signal>": {
"payload": "envelope",
"retained": true,
"rate": "10-50/s",
"publisher": "imu",
"description": "The IMU (WT61) in the car's frame: attitude, gravity-free acceleration, yaw rate; one topic per signal, named exactly as in signals.json (source=imu)."
},
"yukart/raw/imu": {
"payload": "json",
"retained": false,
"rate": "10-100/s",
"publisher": "imu",
"description": "Every IMU sample, raw: module axes, not calibrated. {mono (ns, CLOCK_MONOTONIC), t (µs, realtime), accel [x,y,z] g, gyro [x,y,z] deg/s, angle [roll,pitch,yaw] deg (the module's own)}. Not an envelope; for the recorder (imu.ndjson), so a later calibration can reprocess a session. Displays use yukart/imu/<signal>."
},
"yukart/host/<signal>": {
"payload": "envelope",
"retained": true,
"rate": "1/s",
"publisher": "orin-status",
"description": "The Orin itself: temperatures, load, memory, network links, UPS battery. One topic per signal, named exactly as in signals.json (source=host)."
},
"yukart/system/<service>": { "yukart/system/<service>": {
"payload": "json", "payload": "json",
"retained": true, "retained": true,