@hotlap.ai/telemetry 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +3 -3
- package/README.md +4 -2
- package/dist/cli.js +3 -3
- package/dist/{index-t4qz2011.js → index-ndbp3f5t.js} +194 -34
- package/dist/lap-points.d.ts +76 -0
- package/dist/lap-points.d.ts.map +1 -0
- package/dist/live.d.ts +34 -1
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +1 -1
- package/dist/neutral/lap-points.js +156 -0
- package/dist/telemetry-server.d.ts +100 -1
- package/dist/telemetry-server.d.ts.map +1 -1
- package/dist/telemetry-server.js +462 -126
- package/package.json +9 -2
package/dist/telemetry-server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
import {
|
|
4
4
|
createIRSDK
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-ndbp3f5t.js";
|
|
6
6
|
import"./index-hxw291vy.js";
|
|
7
7
|
import"./index-c5jf6rjt.js";
|
|
8
8
|
import"./index-wb6ebd0j.js";
|
|
@@ -12,20 +12,98 @@ import"./index-ftpvnye9.js";
|
|
|
12
12
|
var TELEMETRY_PORT = 32100;
|
|
13
13
|
var TELEMETRY_RATE_HZ = 60;
|
|
14
14
|
var SESSION_INFO_RATE_HZ = 1;
|
|
15
|
+
var FIELD_PUBLISH_EVERY = 4;
|
|
16
|
+
var PROTOCOL_VERSION = 3;
|
|
17
|
+
var CORS_HEADERS = {
|
|
18
|
+
"Access-Control-Allow-Origin": "*",
|
|
19
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
20
|
+
"Access-Control-Allow-Headers": "Content-Type"
|
|
21
|
+
};
|
|
15
22
|
var ir = createIRSDK();
|
|
16
23
|
var tickCount = 0;
|
|
17
24
|
var lastLap = -1;
|
|
18
25
|
var lastSessionNum = -1;
|
|
19
|
-
var
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
var lastSessionInfoFingerprint = "";
|
|
27
|
+
var telemetryKeys = [
|
|
28
|
+
"SessionTime",
|
|
29
|
+
"Lap",
|
|
30
|
+
"LapDistPct",
|
|
31
|
+
"Speed",
|
|
32
|
+
"RPM",
|
|
33
|
+
"Gear",
|
|
34
|
+
"Throttle",
|
|
35
|
+
"Brake",
|
|
36
|
+
"Clutch",
|
|
37
|
+
"SteeringWheelAngle",
|
|
38
|
+
"Lat",
|
|
39
|
+
"Lon",
|
|
40
|
+
"Alt",
|
|
41
|
+
"Yaw",
|
|
42
|
+
"Pitch",
|
|
43
|
+
"Roll",
|
|
44
|
+
"LapCurrentLapTime",
|
|
45
|
+
"LapLastLapTime",
|
|
46
|
+
"LapBestLapTime",
|
|
47
|
+
"LapDeltaToBestLap",
|
|
48
|
+
"FuelLevel",
|
|
49
|
+
"FuelLevelPct",
|
|
50
|
+
"OilTemp",
|
|
51
|
+
"WaterTemp",
|
|
52
|
+
"IsOnTrack",
|
|
53
|
+
"PlayerCarPosition",
|
|
54
|
+
"PlayerCarClassPosition",
|
|
55
|
+
"BrakeABSactive",
|
|
56
|
+
"dcBrakeBias",
|
|
57
|
+
"dcTractionControl",
|
|
58
|
+
"dcABS",
|
|
59
|
+
"LFtempCL",
|
|
60
|
+
"RFtempCL",
|
|
61
|
+
"LRtempCL",
|
|
62
|
+
"RRtempCL",
|
|
63
|
+
"LFpressure",
|
|
64
|
+
"RFpressure",
|
|
65
|
+
"LRpressure",
|
|
66
|
+
"RRpressure",
|
|
67
|
+
"LFwearM",
|
|
68
|
+
"RFwearM",
|
|
69
|
+
"LRwearM",
|
|
70
|
+
"RRwearM",
|
|
71
|
+
"SessionNum",
|
|
72
|
+
"OnPitRoad",
|
|
73
|
+
"PlayerCarIdx",
|
|
74
|
+
"CarIdxLapDistPct",
|
|
75
|
+
"SessionFlags",
|
|
76
|
+
"CarLeftRight"
|
|
77
|
+
];
|
|
78
|
+
var fieldKeys = [
|
|
79
|
+
"CarIdxPosition",
|
|
80
|
+
"CarIdxClassPosition",
|
|
81
|
+
"CarIdxLap",
|
|
82
|
+
"CarIdxLapCompleted",
|
|
83
|
+
"CarIdxEstTime",
|
|
84
|
+
"CarIdxF2Time",
|
|
85
|
+
"CarIdxOnPitRoad",
|
|
86
|
+
"CarIdxTrackSurface",
|
|
87
|
+
"CarIdxLastLapTime",
|
|
88
|
+
"CarIdxBestLapTime",
|
|
89
|
+
"SessionState",
|
|
90
|
+
"SessionTimeRemain",
|
|
91
|
+
"SessionLapsRemainEx"
|
|
92
|
+
];
|
|
93
|
+
var telemetryData = {};
|
|
94
|
+
for (const key of telemetryKeys) {
|
|
95
|
+
telemetryData[key] = null;
|
|
96
|
+
}
|
|
97
|
+
var fieldData = {};
|
|
98
|
+
for (const key of fieldKeys) {
|
|
99
|
+
fieldData[key] = null;
|
|
100
|
+
}
|
|
101
|
+
var roundedLapDistPct = new Array(64).fill(-1);
|
|
102
|
+
function numArray(value) {
|
|
103
|
+
return Array.isArray(value) ? value : [];
|
|
104
|
+
}
|
|
105
|
+
function boolArray(value) {
|
|
106
|
+
return Array.isArray(value) ? value : [];
|
|
29
107
|
}
|
|
30
108
|
function getSessionInfo() {
|
|
31
109
|
const weekendInfo = ir.getSessionInfo("WeekendInfo");
|
|
@@ -36,6 +114,7 @@ function getSessionInfo() {
|
|
|
36
114
|
const sessionNum = ir.get("SessionNum") ?? 0;
|
|
37
115
|
return {
|
|
38
116
|
trackId: weekendInfo.TrackID,
|
|
117
|
+
sectors: getSectorBoundaries(),
|
|
39
118
|
trackName: weekendInfo.TrackName,
|
|
40
119
|
trackDisplayName: weekendInfo.TrackDisplayName,
|
|
41
120
|
trackLength: weekendInfo.TrackLength,
|
|
@@ -50,70 +129,230 @@ function getSessionInfo() {
|
|
|
50
129
|
tickRate: 60
|
|
51
130
|
};
|
|
52
131
|
}
|
|
132
|
+
function getSectorBoundaries() {
|
|
133
|
+
const splitTimeInfo = ir.getSessionInfo("SplitTimeInfo");
|
|
134
|
+
const sectors = splitTimeInfo?.Sectors;
|
|
135
|
+
if (!Array.isArray(sectors))
|
|
136
|
+
return [];
|
|
137
|
+
return sectors.map((s) => ({ sectorNum: s.SectorNum ?? -1, startPct: s.SectorStartPct ?? -1 })).filter((s) => s.sectorNum >= 0 && s.startPct >= 0 && s.startPct < 1).sort((a, b) => a.startPct - b.startPct);
|
|
138
|
+
}
|
|
139
|
+
function readNullableNum(key) {
|
|
140
|
+
const value = telemetryData[key];
|
|
141
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
142
|
+
}
|
|
53
143
|
function getTelemetryFrame() {
|
|
54
|
-
ir.
|
|
144
|
+
ir.getMultipleInto(telemetryKeys, telemetryData);
|
|
55
145
|
const frame = {
|
|
56
146
|
tick: tickCount++,
|
|
57
|
-
sessionTime:
|
|
58
|
-
lap:
|
|
59
|
-
lapDistPct:
|
|
60
|
-
speed:
|
|
61
|
-
rpm:
|
|
62
|
-
gear:
|
|
63
|
-
throttle:
|
|
64
|
-
brake:
|
|
65
|
-
clutch:
|
|
66
|
-
steer:
|
|
67
|
-
lat:
|
|
68
|
-
lon:
|
|
69
|
-
alt:
|
|
70
|
-
yaw:
|
|
71
|
-
pitch:
|
|
72
|
-
roll:
|
|
73
|
-
lapCurrentLapTime:
|
|
74
|
-
lapLastLapTime:
|
|
75
|
-
lapBestLapTime:
|
|
76
|
-
lapDeltaToBestLap:
|
|
77
|
-
fuelLevel:
|
|
78
|
-
fuelLevelPct:
|
|
79
|
-
oilTemp:
|
|
80
|
-
waterTemp:
|
|
81
|
-
isOnTrack:
|
|
82
|
-
playerPosition:
|
|
83
|
-
playerClassPosition:
|
|
84
|
-
brakeABSactive:
|
|
85
|
-
dcBrakeBias:
|
|
86
|
-
dcTractionControl:
|
|
87
|
-
dcABS:
|
|
88
|
-
tireTempLF:
|
|
89
|
-
tireTempRF:
|
|
90
|
-
tireTempLR:
|
|
91
|
-
tireTempRR:
|
|
147
|
+
sessionTime: telemetryData.SessionTime ?? 0,
|
|
148
|
+
lap: telemetryData.Lap ?? 0,
|
|
149
|
+
lapDistPct: telemetryData.LapDistPct ?? 0,
|
|
150
|
+
speed: telemetryData.Speed ?? 0,
|
|
151
|
+
rpm: telemetryData.RPM ?? 0,
|
|
152
|
+
gear: telemetryData.Gear ?? 0,
|
|
153
|
+
throttle: telemetryData.Throttle ?? 0,
|
|
154
|
+
brake: telemetryData.Brake ?? 0,
|
|
155
|
+
clutch: telemetryData.Clutch ?? 0,
|
|
156
|
+
steer: telemetryData.SteeringWheelAngle ?? 0,
|
|
157
|
+
lat: telemetryData.Lat ?? 0,
|
|
158
|
+
lon: telemetryData.Lon ?? 0,
|
|
159
|
+
alt: telemetryData.Alt ?? 0,
|
|
160
|
+
yaw: telemetryData.Yaw ?? 0,
|
|
161
|
+
pitch: telemetryData.Pitch ?? 0,
|
|
162
|
+
roll: telemetryData.Roll ?? 0,
|
|
163
|
+
lapCurrentLapTime: telemetryData.LapCurrentLapTime ?? 0,
|
|
164
|
+
lapLastLapTime: telemetryData.LapLastLapTime ?? 0,
|
|
165
|
+
lapBestLapTime: telemetryData.LapBestLapTime ?? 0,
|
|
166
|
+
lapDeltaToBestLap: telemetryData.LapDeltaToBestLap ?? 0,
|
|
167
|
+
fuelLevel: telemetryData.FuelLevel ?? 0,
|
|
168
|
+
fuelLevelPct: telemetryData.FuelLevelPct ?? 0,
|
|
169
|
+
oilTemp: telemetryData.OilTemp ?? 0,
|
|
170
|
+
waterTemp: telemetryData.WaterTemp ?? 0,
|
|
171
|
+
isOnTrack: telemetryData.IsOnTrack ?? false,
|
|
172
|
+
playerPosition: telemetryData.PlayerCarPosition ?? 0,
|
|
173
|
+
playerClassPosition: telemetryData.PlayerCarClassPosition ?? 0,
|
|
174
|
+
brakeABSactive: telemetryData.BrakeABSactive ?? false,
|
|
175
|
+
dcBrakeBias: telemetryData.dcBrakeBias ?? 0,
|
|
176
|
+
dcTractionControl: telemetryData.dcTractionControl ?? 0,
|
|
177
|
+
dcABS: telemetryData.dcABS ?? 0,
|
|
178
|
+
tireTempLF: telemetryData.LFtempCL ?? 0,
|
|
179
|
+
tireTempRF: telemetryData.RFtempCL ?? 0,
|
|
180
|
+
tireTempLR: telemetryData.LRtempCL ?? 0,
|
|
181
|
+
tireTempRR: telemetryData.RRtempCL ?? 0,
|
|
182
|
+
tirePressureLF: readNullableNum("LFpressure"),
|
|
183
|
+
tirePressureRF: readNullableNum("RFpressure"),
|
|
184
|
+
tirePressureLR: readNullableNum("LRpressure"),
|
|
185
|
+
tirePressureRR: readNullableNum("RRpressure"),
|
|
186
|
+
tireWearLF: readNullableNum("LFwearM"),
|
|
187
|
+
tireWearRF: readNullableNum("RFwearM"),
|
|
188
|
+
tireWearLR: readNullableNum("LRwearM"),
|
|
189
|
+
tireWearRR: readNullableNum("RRwearM"),
|
|
190
|
+
sessionNum: telemetryData.SessionNum ?? 0,
|
|
191
|
+
onPitRoad: telemetryData.OnPitRoad ?? false,
|
|
192
|
+
playerCarIdx: telemetryData.PlayerCarIdx ?? -1,
|
|
193
|
+
carIdxLapDistPct: roundCarIdxLapDistPct(numArray(telemetryData.CarIdxLapDistPct)),
|
|
194
|
+
sessionFlags: (telemetryData.SessionFlags ?? 0) >>> 0,
|
|
195
|
+
carLeftRight: telemetryData.CarLeftRight ?? 0
|
|
92
196
|
};
|
|
93
|
-
ir.unfreezeVarBufferLatest();
|
|
94
197
|
return frame;
|
|
95
198
|
}
|
|
199
|
+
function roundCarIdxLapDistPct(raw) {
|
|
200
|
+
for (let i = 0;i < roundedLapDistPct.length; i++) {
|
|
201
|
+
const v = raw[i];
|
|
202
|
+
roundedLapDistPct[i] = v == null || v < 0 ? -1 : Math.round(v * 1e4) / 1e4;
|
|
203
|
+
}
|
|
204
|
+
return roundedLapDistPct;
|
|
205
|
+
}
|
|
206
|
+
var cachedPaceCarIdx = -1;
|
|
207
|
+
function getFieldData(frame) {
|
|
208
|
+
ir.getMultipleInto(fieldKeys, fieldData);
|
|
209
|
+
const round3 = (v) => Math.round(v * 1000) / 1000;
|
|
210
|
+
return {
|
|
211
|
+
tick: frame.tick,
|
|
212
|
+
sessionTime: frame.sessionTime,
|
|
213
|
+
sessionNum: frame.sessionNum,
|
|
214
|
+
sessionState: fieldData.SessionState ?? 0,
|
|
215
|
+
sessionTimeRemain: fieldData.SessionTimeRemain ?? -1,
|
|
216
|
+
sessionLapsRemain: fieldData.SessionLapsRemainEx ?? -1,
|
|
217
|
+
playerCarIdx: frame.playerCarIdx,
|
|
218
|
+
paceCarIdx: cachedPaceCarIdx,
|
|
219
|
+
position: numArray(fieldData.CarIdxPosition),
|
|
220
|
+
classPosition: numArray(fieldData.CarIdxClassPosition),
|
|
221
|
+
lap: numArray(fieldData.CarIdxLap),
|
|
222
|
+
lapCompleted: numArray(fieldData.CarIdxLapCompleted),
|
|
223
|
+
lapDistPct: frame.carIdxLapDistPct,
|
|
224
|
+
estTime: numArray(fieldData.CarIdxEstTime).map(round3),
|
|
225
|
+
f2Time: numArray(fieldData.CarIdxF2Time).map(round3),
|
|
226
|
+
onPitRoad: boolArray(fieldData.CarIdxOnPitRoad),
|
|
227
|
+
trackSurface: numArray(fieldData.CarIdxTrackSurface),
|
|
228
|
+
lastLapTime: numArray(fieldData.CarIdxLastLapTime).map(round3),
|
|
229
|
+
bestLapTime: numArray(fieldData.CarIdxBestLapTime).map(round3)
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function computeSof(ratings) {
|
|
233
|
+
if (ratings.length === 0)
|
|
234
|
+
return 0;
|
|
235
|
+
const LN2 = Math.log(2);
|
|
236
|
+
const sum = ratings.reduce((acc, r) => acc + Math.pow(2, -r / 1600), 0);
|
|
237
|
+
if (sum <= 0)
|
|
238
|
+
return 0;
|
|
239
|
+
return Math.round(1600 / LN2 * Math.log(ratings.length / sum));
|
|
240
|
+
}
|
|
241
|
+
function buildSessionState(sessionInfoUpdate) {
|
|
242
|
+
const weekendInfo = ir.getSessionInfo("WeekendInfo");
|
|
243
|
+
const driverInfo = ir.getSessionInfo("DriverInfo");
|
|
244
|
+
if (!weekendInfo || !driverInfo)
|
|
245
|
+
return null;
|
|
246
|
+
const sessionNum = ir.get("SessionNum") ?? 0;
|
|
247
|
+
cachedPaceCarIdx = driverInfo.PaceCarIdx ?? -1;
|
|
248
|
+
const drivers = [];
|
|
249
|
+
for (const d of driverInfo.Drivers ?? []) {
|
|
250
|
+
if (!d || d.CarIdx == null || d.CarIdx < 0)
|
|
251
|
+
continue;
|
|
252
|
+
if (d.IsSpectator || d.CarIsPaceCar)
|
|
253
|
+
continue;
|
|
254
|
+
drivers.push({
|
|
255
|
+
carIdx: d.CarIdx,
|
|
256
|
+
userName: d.UserName ?? "Unknown",
|
|
257
|
+
teamName: d.TeamName || undefined,
|
|
258
|
+
carNumber: d.CarNumber ?? "",
|
|
259
|
+
carClassId: d.CarClassID ?? 0,
|
|
260
|
+
carClassShortName: d.CarClassShortName ?? "",
|
|
261
|
+
carClassColor: d.CarClassColor ?? 0,
|
|
262
|
+
carClassRelSpeed: d.CarClassRelSpeed ?? 0,
|
|
263
|
+
carClassEstLapTime: d.CarClassEstLapTime ?? 0,
|
|
264
|
+
iRating: d.IRating ?? 0,
|
|
265
|
+
licString: d.LicString ?? "",
|
|
266
|
+
licColor: d.LicColor ?? 0,
|
|
267
|
+
incidentCount: d.CurDriverIncidentCount ?? 0
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const byClass = new Map;
|
|
271
|
+
for (const d of drivers) {
|
|
272
|
+
const list = byClass.get(d.carClassId);
|
|
273
|
+
if (list)
|
|
274
|
+
list.push(d);
|
|
275
|
+
else
|
|
276
|
+
byClass.set(d.carClassId, [d]);
|
|
277
|
+
}
|
|
278
|
+
const classes = [...byClass.entries()].map(([classId, list]) => ({
|
|
279
|
+
classId,
|
|
280
|
+
shortName: list[0]?.carClassShortName ?? "",
|
|
281
|
+
color: list[0]?.carClassColor ?? 0,
|
|
282
|
+
sof: computeSof(list.map((d) => d.iRating).filter((r) => r > 0)),
|
|
283
|
+
carCount: list.length
|
|
284
|
+
}));
|
|
285
|
+
const sessionsInfo = ir.getSessionInfo("SessionInfo");
|
|
286
|
+
const session = sessionsInfo?.Sessions?.find((s) => s.SessionNum === sessionNum) ?? sessionsInfo?.Sessions?.[sessionsInfo.Sessions.length - 1];
|
|
287
|
+
return {
|
|
288
|
+
sessionInfoUpdate,
|
|
289
|
+
subSessionId: weekendInfo.SubSessionID ?? 0,
|
|
290
|
+
trackId: weekendInfo.TrackID,
|
|
291
|
+
sessionNum,
|
|
292
|
+
sessionType: session?.SessionType ?? weekendInfo.EventType ?? "Unknown",
|
|
293
|
+
sessionLaps: session?.SessionLaps ?? "unlimited",
|
|
294
|
+
sessionTimeTotal: session?.SessionTime ?? "unlimited",
|
|
295
|
+
drivers,
|
|
296
|
+
classes,
|
|
297
|
+
results: (session?.ResultsPositions ?? []).map((r) => ({
|
|
298
|
+
carIdx: r.CarIdx ?? -1,
|
|
299
|
+
position: r.Position ?? 0,
|
|
300
|
+
classPosition: r.ClassPosition ?? 0,
|
|
301
|
+
fastestTime: r.FastestTime ?? -1,
|
|
302
|
+
lastTime: r.LastTime ?? -1,
|
|
303
|
+
lapsComplete: r.LapsComplete ?? 0,
|
|
304
|
+
incidents: r.Incidents ?? 0
|
|
305
|
+
})),
|
|
306
|
+
fastestLap: (session?.ResultsFastestLap ?? []).map((f) => ({
|
|
307
|
+
carIdx: f.CarIdx ?? -1,
|
|
308
|
+
fastestTime: f.FastestTime ?? -1
|
|
309
|
+
}))
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
var LAP_CHANGE_SETTLE_TICKS = 60;
|
|
313
|
+
var pendingLapChange = null;
|
|
314
|
+
function emitLapChange(pending, lapTime, server) {
|
|
315
|
+
const lapChangeMsg = {
|
|
316
|
+
type: "lap_change",
|
|
317
|
+
data: {
|
|
318
|
+
fromLap: pending.fromLap,
|
|
319
|
+
toLap: pending.toLap,
|
|
320
|
+
lapTime,
|
|
321
|
+
sessionNum: pending.sessionNum
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
server.publish("telemetry", JSON.stringify(lapChangeMsg));
|
|
325
|
+
console.log(`Lap ${pending.fromLap} completed: ${formatLapTime(lapTime)}`);
|
|
326
|
+
}
|
|
96
327
|
function checkLapChange(frame, server) {
|
|
97
328
|
const currentLap = frame.lap;
|
|
98
329
|
const currentSessionNum = ir.get("SessionNum") ?? 0;
|
|
99
330
|
if (currentSessionNum !== lastSessionNum) {
|
|
100
331
|
lastSessionNum = currentSessionNum;
|
|
101
332
|
lastLap = currentLap;
|
|
102
|
-
|
|
333
|
+
lastSessionInfoFingerprint = "";
|
|
334
|
+
pendingLapChange = null;
|
|
103
335
|
return;
|
|
104
336
|
}
|
|
337
|
+
if (pendingLapChange) {
|
|
338
|
+
pendingLapChange.ticksWaited++;
|
|
339
|
+
const settled = frame.lapLastLapTime > 0 && frame.lapLastLapTime !== pendingLapChange.lastLapTimeAtChange;
|
|
340
|
+
if (settled || pendingLapChange.ticksWaited >= LAP_CHANGE_SETTLE_TICKS) {
|
|
341
|
+
emitLapChange(pendingLapChange, frame.lapLastLapTime, server);
|
|
342
|
+
pendingLapChange = null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
105
345
|
if (currentLap !== lastLap && lastLap !== -1) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
346
|
+
if (pendingLapChange) {
|
|
347
|
+
emitLapChange(pendingLapChange, frame.lapLastLapTime, server);
|
|
348
|
+
}
|
|
349
|
+
pendingLapChange = {
|
|
350
|
+
fromLap: lastLap,
|
|
351
|
+
toLap: currentLap,
|
|
352
|
+
sessionNum: currentSessionNum,
|
|
353
|
+
ticksWaited: 0,
|
|
354
|
+
lastLapTimeAtChange: frame.lapLastLapTime
|
|
114
355
|
};
|
|
115
|
-
server.publish("telemetry", JSON.stringify(lapChangeMsg));
|
|
116
|
-
console.log(`Lap ${lastLap} completed: ${formatLapTime(frame.lapLastLapTime)}`);
|
|
117
356
|
}
|
|
118
357
|
lastLap = currentLap;
|
|
119
358
|
}
|
|
@@ -124,95 +363,192 @@ function formatLapTime(seconds) {
|
|
|
124
363
|
const secs = (seconds % 60).toFixed(3).padStart(6, "0");
|
|
125
364
|
return `${mins}:${secs}`;
|
|
126
365
|
}
|
|
366
|
+
var cachedSessionStateJson = null;
|
|
127
367
|
async function main() {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
initialized: ir.isInitialized,
|
|
153
|
-
sessionInfo: getSessionInfo()
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
if (url.pathname === "/health") {
|
|
157
|
-
return new Response("OK", { status: 200 });
|
|
158
|
-
}
|
|
159
|
-
return new Response("Not found", { status: 404 });
|
|
160
|
-
},
|
|
161
|
-
websocket: {
|
|
162
|
-
open(ws) {
|
|
163
|
-
console.log("Client connected");
|
|
164
|
-
ws.subscribe("telemetry");
|
|
165
|
-
const statusMsg = {
|
|
166
|
-
type: "status",
|
|
167
|
-
data: {
|
|
168
|
-
connected: ir.isConnected,
|
|
169
|
-
initialized: ir.isInitialized
|
|
368
|
+
const server = (() => {
|
|
369
|
+
try {
|
|
370
|
+
return Bun.serve({
|
|
371
|
+
port: TELEMETRY_PORT,
|
|
372
|
+
hostname: "127.0.0.1",
|
|
373
|
+
fetch(req, server2) {
|
|
374
|
+
const url = new URL(req.url);
|
|
375
|
+
if (url.pathname === "/ws" || req.headers.get("upgrade") === "websocket") {
|
|
376
|
+
const success = server2.upgrade(req);
|
|
377
|
+
if (success) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
return new Response("WebSocket upgrade failed", { status: 400, headers: CORS_HEADERS });
|
|
381
|
+
}
|
|
382
|
+
if (req.method === "OPTIONS") {
|
|
383
|
+
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
384
|
+
}
|
|
385
|
+
if (url.pathname === "/status") {
|
|
386
|
+
return Response.json({
|
|
387
|
+
connected: ir.isConnected,
|
|
388
|
+
initialized: ir.isInitialized,
|
|
389
|
+
protocol: PROTOCOL_VERSION,
|
|
390
|
+
sessionInfo: getSessionInfo()
|
|
391
|
+
}, { headers: CORS_HEADERS });
|
|
170
392
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
393
|
+
if (url.pathname === "/health") {
|
|
394
|
+
return Response.json({ ok: true, protocol: PROTOCOL_VERSION }, { headers: CORS_HEADERS });
|
|
395
|
+
}
|
|
396
|
+
if (url.pathname === "/shutdown" && req.method === "POST") {
|
|
397
|
+
console.log("Shutdown requested by client");
|
|
398
|
+
setTimeout(() => {
|
|
399
|
+
ir.shutdown();
|
|
400
|
+
process.exit(0);
|
|
401
|
+
}, 50);
|
|
402
|
+
return new Response("shutting down", { headers: CORS_HEADERS });
|
|
403
|
+
}
|
|
404
|
+
return new Response("Not found", { status: 404, headers: CORS_HEADERS });
|
|
405
|
+
},
|
|
406
|
+
websocket: {
|
|
407
|
+
open(ws) {
|
|
408
|
+
console.log("Client connected");
|
|
409
|
+
ws.subscribe("telemetry");
|
|
410
|
+
const statusMsg = {
|
|
411
|
+
type: "status",
|
|
412
|
+
data: {
|
|
413
|
+
connected: ir.isConnected,
|
|
414
|
+
initialized: ir.isInitialized
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
ws.send(JSON.stringify(statusMsg));
|
|
418
|
+
const sessionInfo = getSessionInfo();
|
|
419
|
+
if (sessionInfo) {
|
|
420
|
+
const sessionMsg = {
|
|
421
|
+
type: "session_info",
|
|
422
|
+
data: sessionInfo
|
|
423
|
+
};
|
|
424
|
+
ws.send(JSON.stringify(sessionMsg));
|
|
425
|
+
}
|
|
426
|
+
if (cachedSessionStateJson) {
|
|
427
|
+
ws.send(cachedSessionStateJson);
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
close(ws) {
|
|
431
|
+
console.log("Client disconnected");
|
|
432
|
+
},
|
|
433
|
+
message(ws, msg) {}
|
|
180
434
|
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
message(ws, msg) {}
|
|
435
|
+
});
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.error("Could not bind port \u2014 another sidecar instance is likely running:", err);
|
|
438
|
+
process.exit(0);
|
|
186
439
|
}
|
|
187
|
-
});
|
|
440
|
+
})();
|
|
188
441
|
console.log(`
|
|
189
442
|
Telemetry server running at ws://127.0.0.1:${server.port}/ws`);
|
|
190
443
|
console.log(`Status endpoint: http://127.0.0.1:${server.port}/status`);
|
|
191
|
-
console.log(`Waiting for
|
|
444
|
+
console.log(`Waiting for iRacing...
|
|
192
445
|
`);
|
|
446
|
+
const REATTACH_AFTER_MS = 1e4;
|
|
447
|
+
let startupInFlight = false;
|
|
448
|
+
let wasConnected = false;
|
|
449
|
+
let disconnectedSince = null;
|
|
450
|
+
const ensureIRacing = async () => {
|
|
451
|
+
if (startupInFlight)
|
|
452
|
+
return;
|
|
453
|
+
if (ir.isInitialized) {
|
|
454
|
+
if (ir.isConnected) {
|
|
455
|
+
disconnectedSince = null;
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
disconnectedSince ??= Date.now();
|
|
459
|
+
if (Date.now() - disconnectedSince < REATTACH_AFTER_MS)
|
|
460
|
+
return;
|
|
461
|
+
console.log("iRacing gone \u2014 detaching and waiting for a new session");
|
|
462
|
+
ir.shutdown();
|
|
463
|
+
lastSessionInfoFingerprint = "";
|
|
464
|
+
cachedSessionStateJson = null;
|
|
465
|
+
disconnectedSince = null;
|
|
466
|
+
}
|
|
467
|
+
startupInFlight = true;
|
|
468
|
+
try {
|
|
469
|
+
const ok = await ir.startup();
|
|
470
|
+
if (ok) {
|
|
471
|
+
console.log("Connected to iRacing!");
|
|
472
|
+
const sessionInfo = getSessionInfo();
|
|
473
|
+
if (sessionInfo) {
|
|
474
|
+
console.log(`Track: ${sessionInfo.trackDisplayName}`);
|
|
475
|
+
console.log(`Driver: ${sessionInfo.driverName}`);
|
|
476
|
+
console.log(`Car: ${sessionInfo.carName} #${sessionInfo.carNumber}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error("iRacing startup attempt failed:", err);
|
|
481
|
+
} finally {
|
|
482
|
+
startupInFlight = false;
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
await ensureIRacing();
|
|
486
|
+
setInterval(ensureIRacing, 2000);
|
|
193
487
|
let lastSessionInfoTime = 0;
|
|
488
|
+
let fieldTickCounter = 0;
|
|
489
|
+
let lastSessionStateCheck = 0;
|
|
490
|
+
let lastSessionStatePublish = 0;
|
|
491
|
+
let lastPublishedSessionInfoUpdate = -1;
|
|
194
492
|
setInterval(() => {
|
|
195
|
-
|
|
493
|
+
const connected = ir.isConnected && ir.isInitialized;
|
|
494
|
+
if (connected !== wasConnected) {
|
|
495
|
+
wasConnected = connected;
|
|
496
|
+
const statusMsg = {
|
|
497
|
+
type: "status",
|
|
498
|
+
data: { connected, initialized: ir.isInitialized }
|
|
499
|
+
};
|
|
500
|
+
server.publish("telemetry", JSON.stringify(statusMsg));
|
|
501
|
+
}
|
|
502
|
+
if (!connected) {
|
|
196
503
|
return;
|
|
197
504
|
}
|
|
505
|
+
ir.freezeVarBufferLatest();
|
|
198
506
|
const frame = getTelemetryFrame();
|
|
507
|
+
fieldTickCounter++;
|
|
508
|
+
const field = fieldTickCounter % FIELD_PUBLISH_EVERY === 0 ? getFieldData(frame) : null;
|
|
509
|
+
ir.unfreezeVarBufferLatest();
|
|
199
510
|
checkLapChange(frame, server);
|
|
200
511
|
const telemetryMsg = {
|
|
201
512
|
type: "telemetry",
|
|
202
513
|
data: frame
|
|
203
514
|
};
|
|
204
515
|
server.publish("telemetry", JSON.stringify(telemetryMsg));
|
|
516
|
+
if (field) {
|
|
517
|
+
const fieldMsg = { type: "field", data: field };
|
|
518
|
+
server.publish("telemetry", JSON.stringify(fieldMsg));
|
|
519
|
+
}
|
|
205
520
|
const now = Date.now();
|
|
206
521
|
if (now - lastSessionInfoTime > 1000 / SESSION_INFO_RATE_HZ) {
|
|
207
522
|
lastSessionInfoTime = now;
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
523
|
+
const weekendInfo = ir.getSessionInfo("WeekendInfo");
|
|
524
|
+
if (weekendInfo) {
|
|
525
|
+
const sessionNum = ir.get("SessionNum") ?? 0;
|
|
526
|
+
const fingerprint = `${weekendInfo.SubSessionID ?? 0}:${sessionNum}:${weekendInfo.TrackID ?? 0}`;
|
|
527
|
+
if (fingerprint !== lastSessionInfoFingerprint) {
|
|
528
|
+
const sessionInfo = getSessionInfo();
|
|
529
|
+
if (sessionInfo) {
|
|
530
|
+
const sessionMsg = {
|
|
531
|
+
type: "session_info",
|
|
532
|
+
data: sessionInfo
|
|
533
|
+
};
|
|
534
|
+
server.publish("telemetry", JSON.stringify(sessionMsg));
|
|
535
|
+
lastSessionInfoFingerprint = fingerprint;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if (now - lastSessionStateCheck > 500) {
|
|
541
|
+
lastSessionStateCheck = now;
|
|
542
|
+
const counter = ir.sessionInfoUpdate;
|
|
543
|
+
if (counter !== lastPublishedSessionInfoUpdate && now - lastSessionStatePublish >= 1000) {
|
|
544
|
+
const state = buildSessionState(counter);
|
|
545
|
+
if (state) {
|
|
546
|
+
const stateMsg = { type: "session_state", data: state };
|
|
547
|
+
cachedSessionStateJson = JSON.stringify(stateMsg);
|
|
548
|
+
server.publish("telemetry", cachedSessionStateJson);
|
|
549
|
+
lastPublishedSessionInfoUpdate = counter;
|
|
550
|
+
lastSessionStatePublish = now;
|
|
551
|
+
}
|
|
216
552
|
}
|
|
217
553
|
}
|
|
218
554
|
}, 1000 / TELEMETRY_RATE_HZ);
|