From 734d20189fe74e144ffae515a4080c6e91aab3f5 Mon Sep 17 00:00:00 2001 From: Matthew Kelch Date: Mon, 27 Jul 2026 16:34:20 -0400 Subject: [PATCH 1/2] fix: ITM display never syncs on an SRM Conversion Kit Once an SRM converter identity committed, UpdateIdentity short-circuited before draining the col03 ITM stream. The kit's FF 05 subscription pushes piled up unread in the transport queue, so the ITM lifecycle never saw the push that confirms a page change: bring-up ran the full recovery ladder (re-PageSet x2, flip-away/back, gate cycle) and parked in Unavailable, which is exactly what the field report shows. Identity on a converter is a one-shot; the ITM channel is not. The ITM drain now runs before the converter short-circuit. --- CHANGELOG.md | 5 ++++ .../Transport/FanatecWheelbase.cs | 16 ++++++++--- .../FanaBridge.Tests/FanatecWheelbaseTests.cs | 28 +++++++++++++++++++ 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50ab9127..3e9d68bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## Unreleased + +### Fixed +- **The ITM display works on wheels run through an SRM Conversion Kit.** On a converted wheel the display never came up — Device Status sat at "Unavailable — retry in …" indefinitely. FanaBridge stopped collecting the display's own status messages once it had identified the wheel through the conversion kit, so it was waiting on a confirmation that had already arrived and gone unread. + ## v0.6.0 - 2026-07-14 ### Changed diff --git a/src/FanaBridge.Core/Transport/FanatecWheelbase.cs b/src/FanaBridge.Core/Transport/FanatecWheelbase.cs index 5c2a4b4e..0d3a05dd 100644 --- a/src/FanaBridge.Core/Transport/FanatecWheelbase.cs +++ b/src/FanaBridge.Core/Transport/FanatecWheelbase.cs @@ -463,6 +463,15 @@ public bool UpdateIdentity() if (!IsConnected) return false; + // ITM subscription pushes are a display concern, not an identity one: they keep + // arriving on col03 for the life of the connection no matter how the connection was + // identified. Drain them BEFORE the converter short-circuit below — an SRM kit's + // identity is a one-shot, but its ITM channel is not, and skipping this drain leaves + // the ITM lifecycle waiting on a confirming push that is sitting unread in the + // transport queue, so bring-up runs the whole recovery ladder and settles into + // Unavailable forever. + DrainFamily(Transport.ItmReports, _bufferItm); + // An SRM converter's identity is a fixed one-shot — nothing to drain or re-settle once // committed. Genuine bases fall through to the unchanged FF 08 path. if (IsSrmConverter) @@ -478,12 +487,11 @@ public bool UpdateIdentity() catch { /* transient; the next tick retries */ } } - // Drain this device's three input streams (the transport's reader thread - // routes frames by signature). Lock-free: the wheelbase is the single - // owner of all three, and reads never touch the write lock. + // Drain this device's identity streams (the transport's reader thread routes + // frames by signature; the ITM stream is already drained above). Lock-free: the + // wheelbase is the single owner of all three, and reads never touch the write lock. _drainNow = now; _reportReader.DrainIdentity(_transport, _ingest); - DrainFamily(Transport.ItmReports, _bufferItm); DrainFamily(Transport.SrmReports, _ingestSrm); // Commit a converter identity the drain routed to us — outside the drain, diff --git a/tests/FanaBridge.Tests/FanatecWheelbaseTests.cs b/tests/FanaBridge.Tests/FanatecWheelbaseTests.cs index 3b6890b8..c50ec766 100644 --- a/tests/FanaBridge.Tests/FanatecWheelbaseTests.cs +++ b/tests/FanaBridge.Tests/FanatecWheelbaseTests.cs @@ -632,6 +632,34 @@ public void ItmReports_BufferedDuringDrain_HandedOffOnce() Assert.Equal(2, drained.Count); } + [Fact] + public void ItmReports_StillDrained_OnAnSrmConverter() + { + // Regression (v0.6.0 field report, SRM kit + Podium Bentley GT3): once a + // converter identity committed, UpdateIdentity short-circuited BEFORE the ITM + // drain. The kit's FF 05 subscription pushes then sat unread in the transport + // queue forever, so the ITM lifecycle never got its confirming push — bring-up + // ran the whole recovery ladder and parked in Unavailable. Identity is a + // one-shot on a converter; the ITM channel is not. + var wb = Make(out var t, out var bus, out var clock); + bus.Devices.Add(new HidDeviceInfo(0x0005, 64, 64, "CSL Elite")); + Assert.True(wb.AutoConnect()); + + t.Srm.Enqueue(SrmReply()); + clock.T += 10; + wb.UpdateIdentity(); + Assert.True(wb.IsSrmConverter); + + t.Itm.Enqueue(new byte[] { 0xFF, 0x05, 0x01, 0x11 }); + clock.T += 10; + Assert.False(wb.UpdateIdentity()); // no identity change — but the ITM drain still runs + + var drained = new List(); + wb.DrainItmReports(drained.Add); + Assert.Single(drained); + Assert.Equal(0x11, drained[0][3]); + } + [Fact] public void ItmReports_BufferBounded_DropsOldestWhenFull() { From ad1ca02e6ddd96bfd6a110180a5d78ef87c9dbf7 Mon Sep 17 00:00:00 2001 From: Matthew Kelch Date: Mon, 27 Jul 2026 16:55:15 -0400 Subject: [PATCH 2/2] review: tighten claims, strengthen the converter regression test From an adversarial cross-model review pass: - The changelog asserted a hardware outcome that has not been verified. Reworded to state what the code now does, and to say plainly that a kit which never relays the pushes is still an open possibility. - The regression test drained a 4-byte marker frame that the real parser would discard. It now carries a valid device-4 subscription entry and asserts the parsed result, so the converter path's only coverage would also catch a frame the driver silently drops. - Recorded why the drain lives above the short-circuit rather than being duplicated into the converter branch: one site that cannot drift, at the cost of marginally widening a pre-existing wheel-change race. --- CHANGELOG.md | 2 +- .../Transport/FanatecWheelbase.cs | 19 ++++++++++++------- .../FanaBridge.Tests/FanatecWheelbaseTests.cs | 12 ++++++++++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e9d68bd..61fdb983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## Unreleased ### Fixed -- **The ITM display works on wheels run through an SRM Conversion Kit.** On a converted wheel the display never came up — Device Status sat at "Unavailable — retry in …" indefinitely. FanaBridge stopped collecting the display's own status messages once it had identified the wheel through the conversion kit, so it was waiting on a confirmation that had already arrived and gone unread. +- **FanaBridge now collects the ITM display's status messages on a wheel run through an SRM Conversion Kit.** On a converted wheel the display never came up — Device Status sat at "Unavailable — retry in …" indefinitely — because FanaBridge stopped reading those messages once it had identified the wheel through the conversion kit, leaving it waiting on a confirmation that had already arrived and gone unread. Not yet confirmed on hardware, and it does not rule out a conversion kit that never relays the messages in the first place. ## v0.6.0 - 2026-07-14 diff --git a/src/FanaBridge.Core/Transport/FanatecWheelbase.cs b/src/FanaBridge.Core/Transport/FanatecWheelbase.cs index 0d3a05dd..13c0f0eb 100644 --- a/src/FanaBridge.Core/Transport/FanatecWheelbase.cs +++ b/src/FanaBridge.Core/Transport/FanatecWheelbase.cs @@ -463,13 +463,18 @@ public bool UpdateIdentity() if (!IsConnected) return false; - // ITM subscription pushes are a display concern, not an identity one: they keep - // arriving on col03 for the life of the connection no matter how the connection was - // identified. Drain them BEFORE the converter short-circuit below — an SRM kit's - // identity is a one-shot, but its ITM channel is not, and skipping this drain leaves - // the ITM lifecycle waiting on a confirming push that is sitting unread in the - // transport queue, so bring-up runs the whole recovery ladder and settles into - // Unavailable forever. + // ITM subscription pushes are a display concern, not an identity one, so they are + // drained for the life of the connection regardless of how identity was established. + // This MUST stay above the converter short-circuit below: an SRM kit's identity is a + // one-shot, but its ITM channel is not, and skipping the drain leaves the lifecycle + // waiting on a confirming push that is sitting unread in the transport queue — so + // bring-up runs the whole recovery ladder and settles into Unavailable forever. + // (Whether a given kit relays those pushes at all is a separate, open question; this + // only guarantees we read whatever does arrive.) Draining here rather than alongside + // the identity streams below widens, by a few microseconds of same-thread work, the + // pre-existing window in which a late push from a previous attachment can outlive the + // wheel-change clear in the transport queue — one drain site is worth that over two + // that can drift apart. DrainFamily(Transport.ItmReports, _bufferItm); // An SRM converter's identity is a fixed one-shot — nothing to drain or re-settle once diff --git a/tests/FanaBridge.Tests/FanatecWheelbaseTests.cs b/tests/FanaBridge.Tests/FanatecWheelbaseTests.cs index c50ec766..8369c189 100644 --- a/tests/FanaBridge.Tests/FanatecWheelbaseTests.cs +++ b/tests/FanaBridge.Tests/FanatecWheelbaseTests.cs @@ -650,14 +650,22 @@ public void ItmReports_StillDrained_OnAnSrmConverter() wb.UpdateIdentity(); Assert.True(wb.IsSrmConverter); - t.Itm.Enqueue(new byte[] { 0xFF, 0x05, 0x01, 0x11 }); + // A realistic push rather than a marker byte: FF 05 01 then one 5-byte entry + // [deviceId][fwHandle][paramId-LE][dataType] for display device 4 (the Bentley). + // The sibling tests above only need a distinguishable frame, but this one is the + // converter path's only coverage — so it drains AND parses, which would also catch + // a future regression that hands the driver a frame it silently discards. + t.Itm.Enqueue(new byte[] { 0xFF, 0x05, 0x01, 0x04, 0x00, (byte)ItmParam.Speed, 0x00, 0x34 }); clock.T += 10; Assert.False(wb.UpdateIdentity()); // no identity change — but the ITM drain still runs var drained = new List(); wb.DrainItmReports(drained.Add); Assert.Single(drained); - Assert.Equal(0x11, drained[0][3]); + + var subs = ItmTelemetry.ParseSubscriptionReport(drained[0], drained[0].Length, deviceId: 4); + var sub = Assert.Single(subs); + Assert.Equal(ItmParam.Speed, sub.ParamId); } [Fact]