Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Unreleased

### Fixed
- **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

### Changed
Expand Down
21 changes: 17 additions & 4 deletions src/FanaBridge.Core/Transport/FanatecWheelbase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,20 @@ public bool UpdateIdentity()
if (!IsConnected)
return false;

// 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
// committed. Genuine bases fall through to the unchanged FF 08 path.
if (IsSrmConverter)
Expand All @@ -478,12 +492,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,
Expand Down
36 changes: 36 additions & 0 deletions tests/FanaBridge.Tests/FanatecWheelbaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,42 @@ 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);

// 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<byte[]>();
wb.DrainItmReports(drained.Add);
Assert.Single(drained);

var subs = ItmTelemetry.ParseSubscriptionReport(drained[0], drained[0].Length, deviceId: 4);
var sub = Assert.Single(subs);
Assert.Equal(ItmParam.Speed, sub.ParamId);
}

[Fact]
public void ItmReports_BufferBounded_DropsOldestWhenFull()
{
Expand Down
Loading