Repository navigation
Releases: erickramer42/qttune
Release list
QtTune v0.3.1
QtTune 0.3.1 is the first release with a Windows download: unzip it, run qttune.exe, and pick mock://demo in Settings to watch the built-in simulated car drive. It needs no Qt, no Visual Studio and no separate Visual C++ redistributable. Read DISCLAIMER.md before connecting it to a vehicle.
Try it
- Download
qttune-0.3.1-windows-x64.zipbelow and unzip the whole folder. - Run
qttune.exe. The program isn't code-signed, so SmartScreen may stop it the first time: choose More info, then Run anyway. - In Settings, press the
mock://demopreset, then Connect.
The zip's README.txt covers adapters and what each file is.
What's new since 0.3.0
Talking to the car
- A request engine paces requests by the replies: one request in flight per ECU, the next sent as soon as the answer arrives, with a 100 ms wait,
78(response pending) honoured, and a 20 ms floor between requests (ADR-0015). - A PID-support probe reads each ECU's supported-PID bitmaps before polling and drops the PIDs it refuses, so a car that doesn't support a signal stops being asked for it.
- Vehicle identification at connect: VIN, calibration IDs, CVNs and the ECU's name (Service 09), and the standard UDS identification DIDs (ADR-0021).
- A
uds-genericprofile polls the UDS DIDs you list, or scans a range for the ones that answer, with text and hex values as well as numbers (ADR-0020). - Whole ISO 15765 messages: a frame carries up to 4096 bytes, so long multi-frame replies arrive in one piece.
- Connect failures say why: a bad URI, an unknown profile, a vendor refusal (with the driver's own error text), a surrogate that failed, or a DLL that won't load each have their own code and message (ADR-0018).
The app
- Metric or imperial, chosen in Settings and remembered. Imperial is the default. Recordings keep the values exactly as decoded (ADR-0022).
- Settings composes the URI from profile buttons and a DID field, and reads a typed URI back into them.
- A simulated car worth watching: a modelled 4.5 L rear-drive coupe drives a scripted loop (a cold start, full-throttle pulls onto a fuel-cut limiter, a highway cruise, a stop, and a quarter mile off launch control with wheelspin) in real time. Its readings stay consistent with one another the way a real ECU's log does, and like a real log they never sit dead still.
- Frames carry session time, from the adapter's own clock where it has one.
Engineering
- Line coverage published to Codecov from a Windows CI job, libFuzzer harnesses for the decoder, the validators and the
uds-genericquery parser, and a Code of Conduct and governance notes.
What's verified, and where
QtTune's claims are tiered (docs/testing.md):
- Every build runs the full test suite on Windows, macOS and Linux, plus sanitizers and fuzzing, in CI. Everything above passes it.
- The J2534 stack is verified end to end against an in-tree fake vendor driver, in-process and through the 32-bit surrogate, including the request engine, the probe, identification and the DID scan.
- One real adapter (an OBDX Pro VX) has connected on a quiet bench bus, and one car has been read, read-only, through the surrogate, on 2026-10-22 (session sheet). That session ran 0.3.0's scheduler: the request engine, the probe, identification and
uds-generichave not yet run on a car. The next vehicle session is planned to cover them. - Other vehicles and adapters are untested.
Verifying the download
SHA256SUMS.txt holds the zip's SHA-256. The zip also carries a signed build provenance from this repository's release workflow, which you can check with the GitHub CLI:
gh attestation verify qttune-0.3.1-windows-x64.zip -R erickramer42/qttune
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
Foundation
The skeleton: a Qt-free C++17 core behind a C ABI, a Qt 6 / QML shell, and CI that keeps the two apart.
What's in it
- Core (
core/): the C ABI's shape:qttune_init,qttune_shutdown,qttune_version_string,qttune_status_stringand an append-only status enum. The session functions (qttune_session_create,_close,_sendand_receive) are declared but return "not implemented": there's no transport yet. - App (
app/): a Qt 6 / QML shell with Dashboard, Log and Settings pages, all placeholders. The layout is responsive, with a sidebar at 720 px and wider and a tab bar below that, so one layout can serve desktop and mobile. - CI (GitHub Actions): the core and its tests build on Ubuntu with no Qt installed, which is how the project proves the core never needs Qt, and the full app builds on Windows (MSVC) and macOS, on Qt 6.8.2.
- Tests: 4 GoogleTest cases: status strings, the version format, init and shutdown idempotency, and argument checks on session create.
- The MIT license, a safety disclaimer, and build requirements in the README.
Known limitations
- No transport and no data: sessions can't be created yet.
- Mobile builds haven't been attempted.
Full changelog: https://github.com/erickramer42/qttune/commits/v0.1.0
Bridge & Live Logging
The first release that streams data. A session-scoped callback API delivers frames from a transport's worker thread, a mock transport generates them, and the Qt bridge carries them to a live frame log.
What's new
Core
- Frame callbacks per session:
qttune_register_frame_callbackandqttune_unregister_frame_callback, keyed by the(callback, user_data)pair. Duplicates are deduplicated, unregistering twice is safe, and dispatch runs from a snapshot of the list, so a callback can register or unregister itself without deadlocking. - A mock transport: a
mock://URI starts a worker thread that emits a synthetic 8-byte frame every 50 ms (20 frames a second).qttune_session_startbegins streaming, so creating a session and starting it are separate steps, andqttune_session_closestops and joins the worker before the session goes away.
Bridge and UI
QtTuneBridgemarshals each frame from the worker thread to the GUI thread through a queued call, into a frame model behind a sort proxy (newest or oldest first).- The Log page streams frames live, with the sort toggle, Clear and a dropped-frames count.
- A dark theme (a
Themesingleton), a Home page with a session status card and a Start session button, and themed buttons. The app uses Qt Quick Controls' Basic style, because the native Windows style discarded custom control backgrounds.
CI
- Every job builds on Qt 6.12.0 LTS. Qt 6.8 and earlier reference the AGL framework, which the Xcode 26 SDK removed, so the macOS build needed the upgrade.
- Windows installs Qt with aqtinstall from its master branch, because the 3.3.0 release mishandles Qt 6.11+'s Windows repository layout.
Tests
23 cases: 17 for the core (status strings, the version format, init and shutdown, argument checks, callback registration and deduplication, dispatch, mock delivery, the worker thread's identity, a callback unregistering itself mid-dispatch) and 6 for the bridge's frame path. CI builds and tests the core with no Qt installed on Ubuntu, and builds the full app on Windows and macOS; it was green on the tag commit (7dbdcec).
Known limitations
- Mock data only: there's no real transport yet, and nothing decodes frames into values.
- The per-frame GUI updates and the live sort keep up with the mock's 20 frames a second; faster buses will need coalesced updates.
- Oldest-first mode doesn't follow new frames yet.
- The session API expects one caller (the bridge).
- No Android or iOS build has been attempted, and CI doesn't build the app on Linux.
- Windows CI depends on the aqtinstall master pin until a release fixes the layout bug.
Full changelog: v0.1.0...v0.2.0