Add tunie/docs (full context + open-source roadmap); refresh README

docs/CONTEXT.md captures the entire reverse-engineering effort in one place:
KWP2000 protocol, AES-128 seed/key, the map format (dc decrypt -> flat-ROM
unpack -> directory -> fe table pointers), the fuel/ignition table map, the
validated SAI/O2 device flags, checksums, hardware, and a per-finding confidence
table.

docs/ROADMAP.md covers open-sourcing (legal/IP posture on proprietary maps and
the AES keys, repo hygiene, packaging/CI, community) and the technical path to
tuning the bike (first contact -> ROM dump -> calibration model -> write path ->
editor), with immediate next steps and a risk register.

Top-level README refreshed to describe the whole project (tool + viewer +
research) and point at docs/.
This commit is contained in:
2026-08-11 08:21:16 -05:00
parent 80c9373ded
commit 4debdfa518
4 changed files with 386 additions and 139 deletions

View File

@@ -1,151 +1,53 @@
# tunie
Open-source, **read-only** KWP2000 diagnostics for the Triumph Keihin ECU —
built for a 2010 Bonneville T100 (865cc, mechanical/analog odometer). The long
game is a full open tuning toolchain to replace the closed TuneECU app; this
first piece safely reads the ECU without any risk of bricking it.
Open, offline tooling for the **Triumph Keihin ECU** (2010 Bonneville T100, 865cc,
Renesas SH7054, mechanical odometer) — a from-scratch, reverse-engineered
alternative to the closed TuneECU app. Read the ECU, decode and view its
fuel/ignition maps, understand the device flags, and (eventually) tune it.
> **This tool cannot write to the ECU.** The flash/write services are not
> implemented, and `src/tunie/safety.py` refuses them before any byte leaves the
> program. See [Safety](#safety).
> **Independent interoperability research on hardware I own.** Contains no TuneECU
> source — only original code and documented findings. Proprietary map binaries are
> never committed (see `.gitignore`).
For the full project context, risk register, and pick-up-where-we-left-off
notes, read **[STATUS.md](STATUS.md)**. The seed/key reverse engineering is in
**[research/FINDINGS.md](research/FINDINGS.md)**.
## What's here
---
- **`src/tunie/`** — a **read-only** KWP2000 diagnostic tool (K-Line/FTDI or
Bluetooth ELM327). Cannot write to the ECU by construction; `safety.py` refuses
every flash service before it hits the wire. `pip install -e .`, then `tunie info
--port …`. Verified frames in `tests/verify_protocol.py`.
- **`viewer/tunie-viewer.html`** — a self-contained page (no server, no deps) that
drops in a real map `.hex`, **decrypts and unpacks it, and renders the real
fuel/ignition tables** with RPM/throttle axes, an A→B diff, and SAI/O2 device
checkboxes. Build with `viewer/build_viewer.py`; `download_maps.py` + `serve.py`
add a catalogue dropdown.
- **`research/`** — the reverse engineering: AES-128 seed/key
(`keihin_seedkey.py`), and the map format — decrypt, flat-ROM unpack, table map,
and validated SAI/O2 flags (`reference-maps/`).
- **`docs/`** — start here for the full picture.
## Install
## Read the docs
```sh
cd tunie
python3 -m venv .venv
./.venv/bin/pip install -e .
```
- **[docs/CONTEXT.md](docs/CONTEXT.md)** — everything reverse-engineered and built,
with per-finding confidence levels. The one file to read.
- **[docs/ROADMAP.md](docs/ROADMAP.md)** — open-sourcing plan + the technical path to
actually tuning the bike + next steps + risk register.
- `STATUS.md`, `viewer/FORMAT.md`, `research/FINDINGS.md`,
`research/reference-maps/{README,TABLES,DEVICES}.md` — deeper per-topic writeups.
Requires Python 3.10+ and `pyserial` (pulled in automatically).
## Status (short version)
## Use
```sh
tunie ports # list serial devices
tunie info --port /dev/cu.usbserial-XXXX # interrogate ECU (wired KKL cable)
tunie info --port /dev/cu.OBDII --adapter elm327 # or a Bluetooth ELM327
tunie dtc --port /dev/cu.usbserial-XXXX # read fault codes only
tunie -v info --port ... # verbose: log every KWP frame
# Offline map database (1811 TuneECU maps, already extracted to maps.json):
tunie maps Bonneville --ecu 0 --odometer mechanical
tunie extract-maps <path/to/apktool_out/res/values/arrays.xml> # rebuild it
```
Ignition **on**, engine **off**. If fast init times out, try `--init slow`
(5-baud address init).
A first `tunie info` returns the ECU's identity, its currently-flashed map ID,
and any stored DTCs — that's the immediate goal, and it resolves which stock map
is actually on the bike.
## Protocol facts (recovered from the TuneECU APK, not guessed)
| Parameter | Value |
|---|---|
| ECU address | `0xD5` |
| Tester address (K-Line) | `0xF5` (**not** 0xF1) |
| Format byte | `0x80 \| length` |
| Checksum | additive sum mod 256 |
| Init | fast (`ATTP5`) or 5-baud (`ATTP4` + `ATIIAD5`) |
Verified frames (see `tests/verify_protocol.py`):
```
81 d5 f5 81 cc StartCommunication
82 d5 f5 1a 80 e6 ReadEcuIdentification 0x80
84 d5 f5 18 00 ff 00 65 ReadDtcByStatus
```
Run the tests:
```sh
./.venv/bin/python tests/verify_protocol.py
```
Software + reverse engineering are well along and **validated against real map
files**: the protocol, the AES seed/key, the map decryption, the flat-ROM unpack,
the fuel/ignition table locations, and the SAI (confirmed) / O2 (probable) device
flags. **Nothing has touched the real ECU yet** — first contact is blocked on
wiring a cable to the Triumph diagnostic connector. The write/flash path is future
work (see the roadmap).
## Safety
Bricking an ECU over KWP2000 requires reaching the memory-write services, and
those sit behind Security Access (`0x27`). `safety.assert_read_only()` is called
on **every** outbound request inside `Transport.request()`, before the transport
sees it. It allows only query services and refuses all of: `0x11` EcuReset,
`0x14` Clear, `0x27` SecurityAccess, `0x2E` Write, `0x34`/`0x36` flash,
`0x35` upload, `0x37` — plus every programming diagnostic session. A bug can't
send a write because the write path does not exist.
**The real remaining risk is electrical, not software:** confirm the Triumph
diagnostic connector pinout before plugging any cable in. The 2010 twins do not
use a standard OBD-II socket.
---
## Where we left off (2026-08-10)
Phase 1 (read-only comms) is **built but not yet run against the bike** — nothing
has touched the ECU. Next physical steps, in order:
1. **Identify the wired cable's chip** — `tunie ports`. It's a *VAG* KKL 409.1:
right cable class (K-Line), but wired for a VW OBD-II socket, which this bike
does not have. FTDI shows as `/dev/cu.usbserial-*`; CH340 as
`/dev/cu.wchusbserial*` (needs the CH340 macOS driver).
2. **Confirm the Triumph connector pinout** and build an adapter/re-pin (K-Line,
switched +12V, ground) from the cable to the bike's diagnostic connector.
*Do not plug in until this is confirmed against a wiring diagram.*
3. **First scan:** ignition on / engine off, `tunie info --port …`.
Later phases (all deliberately out of the read-only tool): ROM dump for a
recovery image → firmware checksum patching → the write/flash path.
### Big finding: the seed/key is AES-128
The ECU's Security Access algorithm was fully recovered from the TuneECU APK —
it's **standard AES-128** (verified against FIPS-197 and the app's own T-table),
with three embedded keys selected by an ECU-code index. Not the XOR scheme the
original brief predicted. Working reference and details:
- `research/keihin_seedkey.py` — pure-Python, self-testing
(`python3 research/keihin_seedkey.py`)
- `research/FINDINGS.md` — full writeup
This is **write-path** material, kept in `research/` and never imported by the
`tunie` package. Having the algorithm does not make flashing safe — it's one of
several pieces (checksum patching, a verified stock dump, a recovery plan) that
all have to line up first.
## Layout
```
tunie/
README.md this file
STATUS.md full context, risk register, tuning theory
RESEARCH.md original research brief (partly wrong; STATUS corrects it)
pyproject.toml
maps.json 1811 TuneECU maps, extracted
src/tunie/ the read-only tool
safety.py read-only allowlist, enforced before transmit
kwp2000.py ISO 14230-3 framing / checksums / NRC decoding
triumph.py Triumph constants from the APK
identify.py the read-only interrogation sweep
maps.py map catalogue extraction + search
cli.py command-line entry point
transport/ kline.py (KKL cable) + elm327.py (Bluetooth)
tests/verify_protocol.py
research/ WRITE-PATH work, outside the package
keihin_seedkey.py AES-128 seed/key reference
FINDINGS.md seed/key writeup
```
> The decompiled TuneECU sources used for this research are **not** included here
> (third-party app, bulky). Findings and extracted constants are documented in
> `research/` and `STATUS.md`.
## Legal
Independent interoperability research on a bike I own. TuneECU is a separate
third-party product; this repo contains no TuneECU source, only original code and
documented findings.
- The diagnostic tool is read-only; it can't brick the ECU.
- The **real** risk is electrical: confirm the Triumph connector pinout before
plugging any cable in (the 2010 twins don't use a standard OBD-II socket).
- Device flags are **not** a tune on their own — disabling O2 forces open-loop and
needs matching fuel enrichment. Prefer flashing a complete, matching delete map
over hand-toggling a stock one. See `docs/CONTEXT.md` §7.