Repository navigation
Signals & First Vehicle Read
QtTune now decodes signals, talks to J2534 Pass-Thru adapters on Windows, and has read live data from a car. In a read-only session on 2026-10-22, the engine ECU answered 4,809 Mode-01 requests through an OBDX Pro VX, 4,444 of them positively, and the decoded values tracked the instrument cluster: RPM matched the tachometer through a cold start and at idle (session sheet).
Warning
Experimental, read-only software, tested with one adapter on one vehicle. Read DISCLAIMER.md before connecting it to a car.
Highlights
Signal decoding in the core. A signal definition says where a value sits in a frame (start byte, bit offset and length, value kind, byte order, scale and offset) and which frames carry it (a CAN ID, plus a mux key of up to 3 bytes). Values decode in double, and the UI shows each definition's name, unit and decimal places as they are, so no page formats a value of its own. The validator accepts exactly what the decoder can decode and names the check and the element that failed (ADR-0004, ADR-0016).
Protocol profiles. The URI picks how frames decode: mock (the default for mock://), obd2 (the default for j2534://) or raw (log only), and ?profile= overrides either (ADR-0013). The obd2 profile requests ten standard Mode-01 PIDs in rotation (engine load, coolant temperature, RPM, vehicle speed, intake air temperature, throttle position, fuel level, control module voltage, MAF and engine oil temperature) and declares the 7E0/7E8 channel, so a bare j2534:// URI streams decoded values.
J2534 Pass-Thru (Windows).
- Finds the installed vendor drivers in the registry. Settings can pick one, or take a
j2534://URI with?baud=,?flags=,?filter=,?txid=,?poll=and?profile=(j2534.md). - 32-bit vendor drivers, like the OBDX Pro VX's, run in a single-threaded x86 surrogate process over a private named pipe; the loader picks the backend from the DLL's PE header (ADR-0008, ADR-0009, ADR-0014). The app build copies the surrogate next to
qttune.exewhen abuild-win32tree exists. - The adapter does the ISO 15765 framing: 4-byte flow-control filters and 6-byte
[CAN ID][01][PID]requests, both confirmed on the vehicle (ADR-0012). - Link health that names the problem. A quiet bus, failing requests (nothing on the bus acknowledges them) and an adapter fault are told apart; while a failure lasts, probing backs off from 10 Hz to one try every 5 s; and every connection surface shows the state.
- Connect and disconnect run off the GUI thread, so a slow or wedged adapter never freezes the window.
- Diagnostics: the surrogate's client and child write one timestamped session log in
%TEMP%, and each spawn keeps the log it replaces (the newest 50). WithECUIO_SUR_LOG_FRAMES=1, every request and reply is logged verbatim. A surrogate built from different shared headers is refused at connect instead of misreading the wire.
User interface.
- Dashboard: analog, ring, digital and bar gauges, with shift lights and peak hold. They're fed by signal name, so one layout serves both the mock and
obd2profiles, and a gauge whose signal the profile lacks says "not in profile" instead of showing zero. The page rearranges for short, wide windows. - Live: values refreshed at 10 Hz whatever the bus rate, and a strip chart whose rows double as its legend. A value never received shows "—", never 0, and one older than 3 s dims.
- Log: raw frames in arrival order, newest or oldest first, with Pause.
- Zoom: Ctrl + / Ctrl − / Ctrl 0, or Settings' Display row, kept between runs.
Session recording, opt-in. With Record sessions ticked in Settings, each connect writes frames.csv (every frame, host and adapter time side by side), signals.csv (every decoded value) and events.log under Documents/QtTune/recordings (ADR-0017).
Simulated vehicle. mock:// runs a scripted, roughly two-minute drive on a small vehicle model: a cold-start warm-up, 2nd- and 3rd-gear pulls into the 7,000 rpm limiter, then a quarter-mile run. Every page works with no hardware.
Breaking changes (C ABI)
Since v0.2.0:
- Names: every core symbol takes the
ecuio_prefix (ECUIO_for macros and enum values), the public headers move fromcore/include/qttune/tocore/include/ecuio/, the library target isecuio-core, and runtime environment variables areECUIO_*. The app keeps the QtTune name (ADR-0001, appendix of 2026-10-22). - Frames:
ecuio_frame_tis 88 bytes with no padding. It gains a first-classarbitration_idand aflagsbitfield (extended, remote, FD, bit-rate switch, error, TX echo) that replacesis_extended;data_lengthwidens to 16 bits; the payload stays inline asdata[64]; andtimestamp_usis session-relative (ADR-0006). - Lifecycle:
ecuio_session_closestops streaming and detaches the transport but no longer frees the session; the newecuio_session_destroyfrees it (null-safe, closing first if needed). - Transports are internal. The public
qttune_transport_attachandqttune_transport_detachare gone: the URI given toecuio_session_createselects the transport (ADR-0007). - Status codes, appended:
ECUIO_ERR_LOAD_FAILED(7); the link-health codesECUIO_ERR_VEHICLE_TIMEOUT(8),ECUIO_ERR_TRANSPORT_HARD_FAILURE(9) andECUIO_ERR_REQUEST_FAILED(10), whichecuio_session_get_error_statereports while a session runs; andECUIO_ERR_INVALID_ARGUMENT(11).
The signal-decoding and profile APIs (signals.h, signal_sets.h, profile.h) and J2534 discovery (j2534.h) are new in this release. Their layouts are pinned by static asserts, and every public header is compiled as strict C11 in the test suite.
How it was verified
- 213 ctest cases on Windows, plus a 25-case cross-architecture subset (an x64 test process driving the x86 surrogate and an x86 fake driver), all passing except one environment-gated skip. Every first-party target builds with warnings on, at zero on MSVC.
- CI was green on the tag commit (
bc519d1). It builds and tests on Windows, macOS and Linux, runs the core under ASan and UBSan, cross-builds Android arm64 and iOS, and launches the iOS app in the Simulator on pull requests. - On hardware: quiet-bus bench sessions with the OBDX Pro VX through the surrogate on 2026-10-16, 2026-10-20 and 2026-10-22, then the vehicle session (bench sessions).
Test tiers and gaps: docs/testing.md.
Known issues
Numbers are the Known Issues in STATUS.md.
- Real hardware means one adapter and one car. The in-proc backend (x64 drivers), 29-bit addressing, multi-frame replies and more than one ECU answering haven't met real hardware.
- Each
obd2value refreshes about once a second. The scheduler sends one request per 100 ms slot, and nothing yet skips the PIDs an ECU doesn't support: on the first car, engine oil temperature stays "waiting for data". - J2534 timestamps wrap after about 71.6 minutes (#12). They're the adapter's 32-bit microsecond stamps, passed through. Fixed on
mainsince this release. - With an x64 driver, a TX indication ends a read early (#28), so replies arrive a cycle late. Fixed on
mainsince this release. - Messages longer than 64 bytes are cut without a marker (#36), the 4-byte CAN ID included. Service 01 replies fit; longer Service 09 and Service 0x22 replies won't.
- Link health: if reads recover while writes keep failing, the state stays "adapter fault" instead of "requests failing" (#11).
- The device picker treats one registry entry as one device (#13), so several adapters behind one vendor driver can't be told apart.
- The mobile apps are unvalidated (#2). CI builds Android and iOS, and the iOS app launches in the Simulator, but neither has run on a device, and off Windows the only transport is
mock://.
What's next (v0.3.x)
A request engine that paces requests by the ECU's replies (ADR-0015, proposed), the obd2 PID-support probe on top of it, uds-generic for Mode-22 reads, and a record-replay transport that plays back frames.csv.
Full changelog: v0.2.0...v0.3.0