@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 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 SDK!
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 SDK constants and enums
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
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue.svg)](https://www.typescriptlang.org/)
8
8
  [![Bun](https://img.shields.io/badge/Bun-%3E%3D1.0.0-black.svg)](https://bun.sh/)
9
9
 
10
- High-performance iRacing telemetry SDK for [Bun](https://bun.sh). Parse IBT telemetry files and stream live data from iRacing via Windows shared memory.
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 SDK documentation for the complete list of variables.
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-t4qz2011.js";
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
- Bun iRacing SDK v${VERSION}
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(`Bun iRacing SDK v${VERSION}`);
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.header.status === StatusField.STATUS_CONNECTED) {
202
+ if (this.liveStatus === StatusField.STATUS_CONNECTED) {
134
203
  this.workaroundConnectedState = 0;
135
204
  }
136
- if (this.workaroundConnectedState === 0 && this.header.status !== StatusField.STATUS_CONNECTED) {
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.header.status === StatusField.STATUS_CONNECTED || this.workaroundConnectedState === 3);
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 view = this.frozenView ?? this.sharedMemView;
321
+ const isFrozen = this.isFrozen;
322
+ const view = isFrozen ? this.frozenView : this.sharedMemView;
253
323
  if (!view)
254
324
  return null;
255
- const varBuf = this.frozenView ? { bufOffset: this.frozenBufOffset } : getLatestVarBuffer(this.header.varBuf);
256
- const offset = varBuf.bufOffset + varHeader.offset;
257
- return readVarValue(view, offset, varHeader.type, varHeader.count);
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 varBuf = this.header.varBuf.reduce((a, b) => a.tickCount > b.tickCount ? a : b);
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.frozenBuffer = null;
271
- this.frozenView = null;
272
- this.frozenBufOffset = 0;
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 < this.header.sessionInfoUpdate) {
295
- this.lastSessionInfoUpdate = this.header.sessionInfoUpdate;
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(new RegExp(`
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 = this.cleanYaml(sectionYaml);
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;
@@ -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;
@@ -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,CA8IxB"}
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;
@@ -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;AAuGjE,MAAM,WAAW,YAAY;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,qBAAa,KAAK;IAChB,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,YAAY,CAAiB;IACrC,OAAO,CAAC,eAAe,CAA2B;IAClD,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,iBAAiB,CAAiB;IAC1C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,UAAU,CAAuC;IACzD,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,gBAAgB,CAA6D;IACrF,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,wBAAwB,CAAK;IACrC,OAAO,CAAC,YAAY,CAA2B;IAC/C,OAAO,CAAC,UAAU,CAAyB;IAC3C,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,QAAQ,CAAS;IACzB,aAAa,UAAS;gBAEV,OAAO,GAAE,YAAiB;IAItC,IAAI,WAAW,IAAI,OAAO,CAoBzB;IAED,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAED,IAAI,cAAc,IAAI,MAAM,EAAE,GAAG,IAAI,CAMpC;IAEK,OAAO,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;YAkErD,eAAe;IAqB7B,QAAQ,IAAI,IAAI;IAiChB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI;IAiBvC,qBAAqB,IAAI,IAAI;IAa7B,uBAAuB,IAAI,IAAI;YAMjB,cAAc;IAU5B,OAAO,CAAC,kBAAkB;IAQ1B,cAAc,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI;IAoClD,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,iBAAiB;IAUzB,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,SAAS;IAcjB,YAAY,CAAC,QAAQ,SAAI,EAAE,KAAK,SAAI,EAAE,MAAM,SAAI,GAAG,OAAO;IAI1D,YAAY,CAAC,SAAS,GAAE,MAAM,GAAG,MAAY,EAAE,KAAK,SAAI,EAAE,MAAM,SAAI,GAAG,OAAO;IAI9E,WAAW,CAAC,WAAW,IAA8B,GAAG,OAAO;IAI/D,kBAAkB,CAAC,KAAK,SAAI,EAAE,UAAU,UAAQ,GAAG,OAAO;IAI1D,qBAAqB,CAAC,OAAO,IAAmB,EAAE,QAAQ,SAAI,GAAG,OAAO;IAIxE,YAAY,CAAC,UAAU,IAAuB,GAAG,OAAO;IAIxD,cAAc,CAAC,SAAS,IAA0B,GAAG,OAAO;IAI5D,iBAAiB,IAAI,OAAO;IAI5B,aAAa,CAAC,MAAM,SAAI,GAAG,OAAO;IAIlC,WAAW,CAAC,eAAe,IAA6B,GAAG,OAAO;IAIlE,gBAAgB,CAAC,QAAQ,SAAI,GAAG,OAAO;IAIvC,UAAU,CAAC,cAAc,IAAuB,EAAE,QAAQ,SAAI,GAAG,OAAO;IAIxE,YAAY,CAAC,gBAAgB,IAAwB,GAAG,OAAO;IAI/D,UAAU,CAAC,cAAc,IAAuC,EAAE,KAAK,SAAI,GAAG,OAAO;IAIrF,uBAAuB,CAAC,UAAU,SAAI,EAAE,aAAa,SAAI,GAAG,OAAO;IAInE,YAAY,CAAC,gBAAgB,IAAuC,GAAG,OAAO;CAG/E;AAED,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,YAAY,GAAG,KAAK,CAEzD"}
1
+ {"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"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"}
package/dist/live.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  IRSDK,
4
4
  createIRSDK
5
- } from "./index-t4qz2011.js";
5
+ } from "./index-ndbp3f5t.js";
6
6
  import"./index-hxw291vy.js";
7
7
  import"./index-c5jf6rjt.js";
8
8
  import"./index-wb6ebd0j.js";
@@ -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 type ServerMessage = SessionInfoMessage | TelemetryMessage | LapChangeMessage | StatusMessage;
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":";AASA,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,cAAc,CAAA;IACpB,IAAI,EAAE;QACJ,OAAO,EAAE,MAAM,CAAA;QACf,SAAS,EAAE,MAAM,CAAA;QACjB,gBAAgB,EAAE,MAAM,CAAA;QACxB,WAAW,EAAE,MAAM,CAAA;QACnB,WAAW,EAAE,MAAM,CAAA;QACnB,KAAK,EAAE,MAAM,CAAA;QACb,OAAO,EAAE,MAAM,CAAA;QACf,SAAS,EAAE,MAAM,CAAA;QACjB,UAAU,EAAE,MAAM,CAAA;QAClB,QAAQ,EAAE,MAAM,CAAA;QAChB,WAAW,EAAE,MAAM,CAAA;QACnB,UAAU,EAAE,MAAM,CAAA;QAClB,QAAQ,EAAE,MAAM,CAAA;KACjB,CAAA;CACF;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,CAAA;IACjB,IAAI,EAAE,cAAc,CAAA;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,iBAAiB,EAAE,MAAM,CAAA;IACzB,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,OAAO,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,cAAc,EAAE,OAAO,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,YAAY,CAAA;IAClB,IAAI,EAAE;QACJ,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,OAAO,EAAE,MAAM,CAAA;QACf,UAAU,EAAE,MAAM,CAAA;KACnB,CAAA;CACF;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,QAAQ,CAAA;IACd,IAAI,EAAE;QACJ,SAAS,EAAE,OAAO,CAAA;QAClB,WAAW,EAAE,OAAO,CAAA;QACpB,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;CACF;AAED,MAAM,MAAM,aAAa,GAAG,kBAAkB,GAAG,gBAAgB,GAAG,gBAAgB,GAAG,aAAa,CAAA"}
1
+ {"version":3,"file":"telemetry-server.d.ts","sourceRoot":"","sources":["../src/telemetry-server.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
  import {
4
4
  createIRSDK
5
- } from "./index-t4qz2011.js";
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 sessionInfoSent = false;
20
- async function connectToIRacing() {
21
- console.log("Connecting to iRacing...");
22
- const connected = await ir.startup();
23
- if (!connected) {
24
- console.error("Failed to connect to iRacing. Make sure iRacing is running.");
25
- return false;
26
- }
27
- console.log("Connected to iRacing!");
28
- return true;
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.freezeVarBufferLatest();
144
+ ir.getMultipleInto(telemetryKeys, telemetryData);
55
145
  const frame = {
56
146
  tick: tickCount++,
57
- sessionTime: ir.get("SessionTime") ?? 0,
58
- lap: ir.get("Lap") ?? 0,
59
- lapDistPct: ir.get("LapDistPct") ?? 0,
60
- speed: ir.get("Speed") ?? 0,
61
- rpm: ir.get("RPM") ?? 0,
62
- gear: ir.get("Gear") ?? 0,
63
- throttle: ir.get("Throttle") ?? 0,
64
- brake: ir.get("Brake") ?? 0,
65
- clutch: ir.get("Clutch") ?? 0,
66
- steer: ir.get("SteeringWheelAngle") ?? 0,
67
- lat: ir.get("Lat") ?? 0,
68
- lon: ir.get("Lon") ?? 0,
69
- alt: ir.get("Alt") ?? 0,
70
- yaw: ir.get("Yaw") ?? 0,
71
- pitch: ir.get("Pitch") ?? 0,
72
- roll: ir.get("Roll") ?? 0,
73
- lapCurrentLapTime: ir.get("LapCurrentLapTime") ?? 0,
74
- lapLastLapTime: ir.get("LapLastLapTime") ?? 0,
75
- lapBestLapTime: ir.get("LapBestLapTime") ?? 0,
76
- lapDeltaToBestLap: ir.get("LapDeltaToBestLap") ?? 0,
77
- fuelLevel: ir.get("FuelLevel") ?? 0,
78
- fuelLevelPct: ir.get("FuelLevelPct") ?? 0,
79
- oilTemp: ir.get("OilTemp") ?? 0,
80
- waterTemp: ir.get("WaterTemp") ?? 0,
81
- isOnTrack: ir.get("IsOnTrack") ?? false,
82
- playerPosition: ir.get("PlayerCarPosition") ?? 0,
83
- playerClassPosition: ir.get("PlayerCarClassPosition") ?? 0,
84
- brakeABSactive: ir.get("BrakeABSactive") ?? false,
85
- dcBrakeBias: ir.get("dcBrakeBias") ?? 0,
86
- dcTractionControl: ir.get("dcTractionControl") ?? 0,
87
- dcABS: ir.get("dcABS") ?? 0,
88
- tireTempLF: ir.get("LFtempCL") ?? 0,
89
- tireTempRF: ir.get("RFtempCL") ?? 0,
90
- tireTempLR: ir.get("LRtempCL") ?? 0,
91
- tireTempRR: ir.get("RRtempCL") ?? 0
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
- sessionInfoSent = false;
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
- const lapChangeMsg = {
107
- type: "lap_change",
108
- data: {
109
- fromLap: lastLap,
110
- toLap: currentLap,
111
- lapTime: frame.lapLastLapTime,
112
- sessionNum: currentSessionNum
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 connected = await connectToIRacing();
129
- if (!connected) {
130
- process.exit(1);
131
- }
132
- const sessionInfo = getSessionInfo();
133
- if (sessionInfo) {
134
- console.log(`Track: ${sessionInfo.trackDisplayName}`);
135
- console.log(`Driver: ${sessionInfo.driverName}`);
136
- console.log(`Car: ${sessionInfo.carName} #${sessionInfo.carNumber}`);
137
- }
138
- const server = Bun.serve({
139
- port: TELEMETRY_PORT,
140
- fetch(req, server2) {
141
- const url = new URL(req.url);
142
- if (url.pathname === "/ws" || req.headers.get("upgrade") === "websocket") {
143
- const success = server2.upgrade(req);
144
- if (success) {
145
- return;
146
- }
147
- return new Response("WebSocket upgrade failed", { status: 400 });
148
- }
149
- if (url.pathname === "/status") {
150
- return Response.json({
151
- connected: ir.isConnected,
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
- ws.send(JSON.stringify(statusMsg));
173
- const sessionInfo2 = getSessionInfo();
174
- if (sessionInfo2) {
175
- const sessionMsg = {
176
- type: "session_info",
177
- data: sessionInfo2
178
- };
179
- ws.send(JSON.stringify(sessionMsg));
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
- close(ws) {
183
- console.log("Client disconnected");
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 connections...
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
- if (!ir.isConnected) {
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 sessionInfo2 = getSessionInfo();
209
- if (sessionInfo2 && !sessionInfoSent) {
210
- const sessionMsg = {
211
- type: "session_info",
212
- data: sessionInfo2
213
- };
214
- server.publish("telemetry", JSON.stringify(sessionMsg));
215
- sessionInfoSent = true;
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.0",
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",