@hotlap.ai/telemetry 1.3.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 +6 -0
- package/dist/lap-points.d.ts.map +1 -1
- 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 +7 -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 +3 -1
package/CONTRIBUTING.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Contributing to @hotlap/telemetry
|
|
1
|
+
# Contributing to @hotlap.ai/telemetry
|
|
2
2
|
|
|
3
|
-
Thank you for your interest in contributing to the HotLap.ai telemetry
|
|
3
|
+
Thank you for your interest in contributing to the HotLap.ai telemetry library!
|
|
4
4
|
|
|
5
5
|
## Getting Started
|
|
6
6
|
|
|
@@ -52,7 +52,7 @@ bun run typecheck
|
|
|
52
52
|
src/
|
|
53
53
|
binary-reader.ts # Low-level binary parsing utilities
|
|
54
54
|
cli.ts # Command-line interface
|
|
55
|
-
constants.ts # iRacing
|
|
55
|
+
constants.ts # iRacing telemetry constants and enums
|
|
56
56
|
ibt.ts # IBT file parser
|
|
57
57
|
index.ts # Main exports
|
|
58
58
|
live.ts # Live telemetry via Windows FFI
|
package/README.md
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
[](https://bun.sh/)
|
|
9
9
|
|
|
10
|
-
High-performance iRacing telemetry
|
|
10
|
+
High-performance iRacing telemetry library for [Bun](https://bun.sh), built by the [HotLap.ai](https://hotlap.ai) team. Parse IBT telemetry files and stream live data from iRacing via Windows shared memory.
|
|
11
|
+
|
|
12
|
+
> **Note:** This is an unofficial community library, not affiliated with or endorsed by iRacing.com.
|
|
11
13
|
|
|
12
14
|
## Features
|
|
13
15
|
|
|
@@ -193,7 +195,7 @@ ir.pitCommand(pitCommandMode, variable);
|
|
|
193
195
|
| `OilTemp` | float | Oil temperature (C) |
|
|
194
196
|
| `WaterTemp` | float | Water temperature (C) |
|
|
195
197
|
|
|
196
|
-
See iRacing
|
|
198
|
+
See [iRacing documentation](https://www.iracing.com/) for the complete list of variables.
|
|
197
199
|
|
|
198
200
|
## Scripts
|
|
199
201
|
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./index-6wrtvcny.js";
|
|
6
6
|
import {
|
|
7
7
|
createIRSDK
|
|
8
|
-
} from "./index-
|
|
8
|
+
} from "./index-ndbp3f5t.js";
|
|
9
9
|
import"./index-hxw291vy.js";
|
|
10
10
|
import"./index-c5jf6rjt.js";
|
|
11
11
|
import {
|
|
@@ -36,7 +36,7 @@ var { values, positionals } = parseArgs({
|
|
|
36
36
|
});
|
|
37
37
|
function printHelp() {
|
|
38
38
|
console.log(`
|
|
39
|
-
|
|
39
|
+
HotLap.ai Telemetry CLI v${VERSION}
|
|
40
40
|
|
|
41
41
|
Usage: irsdk [options] [file.ibt]
|
|
42
42
|
|
|
@@ -67,7 +67,7 @@ Examples:
|
|
|
67
67
|
}
|
|
68
68
|
async function main() {
|
|
69
69
|
if (values.version) {
|
|
70
|
-
console.log(`
|
|
70
|
+
console.log(`HotLap.ai Telemetry CLI v${VERSION}`);
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
if (values.help) {
|
|
@@ -104,6 +104,25 @@ function ensureFFILoaded() {
|
|
|
104
104
|
return false;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
var YAML_SPECIAL_FIELDS_REGEX = /((?:DriverSetupName|UserName|TeamName|AbbrevName|Initials): )(.*)/g;
|
|
108
|
+
var YAML_COMMA_VALUE_REGEX = /(\w+: )(,.*)/g;
|
|
109
|
+
function cleanYamlFast(yaml) {
|
|
110
|
+
let cleaned = "";
|
|
111
|
+
for (let i = 0;i < yaml.length; i++) {
|
|
112
|
+
const code = yaml.charCodeAt(i);
|
|
113
|
+
if (code === 129 || code === 141 || code === 143 || code === 144 || code === 157) {
|
|
114
|
+
cleaned += " ";
|
|
115
|
+
} else if (code === 9 || code === 10 || code === 13 || code >= 32 && code <= 126 || code >= 160 && code <= 255) {
|
|
116
|
+
cleaned += yaml[i];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
cleaned = cleaned.replace(YAML_SPECIAL_FIELDS_REGEX, (_, prefix, value) => {
|
|
120
|
+
const escaped = value.replace(/["\\]/g, "\\$&");
|
|
121
|
+
return `${prefix}"${escaped}"`;
|
|
122
|
+
});
|
|
123
|
+
cleaned = cleaned.replace(YAML_COMMA_VALUE_REGEX, '$1"$2"');
|
|
124
|
+
return cleaned;
|
|
125
|
+
}
|
|
107
126
|
|
|
108
127
|
class IRSDK {
|
|
109
128
|
parseYamlAsync;
|
|
@@ -116,24 +135,74 @@ class IRSDK {
|
|
|
116
135
|
varHeaders = null;
|
|
117
136
|
varHeadersNames = null;
|
|
118
137
|
sessionInfoCache = new Map;
|
|
138
|
+
static sessionRegexCache = new Map;
|
|
119
139
|
broadcastMsgId = null;
|
|
120
140
|
lastSessionInfoUpdate = 0;
|
|
121
141
|
workaroundConnectedState = 0;
|
|
122
142
|
frozenBuffer = null;
|
|
123
143
|
frozenView = null;
|
|
124
144
|
frozenBufOffset = 0;
|
|
145
|
+
frozenBufferCapacity = 0;
|
|
125
146
|
testMode = false;
|
|
126
147
|
isInitialized = false;
|
|
127
148
|
constructor(options = {}) {
|
|
128
149
|
this.parseYamlAsync = options.parseYamlAsync ?? false;
|
|
129
150
|
}
|
|
151
|
+
liveHeaderInt(byteOffset, fallback = 0) {
|
|
152
|
+
if (this.testMode)
|
|
153
|
+
return fallback;
|
|
154
|
+
return this.sharedMemView ? this.sharedMemView.getInt32(byteOffset, true) : fallback;
|
|
155
|
+
}
|
|
156
|
+
get liveStatus() {
|
|
157
|
+
return this.liveHeaderInt(4, this.header?.status ?? 0);
|
|
158
|
+
}
|
|
159
|
+
liveReadBufOffset() {
|
|
160
|
+
const varBufs = this.header.varBuf;
|
|
161
|
+
if (this.testMode || !this.sharedMemView) {
|
|
162
|
+
return getLatestVarBuffer(varBufs).bufOffset;
|
|
163
|
+
}
|
|
164
|
+
let latestTick = -1;
|
|
165
|
+
let latestOffset = varBufs[0].bufOffset;
|
|
166
|
+
let secondTick = -1;
|
|
167
|
+
let secondOffset = varBufs[0].bufOffset;
|
|
168
|
+
for (let i = 0;i < varBufs.length; i++) {
|
|
169
|
+
const tick = this.liveHeaderInt(48 + i * 16);
|
|
170
|
+
const offset = varBufs[i].bufOffset;
|
|
171
|
+
if (tick > latestTick) {
|
|
172
|
+
secondTick = latestTick;
|
|
173
|
+
secondOffset = latestOffset;
|
|
174
|
+
latestTick = tick;
|
|
175
|
+
latestOffset = offset;
|
|
176
|
+
} else if (tick > secondTick) {
|
|
177
|
+
secondTick = tick;
|
|
178
|
+
secondOffset = offset;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return varBufs.length > 1 && secondTick >= 0 ? secondOffset : latestOffset;
|
|
182
|
+
}
|
|
183
|
+
liveLatestBufOffset() {
|
|
184
|
+
const varBufs = this.header.varBuf;
|
|
185
|
+
if (this.testMode || !this.sharedMemView) {
|
|
186
|
+
return varBufs.reduce((a, b) => a.tickCount > b.tickCount ? a : b).bufOffset;
|
|
187
|
+
}
|
|
188
|
+
let latestTick = -1;
|
|
189
|
+
let latestOffset = varBufs[0].bufOffset;
|
|
190
|
+
for (let i = 0;i < varBufs.length; i++) {
|
|
191
|
+
const tick = this.liveHeaderInt(48 + i * 16);
|
|
192
|
+
if (tick > latestTick) {
|
|
193
|
+
latestTick = tick;
|
|
194
|
+
latestOffset = varBufs[i].bufOffset;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return latestOffset;
|
|
198
|
+
}
|
|
130
199
|
get isConnected() {
|
|
131
200
|
if (!this.header)
|
|
132
201
|
return false;
|
|
133
|
-
if (this.
|
|
202
|
+
if (this.liveStatus === StatusField.STATUS_CONNECTED) {
|
|
134
203
|
this.workaroundConnectedState = 0;
|
|
135
204
|
}
|
|
136
|
-
if (this.workaroundConnectedState === 0 && this.
|
|
205
|
+
if (this.workaroundConnectedState === 0 && this.liveStatus !== StatusField.STATUS_CONNECTED) {
|
|
137
206
|
this.workaroundConnectedState = 1;
|
|
138
207
|
}
|
|
139
208
|
if (this.workaroundConnectedState === 1 && this.get("SessionNum") === null) {
|
|
@@ -142,10 +211,10 @@ class IRSDK {
|
|
|
142
211
|
if (this.workaroundConnectedState === 2 && this.get("SessionNum") !== null) {
|
|
143
212
|
this.workaroundConnectedState = 3;
|
|
144
213
|
}
|
|
145
|
-
return (this.dataValidEvent !== null || this.testMode) && (this.
|
|
214
|
+
return (this.dataValidEvent !== null || this.testMode) && (this.liveStatus === StatusField.STATUS_CONNECTED || this.workaroundConnectedState === 3);
|
|
146
215
|
}
|
|
147
216
|
get sessionInfoUpdate() {
|
|
148
|
-
return this.header?.sessionInfoUpdate ?? 0;
|
|
217
|
+
return this.liveHeaderInt(12, this.header?.sessionInfoUpdate ?? 0);
|
|
149
218
|
}
|
|
150
219
|
get varHeaderNames() {
|
|
151
220
|
if (!this.varHeaders)
|
|
@@ -249,27 +318,119 @@ class IRSDK {
|
|
|
249
318
|
if (!varHeader) {
|
|
250
319
|
return this.getSessionInfo(key);
|
|
251
320
|
}
|
|
252
|
-
const
|
|
321
|
+
const isFrozen = this.isFrozen;
|
|
322
|
+
const view = isFrozen ? this.frozenView : this.sharedMemView;
|
|
253
323
|
if (!view)
|
|
254
324
|
return null;
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
325
|
+
const bufOffset = isFrozen ? this.frozenBufOffset : this.liveReadBufOffset();
|
|
326
|
+
return readVarValue(view, bufOffset + varHeader.offset, varHeader.type, varHeader.count);
|
|
327
|
+
}
|
|
328
|
+
getMultiple(keys) {
|
|
329
|
+
const results = new Map;
|
|
330
|
+
if (!this.header || !this.varHeaders)
|
|
331
|
+
return results;
|
|
332
|
+
const isFrozen = this.isFrozen;
|
|
333
|
+
const view = isFrozen ? this.frozenView : this.sharedMemView;
|
|
334
|
+
if (!view)
|
|
335
|
+
return results;
|
|
336
|
+
const bufOffset = isFrozen ? this.frozenBufOffset : this.liveReadBufOffset();
|
|
337
|
+
for (let i = 0;i < keys.length; i++) {
|
|
338
|
+
const key = keys[i];
|
|
339
|
+
const varHeader = this.varHeaders.get(key);
|
|
340
|
+
if (varHeader) {
|
|
341
|
+
results.set(key, readVarValue(view, bufOffset + varHeader.offset, varHeader.type, varHeader.count));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return results;
|
|
345
|
+
}
|
|
346
|
+
getMultipleInto(keys, out) {
|
|
347
|
+
if (!this.header || !this.varHeaders) {
|
|
348
|
+
for (let i = 0;i < keys.length; i++) {
|
|
349
|
+
out[keys[i]] = null;
|
|
350
|
+
}
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
const isFrozen = this.isFrozen;
|
|
354
|
+
const view = isFrozen ? this.frozenView : this.sharedMemView;
|
|
355
|
+
if (!view)
|
|
356
|
+
return out;
|
|
357
|
+
const bufOffset = isFrozen ? this.frozenBufOffset : this.liveReadBufOffset();
|
|
358
|
+
for (let i = 0;i < keys.length; i++) {
|
|
359
|
+
const key = keys[i];
|
|
360
|
+
const varHeader = this.varHeaders.get(key);
|
|
361
|
+
if (varHeader) {
|
|
362
|
+
out[key] = readVarValue(view, bufOffset + varHeader.offset, varHeader.type, varHeader.count);
|
|
363
|
+
} else {
|
|
364
|
+
out[key] = null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
createReader(keys) {
|
|
370
|
+
if (!this.header || !this.varHeaders) {
|
|
371
|
+
return {
|
|
372
|
+
read: () => keys.map(() => null),
|
|
373
|
+
keys
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const varInfos = new Array(keys.length);
|
|
377
|
+
for (let i = 0;i < keys.length; i++) {
|
|
378
|
+
const varHeader = this.varHeaders.get(keys[i]);
|
|
379
|
+
if (varHeader) {
|
|
380
|
+
varInfos[i] = { offset: varHeader.offset, type: varHeader.type, count: varHeader.count };
|
|
381
|
+
} else {
|
|
382
|
+
varInfos[i] = null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const self = this;
|
|
386
|
+
const results = new Array(keys.length);
|
|
387
|
+
return {
|
|
388
|
+
keys,
|
|
389
|
+
read() {
|
|
390
|
+
if (!self.header) {
|
|
391
|
+
for (let i = 0;i < keys.length; i++)
|
|
392
|
+
results[i] = null;
|
|
393
|
+
return results;
|
|
394
|
+
}
|
|
395
|
+
const isFrozen = self.isFrozen;
|
|
396
|
+
const view = isFrozen ? self.frozenView : self.sharedMemView;
|
|
397
|
+
if (!view) {
|
|
398
|
+
for (let i = 0;i < keys.length; i++)
|
|
399
|
+
results[i] = null;
|
|
400
|
+
return results;
|
|
401
|
+
}
|
|
402
|
+
const bufOffset = isFrozen ? self.frozenBufOffset : self.liveReadBufOffset();
|
|
403
|
+
for (let i = 0;i < varInfos.length; i++) {
|
|
404
|
+
const info = varInfos[i];
|
|
405
|
+
if (info) {
|
|
406
|
+
results[i] = readVarValue(view, bufOffset + info.offset, info.type, info.count);
|
|
407
|
+
} else {
|
|
408
|
+
results[i] = null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return results;
|
|
412
|
+
}
|
|
413
|
+
};
|
|
258
414
|
}
|
|
259
415
|
freezeVarBufferLatest() {
|
|
260
|
-
this.unfreezeVarBufferLatest();
|
|
261
416
|
if (!this.header || !this.sharedMemBuffer)
|
|
262
417
|
return;
|
|
263
418
|
this.waitValidDataEvent();
|
|
264
|
-
const
|
|
419
|
+
const latestBufOffset = this.liveLatestBufOffset();
|
|
420
|
+
const bufLen = this.header.bufLen;
|
|
421
|
+
if (!this.frozenBuffer || this.frozenBufferCapacity < bufLen) {
|
|
422
|
+
this.frozenBuffer = new Uint8Array(bufLen);
|
|
423
|
+
this.frozenBufferCapacity = bufLen;
|
|
424
|
+
this.frozenView = new DataView(this.frozenBuffer.buffer);
|
|
425
|
+
}
|
|
426
|
+
this.frozenBuffer.set(new Uint8Array(this.sharedMemBuffer.buffer, latestBufOffset, bufLen));
|
|
265
427
|
this.frozenBufOffset = 0;
|
|
266
|
-
this.frozenBuffer = this.sharedMemBuffer.slice(varBuf.bufOffset, varBuf.bufOffset + this.header.bufLen);
|
|
267
|
-
this.frozenView = new DataView(this.frozenBuffer.buffer);
|
|
268
428
|
}
|
|
269
429
|
unfreezeVarBufferLatest() {
|
|
270
|
-
this.
|
|
271
|
-
|
|
272
|
-
|
|
430
|
+
this.frozenBufOffset = -1;
|
|
431
|
+
}
|
|
432
|
+
get isFrozen() {
|
|
433
|
+
return this.frozenBufOffset >= 0 && this.frozenView !== null;
|
|
273
434
|
}
|
|
274
435
|
async checkSimStatus() {
|
|
275
436
|
try {
|
|
@@ -288,11 +449,23 @@ class IRSDK {
|
|
|
288
449
|
}
|
|
289
450
|
return true;
|
|
290
451
|
}
|
|
452
|
+
static getSectionRegex(key) {
|
|
453
|
+
let regex = IRSDK.sessionRegexCache.get(key);
|
|
454
|
+
if (!regex) {
|
|
455
|
+
regex = new RegExp(`
|
|
456
|
+
${key}:
|
|
457
|
+
([\\s\\S]*?)(?=
|
|
458
|
+
|
|
459
|
+
|$)`);
|
|
460
|
+
IRSDK.sessionRegexCache.set(key, regex);
|
|
461
|
+
}
|
|
462
|
+
return regex;
|
|
463
|
+
}
|
|
291
464
|
getSessionInfo(key) {
|
|
292
465
|
if (!this.header || !this.sharedMemBuffer)
|
|
293
466
|
return null;
|
|
294
|
-
if (this.lastSessionInfoUpdate
|
|
295
|
-
this.lastSessionInfoUpdate = this.
|
|
467
|
+
if (this.lastSessionInfoUpdate !== this.sessionInfoUpdate) {
|
|
468
|
+
this.lastSessionInfoUpdate = this.sessionInfoUpdate;
|
|
296
469
|
for (const [k, v] of this.sessionInfoCache) {
|
|
297
470
|
if (v.data) {
|
|
298
471
|
this.sessionInfoCache.set(k, { ...v, data: null });
|
|
@@ -303,20 +476,16 @@ class IRSDK {
|
|
|
303
476
|
if (cached?.data) {
|
|
304
477
|
return cached.data;
|
|
305
478
|
}
|
|
306
|
-
const start = this.header.sessionInfoOffset;
|
|
307
|
-
const len = this.header.sessionInfoLen;
|
|
479
|
+
const start = this.liveHeaderInt(20, this.header.sessionInfoOffset);
|
|
480
|
+
const len = this.liveHeaderInt(16, this.header.sessionInfoLen);
|
|
308
481
|
const raw = readString(this.sharedMemBuffer, start, len);
|
|
309
|
-
const sectionMatch = raw.match(
|
|
310
|
-
${key}:
|
|
311
|
-
([\\s\\S]*?)(?=
|
|
312
|
-
|
|
313
|
-
|$)`));
|
|
482
|
+
const sectionMatch = raw.match(IRSDK.getSectionRegex(key));
|
|
314
483
|
if (!sectionMatch)
|
|
315
484
|
return null;
|
|
316
485
|
try {
|
|
317
486
|
const sectionYaml = `${key}:
|
|
318
487
|
${sectionMatch[1]}`;
|
|
319
|
-
const cleaned =
|
|
488
|
+
const cleaned = cleanYamlFast(sectionYaml);
|
|
320
489
|
const parsed = $parse(cleaned);
|
|
321
490
|
const result = parsed?.[key] ?? null;
|
|
322
491
|
this.sessionInfoCache.set(key, { data: result, update: this.lastSessionInfoUpdate });
|
|
@@ -325,15 +494,6 @@ ${sectionMatch[1]}`;
|
|
|
325
494
|
return null;
|
|
326
495
|
}
|
|
327
496
|
}
|
|
328
|
-
cleanYaml(yaml) {
|
|
329
|
-
let cleaned = yaml.replace(/[\x81\x8D\x8F\x90\x9D]/g, " ").replace(/[^\x09\x0A\x0D\x20-\x7E\xA0-\xFF]/g, "");
|
|
330
|
-
cleaned = cleaned.replace(/((?:DriverSetupName|UserName|TeamName|AbbrevName|Initials): )(.*)/g, (_, prefix, value) => {
|
|
331
|
-
const escaped = value.replace(/["\\]/g, "\\$&");
|
|
332
|
-
return `${prefix}"${escaped}"`;
|
|
333
|
-
});
|
|
334
|
-
cleaned = cleaned.replace(/(\w+: )(,.*)/g, '$1"$2"');
|
|
335
|
-
return cleaned;
|
|
336
|
-
}
|
|
337
497
|
getBroadcastMsgId() {
|
|
338
498
|
if (!user32)
|
|
339
499
|
return 0;
|
package/dist/lap-points.d.ts
CHANGED
|
@@ -33,6 +33,12 @@ export interface LapPoint {
|
|
|
33
33
|
lat: number | null;
|
|
34
34
|
lon: number | null;
|
|
35
35
|
altitudeM: number | null;
|
|
36
|
+
/** Lateral acceleration in m/s² (iRacing LatAccel). Added in 1.4.0. */
|
|
37
|
+
latAccelMs2: number | null;
|
|
38
|
+
/** Longitudinal acceleration in m/s² (iRacing LongAccel). Added in 1.4.0. */
|
|
39
|
+
longAccelMs2: number | null;
|
|
40
|
+
/** Yaw rate in deg/s (iRacing YawRate rad/s → deg/s). Added in 1.4.0. */
|
|
41
|
+
yawRateDegS: number | null;
|
|
36
42
|
tireTempLF: number | null;
|
|
37
43
|
tireTempRF: number | null;
|
|
38
44
|
tireTempLR: number | null;
|
package/dist/lap-points.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lap-points.d.ts","sourceRoot":"","sources":["../src/lap-points.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAM5D,mEAAmE;AACnE,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,2DAA2D;IAC3D,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,wDAAwD;IACxD,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,kEAAkE;IAClE,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA8CD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,aAAa,EAClB,IAAI,CAAC,EAAE,uBAAuB,GAC7B,sBAAsB,
|
|
1
|
+
{"version":3,"file":"lap-points.d.ts","sourceRoot":"","sources":["../src/lap-points.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAM5D,mEAAmE;AACnE,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,cAAc,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,uEAAuE;IACvE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,6EAA6E;IAC7E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,yEAAyE;IACzE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,sBAAsB;IACrC,2DAA2D;IAC3D,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,wDAAwD;IACxD,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,kEAAkE;IAClE,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,uBAAuB;IACtC,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA8CD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EACjB,GAAG,EAAE,aAAa,EAClB,IAAI,CAAC,EAAE,uBAAuB,GAC7B,sBAAsB,CAqJxB"}
|
package/dist/live.d.ts
CHANGED
|
@@ -13,15 +13,23 @@ export declare class IRSDK {
|
|
|
13
13
|
private varHeaders;
|
|
14
14
|
private varHeadersNames;
|
|
15
15
|
private sessionInfoCache;
|
|
16
|
+
private static readonly sessionRegexCache;
|
|
16
17
|
private broadcastMsgId;
|
|
17
18
|
private lastSessionInfoUpdate;
|
|
18
19
|
private workaroundConnectedState;
|
|
19
20
|
private frozenBuffer;
|
|
20
21
|
private frozenView;
|
|
21
22
|
private frozenBufOffset;
|
|
23
|
+
private frozenBufferCapacity;
|
|
22
24
|
private testMode;
|
|
23
25
|
isInitialized: boolean;
|
|
24
26
|
constructor(options?: IRSDKOptions);
|
|
27
|
+
private liveHeaderInt;
|
|
28
|
+
private get liveStatus();
|
|
29
|
+
/** Tear-safe read target: the second-latest buffer by LIVE tick count. */
|
|
30
|
+
private liveReadBufOffset;
|
|
31
|
+
/** Freeze target: the newest buffer by LIVE tick count. */
|
|
32
|
+
private liveLatestBufOffset;
|
|
25
33
|
get isConnected(): boolean;
|
|
26
34
|
get sessionInfoUpdate(): number;
|
|
27
35
|
get varHeaderNames(): string[] | null;
|
|
@@ -29,12 +37,37 @@ export declare class IRSDK {
|
|
|
29
37
|
private startupFromFile;
|
|
30
38
|
shutdown(): void;
|
|
31
39
|
get(key: string): TelemetryValue | null;
|
|
40
|
+
/**
|
|
41
|
+
* Batch read multiple telemetry variables in a single pass.
|
|
42
|
+
* Much more efficient than calling get() multiple times.
|
|
43
|
+
* @param keys Array of variable names to read
|
|
44
|
+
* @returns Map of variable names to their values
|
|
45
|
+
*/
|
|
46
|
+
getMultiple(keys: string[]): Map<string, TelemetryValue>;
|
|
47
|
+
/**
|
|
48
|
+
* Batch read multiple telemetry variables into a pre-allocated object.
|
|
49
|
+
* Zero allocation in the hot path - fastest option for real-time telemetry.
|
|
50
|
+
* @param keys Array of variable names to read
|
|
51
|
+
* @param out Pre-allocated object to write values into
|
|
52
|
+
*/
|
|
53
|
+
getMultipleInto<T extends Record<string, TelemetryValue | null>>(keys: string[], out: T): T;
|
|
54
|
+
/**
|
|
55
|
+
* Creates a pre-compiled reader for maximum performance in hot loops.
|
|
56
|
+
* Pre-computes all offsets at creation time, avoiding Map lookups during reads.
|
|
57
|
+
* @param keys Array of variable names to read
|
|
58
|
+
* @returns A reader object with a read() method that returns values as an array
|
|
59
|
+
*/
|
|
60
|
+
createReader(keys: string[]): {
|
|
61
|
+
read: () => (TelemetryValue | null)[];
|
|
62
|
+
keys: string[];
|
|
63
|
+
};
|
|
32
64
|
freezeVarBufferLatest(): void;
|
|
33
65
|
unfreezeVarBufferLatest(): void;
|
|
66
|
+
private get isFrozen();
|
|
34
67
|
private checkSimStatus;
|
|
35
68
|
private waitValidDataEvent;
|
|
69
|
+
private static getSectionRegex;
|
|
36
70
|
getSessionInfo<T = unknown>(key: string): T | null;
|
|
37
|
-
private cleanYaml;
|
|
38
71
|
private getBroadcastMsgId;
|
|
39
72
|
private broadcastMsg;
|
|
40
73
|
private padCarNum;
|
package/dist/live.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAqB,cAAc,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"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"}
|
package/dist/live.js
CHANGED
|
@@ -51,6 +51,9 @@ function extractLapPoints(source, lap, opts) {
|
|
|
51
51
|
const latArr = channel(source, "Lat");
|
|
52
52
|
const lonArr = channel(source, "Lon");
|
|
53
53
|
const altArr = channel(source, "Alt");
|
|
54
|
+
const latAccelArr = channel(source, "LatAccel");
|
|
55
|
+
const longAccelArr = channel(source, "LongAccel");
|
|
56
|
+
const yawRateArr = channel(source, "YawRate");
|
|
54
57
|
const tempLF = channelWithFallback(source, "LFtempM", "LFtempCM");
|
|
55
58
|
const tempRF = channelWithFallback(source, "RFtempM", "RFtempCM");
|
|
56
59
|
const tempLR = channelWithFallback(source, "LRtempM", "LRtempCM");
|
|
@@ -105,6 +108,7 @@ function extractLapPoints(source, lap, opts) {
|
|
|
105
108
|
const throttle = readNum(throttleArr, i);
|
|
106
109
|
const brake = readNum(brakeArr, i);
|
|
107
110
|
const steerRad = readNum(steerArr, i);
|
|
111
|
+
const yawRateRad = readNum(yawRateArr, i);
|
|
108
112
|
const wLF = readNum(wearLF, i);
|
|
109
113
|
const wRF = readNum(wearRF, i);
|
|
110
114
|
const wLR = readNum(wearLR, i);
|
|
@@ -122,6 +126,9 @@ function extractLapPoints(source, lap, opts) {
|
|
|
122
126
|
lat: readNum(latArr, i),
|
|
123
127
|
lon: readNum(lonArr, i),
|
|
124
128
|
altitudeM: readNum(altArr, i),
|
|
129
|
+
latAccelMs2: readNum(latAccelArr, i),
|
|
130
|
+
longAccelMs2: readNum(longAccelArr, i),
|
|
131
|
+
yawRateDegS: yawRateRad != null ? yawRateRad * 180 / Math.PI : null,
|
|
125
132
|
tireTempLF: readNum(tempLF, i),
|
|
126
133
|
tireTempRF: readNum(tempRF, i),
|
|
127
134
|
tireTempLR: readNum(tempLR, i),
|
|
@@ -15,6 +15,10 @@ export interface SessionInfoMessage {
|
|
|
15
15
|
sessionType: string;
|
|
16
16
|
sessionNum: number;
|
|
17
17
|
tickRate: number;
|
|
18
|
+
sectors: Array<{
|
|
19
|
+
sectorNum: number;
|
|
20
|
+
startPct: number;
|
|
21
|
+
}>;
|
|
18
22
|
};
|
|
19
23
|
}
|
|
20
24
|
export interface TelemetryMessage {
|
|
@@ -58,6 +62,24 @@ export interface TelemetryFrame {
|
|
|
58
62
|
tireTempRF: number;
|
|
59
63
|
tireTempLR: number;
|
|
60
64
|
tireTempRR: number;
|
|
65
|
+
tirePressureLF: number | null;
|
|
66
|
+
tirePressureRF: number | null;
|
|
67
|
+
tirePressureLR: number | null;
|
|
68
|
+
tirePressureRR: number | null;
|
|
69
|
+
tireWearLF: number | null;
|
|
70
|
+
tireWearRF: number | null;
|
|
71
|
+
tireWearLR: number | null;
|
|
72
|
+
tireWearRR: number | null;
|
|
73
|
+
sessionNum: number;
|
|
74
|
+
onPitRoad: boolean;
|
|
75
|
+
playerCarIdx: number;
|
|
76
|
+
/** All cars' lap-distance pct at this tick (4dp) — head-to-head sector
|
|
77
|
+
* timing needs the full 60Hz stream, not the 15Hz field snapshot. */
|
|
78
|
+
carIdxLapDistPct: number[];
|
|
79
|
+
/** irsdk_Flags bitfield (green/yellow/blue/white/checkered...). */
|
|
80
|
+
sessionFlags: number;
|
|
81
|
+
/** irsdk_CarLeftRight spotter enum: 1 clear, 2 left, 3 right, 4 both, 5 two left, 6 two right. */
|
|
82
|
+
carLeftRight: number;
|
|
61
83
|
}
|
|
62
84
|
export interface LapChangeMessage {
|
|
63
85
|
type: 'lap_change';
|
|
@@ -76,5 +98,82 @@ export interface StatusMessage {
|
|
|
76
98
|
error?: string;
|
|
77
99
|
};
|
|
78
100
|
}
|
|
79
|
-
export
|
|
101
|
+
export interface FieldMessage {
|
|
102
|
+
type: 'field';
|
|
103
|
+
data: FieldData;
|
|
104
|
+
}
|
|
105
|
+
/** Live multi-car snapshot, published at ~15Hz. Parallel arrays indexed by CarIdx. */
|
|
106
|
+
export interface FieldData {
|
|
107
|
+
tick: number;
|
|
108
|
+
sessionTime: number;
|
|
109
|
+
sessionNum: number;
|
|
110
|
+
sessionState: number;
|
|
111
|
+
sessionTimeRemain: number;
|
|
112
|
+
sessionLapsRemain: number;
|
|
113
|
+
playerCarIdx: number;
|
|
114
|
+
paceCarIdx: number;
|
|
115
|
+
position: number[];
|
|
116
|
+
classPosition: number[];
|
|
117
|
+
lap: number[];
|
|
118
|
+
lapCompleted: number[];
|
|
119
|
+
lapDistPct: number[];
|
|
120
|
+
estTime: number[];
|
|
121
|
+
f2Time: number[];
|
|
122
|
+
onPitRoad: boolean[];
|
|
123
|
+
trackSurface: number[];
|
|
124
|
+
lastLapTime: number[];
|
|
125
|
+
bestLapTime: number[];
|
|
126
|
+
}
|
|
127
|
+
export interface OverlayDriver {
|
|
128
|
+
carIdx: number;
|
|
129
|
+
userName: string;
|
|
130
|
+
teamName?: string;
|
|
131
|
+
carNumber: string;
|
|
132
|
+
carClassId: number;
|
|
133
|
+
carClassShortName: string;
|
|
134
|
+
carClassColor: number;
|
|
135
|
+
carClassRelSpeed: number;
|
|
136
|
+
carClassEstLapTime: number;
|
|
137
|
+
iRating: number;
|
|
138
|
+
licString: string;
|
|
139
|
+
licColor: number;
|
|
140
|
+
incidentCount: number;
|
|
141
|
+
}
|
|
142
|
+
export interface ClassInfo {
|
|
143
|
+
classId: number;
|
|
144
|
+
shortName: string;
|
|
145
|
+
color: number;
|
|
146
|
+
sof: number;
|
|
147
|
+
carCount: number;
|
|
148
|
+
}
|
|
149
|
+
export interface SessionStateMessage {
|
|
150
|
+
type: 'session_state';
|
|
151
|
+
data: SessionStateData;
|
|
152
|
+
}
|
|
153
|
+
/** Roster + results snapshot, published when iRacing bumps its session-info counter. */
|
|
154
|
+
export interface SessionStateData {
|
|
155
|
+
sessionInfoUpdate: number;
|
|
156
|
+
subSessionId: number;
|
|
157
|
+
trackId: number;
|
|
158
|
+
sessionNum: number;
|
|
159
|
+
sessionType: string;
|
|
160
|
+
sessionLaps: string | number;
|
|
161
|
+
sessionTimeTotal: string | number;
|
|
162
|
+
drivers: OverlayDriver[];
|
|
163
|
+
classes: ClassInfo[];
|
|
164
|
+
results: Array<{
|
|
165
|
+
carIdx: number;
|
|
166
|
+
position: number;
|
|
167
|
+
classPosition: number;
|
|
168
|
+
fastestTime: number;
|
|
169
|
+
lastTime: number;
|
|
170
|
+
lapsComplete: number;
|
|
171
|
+
incidents: number;
|
|
172
|
+
}>;
|
|
173
|
+
fastestLap: Array<{
|
|
174
|
+
carIdx: number;
|
|
175
|
+
fastestTime: number;
|
|
176
|
+
}>;
|
|
177
|
+
}
|
|
178
|
+
export type ServerMessage = SessionInfoMessage | TelemetryMessage | LapChangeMessage | StatusMessage | FieldMessage | SessionStateMessage;
|
|
80
179
|
//# sourceMappingURL=telemetry-server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry-server.d.ts","sourceRoot":"","sources":["../src/telemetry-server.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"telemetry-server.d.ts","sourceRoot":"","sources":["../src/telemetry-server.ts"],"names":[],"mappings":"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"}
|
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);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotlap.ai/telemetry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "High-performance iRacing telemetry library for Bun - IBT file parsing and live data streaming",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -63,10 +63,12 @@
|
|
|
63
63
|
"build:neutral": "bun build ./src/ibt.ts ./src/normalize.ts ./src/session-yaml.ts ./src/lap-points.ts --outdir ./dist/neutral --target browser --splitting",
|
|
64
64
|
"build:types": "tsc --emitDeclarationOnly --declaration --declarationMap --outDir ./dist",
|
|
65
65
|
"build:server": "bun build ./src/telemetry-server.ts --compile --outfile ./dist/hotlap-telemetry",
|
|
66
|
+
"build:sidecar": "bun run build:server && bun run scripts/copy-sidecar.ts",
|
|
66
67
|
"clean": "rm -rf dist",
|
|
67
68
|
"dev": "bun --watch src/cli.ts",
|
|
68
69
|
"dashboard": "bun run examples/dashboard-server.ts",
|
|
69
70
|
"server": "bun run src/telemetry-server.ts",
|
|
71
|
+
"mock:overlays": "bun run scripts/mock-overlay-server.ts",
|
|
70
72
|
"typecheck": "tsc --noEmit",
|
|
71
73
|
"test": "bun test",
|
|
72
74
|
"fixtures:pull": "bun run scripts/fixtures-pull.ts",
|