Skip to content

Signals & First Vehicle Read

Choose a tag to compare

@erickramer42 erickramer42 released this 23 Oct 06:12
· 164 commits to main since this release

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