Skip to content

Commit 6cd5e17

Browse files
committed
feat: move session bar data to channel
1 parent 2dab47c commit 6cd5e17

38 files changed

Lines changed: 1080 additions & 151 deletions

File tree

docs/IMPLEMENTATION_PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ Today every renderer wakes 25 times/sec regardless of what's mounted. A weather
276276
- [x] Radio transmit state — `radio.snapshot`, event-driven and demand-activated; PR #666
277277
- [ ] Session-bar telemetry migration
278278
- [x] Shared race/session timing projection — `session-timing.snapshot`, demand-driven at 5 Hz; PR #667 in review
279-
- [ ] Auxiliary items still reading legacy telemetry (weather, fuel/units, brake bias, incidents, lap results, and player position)
279+
- [x] Auxiliary items (weather, fuel/units, brake bias, incidents, lap results, player position, and top speed) — `session-bar.snapshot`; PR #667
280280
- [ ] Legacy `'telemetry'` channel removed or dev-only
281281

282282
### Phase 5 — Worker-thread SDK loop

src/app/bridge/iracingSdk/iracingSdkBridge.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { SectorTimingRuntime } from '../../processors/sectorTimingRuntime';
1818
import { StandingsRuntime } from '../../processors/standingsRuntime';
1919
import { RadioRuntime } from '../../processors/radioRuntime';
2020
import { SessionTimingRuntime } from '../../processors/sessionTimingRuntime';
21+
import { SessionBarRuntime } from '../../processors/sessionBarRuntime';
2122

2223
// Keys consumed by the renderer. Anything outside this set is dropped before
2324
// the telemetry object crosses the IPC boundary — reducing structured-clone
@@ -219,6 +220,9 @@ export async function publishIRacingSDKEvents(
219220
isTapeReplay
220221
)
221222
: undefined;
223+
const sessionBarRuntime = channelBus
224+
? new SessionBarRuntime(channelBus, lifecycle, perfMetrics, isTapeReplay)
225+
: undefined;
222226

223227
let shouldStop = false;
224228
let lastRunningState: boolean | undefined = undefined;
@@ -324,6 +328,7 @@ export async function publishIRacingSDKEvents(
324328
standingsRuntime?.onFrame(telemetry);
325329
radioRuntime?.onFrame(telemetry);
326330
sessionTimingRuntime?.onFrame(telemetry);
331+
sessionBarRuntime?.onFrame(telemetry);
327332
if (
328333
perfTelemetryDeliveryEnabled &&
329334
overlayManager.hasLegacyStreamSubscribers('telemetry')
@@ -359,6 +364,7 @@ export async function publishIRacingSDKEvents(
359364
sectorTimingRuntime?.onSession(session);
360365
standingsRuntime?.onSession(session);
361366
sessionTimingRuntime?.onSession(session);
367+
sessionBarRuntime?.onSession(session);
362368
overlayManager.publishMessage('sessionData', session);
363369
sessionCallbacks.forEach((callback) => callback(session));
364370
perfMetrics.markEnd('sessionPublish');
@@ -423,6 +429,7 @@ export async function publishIRacingSDKEvents(
423429
standingsRuntime?.dispose();
424430
radioRuntime?.dispose();
425431
sessionTimingRuntime?.dispose();
432+
sessionBarRuntime?.dispose();
426433
referenceLapRuntime?.dispose();
427434
perfMetrics.stopReporting();
428435
},

src/app/bridge/iracingSdk/mock-data/mockSdkBridge.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { StandingsRuntime } from '../../../processors/standingsRuntime';
1111
import { RadioRuntime } from '../../../processors/radioRuntime';
1212
import { SessionTimingRuntime } from '../../../processors/sessionTimingRuntime';
1313
import { LapTimesRuntime } from '../../../processors/lapTimesRuntime';
14+
import { SessionBarRuntime } from '../../../processors/sessionBarRuntime';
1415

1516
export async function publishIRacingSDKEvents(
1617
overlayManager: OverlayManager,
@@ -60,6 +61,9 @@ export async function publishIRacingSDKEvents(
6061
lapTimesRuntime
6162
)
6263
: undefined;
64+
const sessionBarRuntime = channelBus
65+
? new SessionBarRuntime(channelBus, lifecycle, perfMetrics)
66+
: undefined;
6367

6468
bridge.onSessionData((session) => {
6569
carSpeedsRuntime?.onSession(session);
@@ -68,6 +72,7 @@ export async function publishIRacingSDKEvents(
6872
sectorTimingRuntime?.onSession(session);
6973
standingsRuntime?.onSession(session);
7074
sessionTimingRuntime?.onSession(session);
75+
sessionBarRuntime?.onSession(session);
7176
overlayManager.publishMessage('sessionData', session);
7277
});
7378

@@ -81,6 +86,7 @@ export async function publishIRacingSDKEvents(
8186
standingsRuntime?.onFrame(telemetry);
8287
radioRuntime?.onFrame(telemetry);
8388
sessionTimingRuntime?.onFrame(telemetry);
89+
sessionBarRuntime?.onFrame(telemetry);
8490
perfMetrics.markStart('broadcast');
8591
overlayManager.publishMessage('telemetry', telemetry);
8692
perfMetrics.markEnd('broadcast');
@@ -103,6 +109,7 @@ export async function publishIRacingSDKEvents(
103109
standingsRuntime?.dispose();
104110
radioRuntime?.dispose();
105111
sessionTimingRuntime?.dispose();
112+
sessionBarRuntime?.dispose();
106113
referenceLapRuntime?.dispose();
107114
perfMetrics.stopReporting();
108115
originalStop();
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { describe, expect, it } from 'vitest';
2+
import type { Session, Telemetry } from '@irdashies/types';
3+
import { SessionBarProcessor } from './SessionBarProcessor';
4+
5+
describe('SessionBarProcessor', () => {
6+
it('projects the auxiliary session bar state and resets with lifecycle', () => {
7+
const processor = new SessionBarProcessor();
8+
processor.init({
9+
WeekendInfo: {
10+
TrackDisplayName: 'Okayama',
11+
WeekendOptions: { IncidentLimit: 17 },
12+
},
13+
DriverInfo: {
14+
DriverCarIdx: 0,
15+
Drivers: [{ CarIdx: 0, CarID: 67, CarClassID: 1 }],
16+
},
17+
SessionInfo: {
18+
Sessions: [{ SessionNum: 1, SessionName: 'Race', SessionType: 'Race' }],
19+
},
20+
} as unknown as Session);
21+
processor.onFrame({
22+
SessionTime: { value: [1] },
23+
SessionNum: { value: [1] },
24+
DisplayUnits: { value: [1] },
25+
FuelLevel: { value: [30] },
26+
PlayerCarTeamIncidentCount: { value: [2] },
27+
CarIdxPosition: { value: [1] },
28+
CarIdxClassPosition: { value: [1] },
29+
CarIdxBestLapTime: { value: [90] },
30+
Lap: { value: [2] },
31+
Speed: { value: [50] },
32+
} as unknown as Telemetry);
33+
expect(processor.snapshot()).toMatchObject({
34+
sessionName: 'Race',
35+
trackDisplayName: 'Okayama',
36+
fuelLevel: 30,
37+
incidents: 2,
38+
playerClassPosition: 1,
39+
playerClassSize: 1,
40+
sessionBestLap: 90,
41+
});
42+
processor.onFrame({
43+
SessionTime: { value: [1.05] },
44+
SessionNum: { value: [1] },
45+
Lap: { value: [2] },
46+
Speed: { value: [70] },
47+
} as unknown as Telemetry);
48+
processor.onFrame({
49+
SessionTime: { value: [1.2] },
50+
SessionNum: { value: [1] },
51+
Lap: { value: [2] },
52+
Speed: { value: [40] },
53+
} as unknown as Telemetry);
54+
expect(processor.snapshot().sessionBestTopSpeed).toBe(70);
55+
processor.onLifecycle({ type: 'disconnect' });
56+
expect(processor.snapshot()).toMatchObject({
57+
sessionNum: null,
58+
incidents: 0,
59+
});
60+
});
61+
});
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import type {
2+
Session,
3+
SessionBarSnapshot,
4+
SessionLifecycleEvent,
5+
Telemetry,
6+
} from '@irdashies/types';
7+
import type { TelemetryProcessor } from './TelemetryProcessor';
8+
9+
const n = (f: Telemetry, k: keyof Telemetry): number | undefined => {
10+
const v = f[k]?.value?.[0];
11+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
12+
};
13+
const a = (f: Telemetry, k: keyof Telemetry): readonly unknown[] => {
14+
const value = f[k]?.value;
15+
return Array.isArray(value) ? value : [];
16+
};
17+
18+
export class SessionBarProcessor implements TelemetryProcessor<SessionBarSnapshot> {
19+
readonly channel = 'session-bar.snapshot';
20+
readonly tickRateHz = 5;
21+
private session?: Session;
22+
private lastTime = -Infinity;
23+
private lap = -1;
24+
private lapTop = 0;
25+
private enabled = true;
26+
private readonly latest = this.empty();
27+
28+
init(session: Session): void {
29+
this.session = session;
30+
}
31+
onFrame(frame: Telemetry): void {
32+
if (!this.enabled) return;
33+
const time = n(frame, 'SessionTime');
34+
if (time === undefined) return;
35+
const sessionNum = n(frame, 'SessionNum') ?? null;
36+
if (
37+
this.latest.sessionNum !== null &&
38+
sessionNum !== this.latest.sessionNum
39+
)
40+
this.reset(sessionNum);
41+
const currentLap = n(frame, 'Lap') ?? 0;
42+
const speed = n(frame, 'Speed') ?? 0;
43+
if (this.lap >= 0 && currentLap !== this.lap) {
44+
this.latest.lastLapTopSpeed = this.lapTop || null;
45+
this.lapTop = 0;
46+
}
47+
this.lap = currentLap;
48+
this.lapTop = Math.max(this.lapTop, speed);
49+
this.latest.sessionBestTopSpeed =
50+
Math.max(this.latest.sessionBestTopSpeed ?? 0, speed) || null;
51+
if (time < this.lastTime || time - this.lastTime < 0.2 - 1e-6) return;
52+
this.lastTime = time;
53+
const info = this.session?.SessionInfo?.Sessions?.find(
54+
(s) => s.SessionNum === sessionNum
55+
);
56+
const drivers = this.session?.DriverInfo?.Drivers ?? [];
57+
const playerCarIdx = this.session?.DriverInfo?.DriverCarIdx ?? null;
58+
const player = drivers.find((d) => d?.CarIdx === playerCarIdx);
59+
const positions = a(frame, 'CarIdxPosition');
60+
const classPositions = a(frame, 'CarIdxClassPosition');
61+
this.latest.competitorCarIds.length = 0;
62+
this.latest.competitorPositions.length = 0;
63+
let classSize = 0;
64+
for (const driver of drivers) {
65+
if (!driver || driver.CarIsPaceCar || driver.IsSpectator) continue;
66+
if (driver.CarClassID === player?.CarClassID) classSize += 1;
67+
this.latest.competitorCarIds.push(driver.CarID ?? 0);
68+
const position = positions[driver.CarIdx];
69+
this.latest.competitorPositions.push(
70+
typeof position === 'number' ? position : 0
71+
);
72+
}
73+
const bestLaps = a(frame, 'CarIdxBestLapTime');
74+
let sessionBest: number | undefined;
75+
for (const value of bestLaps)
76+
if (
77+
typeof value === 'number' &&
78+
value > 0 &&
79+
(sessionBest === undefined || value < sessionBest)
80+
)
81+
sessionBest = value;
82+
const isClio = player?.CarID === 162;
83+
Object.assign(this.latest, {
84+
sessionName: info?.SessionType,
85+
trackDisplayName: this.session?.WeekendInfo?.TrackDisplayName,
86+
displayUnits: n(frame, 'DisplayUnits') ?? 0,
87+
brakeBias: n(frame, isClio ? 'dcPeakBrakeBias' : 'dcBrakeBias'),
88+
brakeBiasIsClio: isClio,
89+
incidents: n(frame, 'PlayerCarTeamIncidentCount') ?? 0,
90+
incidentLimit: this.session?.WeekendInfo?.WeekendOptions?.IncidentLimit,
91+
incidentWarningInitialLimit:
92+
this.session?.WeekendInfo?.WeekendOptions?.IncidentWarningInitialLimit,
93+
incidentWarningSubsequentLimit:
94+
this.session?.WeekendInfo?.WeekendOptions
95+
?.IncidentWarningSubsequentLimit,
96+
trackWetness: n(frame, 'TrackWetness') ?? 0,
97+
precipitation: n(frame, 'Precipitation'),
98+
airTemp: n(frame, 'AirTemp'),
99+
trackTemp: n(frame, 'TrackTempCrew'),
100+
windDirection: n(frame, 'WindDir'),
101+
windVelocity: n(frame, 'WindVel'),
102+
windYaw: n(frame, 'YawNorth'),
103+
fuelLevel: n(frame, 'FuelLevel'),
104+
lastLapTime: n(frame, 'LapLastLapTime'),
105+
bestLapTime: n(frame, 'LapBestLapTime'),
106+
sessionBestLap: sessionBest,
107+
sessionTimeOfDay: n(frame, 'SessionTimeOfDay'),
108+
playerCarIdx,
109+
playerCarId: player?.CarID,
110+
playerOverallPosition:
111+
typeof positions[playerCarIdx ?? -1] === 'number'
112+
? (positions[playerCarIdx ?? -1] as number)
113+
: 0,
114+
playerClassPosition:
115+
typeof classPositions[playerCarIdx ?? -1] === 'number'
116+
? (classPositions[playerCarIdx ?? -1] as number)
117+
: 0,
118+
playerClassSize: classSize,
119+
sessionNum,
120+
version: this.latest.version + 1,
121+
});
122+
}
123+
onLifecycle(event: SessionLifecycleEvent): void {
124+
if (event.type === 'enter') {
125+
this.enabled = !event.replay;
126+
if (event.replay) this.reset(null);
127+
} else this.reset(null);
128+
}
129+
snapshot(): SessionBarSnapshot {
130+
return this.latest;
131+
}
132+
private reset(sessionNum: number | null): void {
133+
const version = this.latest.version + 1;
134+
Object.assign(this.latest, this.empty(), { sessionNum, version });
135+
this.lastTime = -Infinity;
136+
this.lap = -1;
137+
this.lapTop = 0;
138+
}
139+
private empty(): SessionBarSnapshot {
140+
return {
141+
displayUnits: 0,
142+
brakeBiasIsClio: false,
143+
incidents: 0,
144+
trackWetness: 0,
145+
playerCarIdx: null,
146+
playerOverallPosition: 0,
147+
playerClassPosition: 0,
148+
playerClassSize: 0,
149+
competitorCarIds: [],
150+
competitorPositions: [],
151+
lastLapTopSpeed: null,
152+
sessionBestTopSpeed: null,
153+
sessionNum: null,
154+
version: 0,
155+
};
156+
}
157+
}

0 commit comments

Comments
 (0)