Skip to content

Releases: erickramer42/qttune

QtTune v0.3.1

Choose a tag to compare

@github-actions github-actions released this 25 Oct 18:05

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

  1. Download qttune-0.3.1-windows-x64.zip below and unzip the whole folder.
  2. Run qttune.exe. The program isn't code-signed, so SmartScreen may stop it the first time: choose More info, then Run anyway.
  3. In Settings, press the mock://demo preset, 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-generic profile 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-generic query 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-generic have 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

Choose a tag to compare

@erickramer42 erickramer42 released this 23 Oct 06:12

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.exe when a build-win32 tree 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). With ECUIO_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 obd2 profiles, 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 from core/include/qttune/ to core/include/ecuio/, the library target is ecuio-core, and runtime environment variables are ECUIO_*. The app keeps the QtTune name (ADR-0001, appendix of 2026-10-22).
  • Frames: ecuio_frame_t is 88 bytes with no padding. It gains a first-class arbitration_id and a flags bitfield (extended, remote, FD, bit-rate switch, error, TX echo) that replaces is_extended; data_length widens to 16 bits; the payload stays inline as data[64]; and timestamp_us is session-relative (ADR-0006).
  • Lifecycle: ecuio_session_close stops streaming and detaches the transport but no longer frees the session; the new ecuio_session_destroy frees it (null-safe, closing first if needed).
  • Transports are internal. The public qttune_transport_attach and qttune_transport_detach are gone: the URI given to ecuio_session_create selects the transport (ADR-0007).
  • Status codes, appended: ECUIO_ERR_LOAD_FAILED (7); the link-health codes ECUIO_ERR_VEHICLE_TIMEOUT (8), ECUIO_ERR_TRANSPORT_HARD_FAILURE (9) and ECUIO_ERR_REQUEST_FAILED (10), which ecuio_session_get_error_state reports while a session runs; and ECUIO_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 obd2 value 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 main since this release.
  • With an x64 driver, a TX indication ends a read early (#28), so replies arrive a cycle late. Fixed on main since 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

Choose a tag to compare

@erickramer42 erickramer42 released this 23 Oct 06:10

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_string and an append-only status enum. The session functions (qttune_session_create, _close, _send and _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

Choose a tag to compare

@erickramer42 erickramer42 released this 23 Oct 06:11

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_callback and qttune_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_start begins streaming, so creating a session and starting it are separate steps, and qttune_session_close stops and joins the worker before the session goes away.

Bridge and UI

  • QtTuneBridge marshals 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 Theme singleton), 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