@hotlap.ai/telemetry 1.2.0 → 1.3.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/dist/lap-points.d.ts +70 -0
- package/dist/lap-points.d.ts.map +1 -0
- package/dist/neutral/lap-points.js +149 -0
- package/package.json +7 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical lap point extraction — produces the per-sample telemetry series
|
|
3
|
+
* (byDist / byTime) that the UI charts and the backend lap_bundles store.
|
|
4
|
+
*
|
|
5
|
+
* Output shape matches the deployed IbtLapPoint contract exactly
|
|
6
|
+
* (UI/src/lib/ibt-extract.ts and BACKEND process-job convertToIbtLapData):
|
|
7
|
+
* - Speed m/s → km/h, Throttle/Brake 0–1 → 0–100 %, SteeringWheelAngle rad → deg
|
|
8
|
+
* - distanceKm normalized per lap: (LapDist − min LapDist) / 1000
|
|
9
|
+
* - timeSec normalized per lap: SessionTime − min SessionTime
|
|
10
|
+
* - tire temps LFtempM.. with LFtempCM.. carcass fallback (process-job behavior)
|
|
11
|
+
*
|
|
12
|
+
* CANONICAL DECISION — tire wear is percent 0–100 (×100 from the raw 0–1
|
|
13
|
+
* channel), matching process-job and therefore every stored lap bundle. The
|
|
14
|
+
* UI's ibt-extract.ts left wear at raw 0–1; that was the divergent one.
|
|
15
|
+
*
|
|
16
|
+
* RFC-0001 C8: extraction runs at FULL resolution; downsampling to maxPoints
|
|
17
|
+
* happens last and is presentation-only — never an input to metrics.
|
|
18
|
+
*
|
|
19
|
+
* RUNTIME NEUTRALITY: this module must not import Bun/Deno/Node APIs.
|
|
20
|
+
*/
|
|
21
|
+
import type { IbtSource, NormalizedLap } from './normalize';
|
|
22
|
+
/** Matches the UI/backend IbtLapPoint contract field-for-field. */
|
|
23
|
+
export interface LapPoint {
|
|
24
|
+
distanceKm: number;
|
|
25
|
+
timeSec: number;
|
|
26
|
+
speedKmh: number | null;
|
|
27
|
+
throttlePct: number | null;
|
|
28
|
+
brakePct: number | null;
|
|
29
|
+
brakeABSactive: boolean | null;
|
|
30
|
+
gear: number | null;
|
|
31
|
+
rpm: number | null;
|
|
32
|
+
steeringDeg: number | null;
|
|
33
|
+
lat: number | null;
|
|
34
|
+
lon: number | null;
|
|
35
|
+
altitudeM: number | null;
|
|
36
|
+
tireTempLF: number | null;
|
|
37
|
+
tireTempRF: number | null;
|
|
38
|
+
tireTempLR: number | null;
|
|
39
|
+
tireTempRR: number | null;
|
|
40
|
+
tirePressureLF: number | null;
|
|
41
|
+
tirePressureRF: number | null;
|
|
42
|
+
tirePressureLR: number | null;
|
|
43
|
+
tirePressureRR: number | null;
|
|
44
|
+
tireWearLF: number | null;
|
|
45
|
+
tireWearRF: number | null;
|
|
46
|
+
tireWearLR: number | null;
|
|
47
|
+
tireWearRR: number | null;
|
|
48
|
+
}
|
|
49
|
+
export interface ExtractLapPointsResult {
|
|
50
|
+
/** Points sorted ascending by distanceKm (downsampled). */
|
|
51
|
+
byDist: LapPoint[];
|
|
52
|
+
/** Points sorted ascending by timeSec (downsampled). */
|
|
53
|
+
byTime: LapPoint[];
|
|
54
|
+
/** Number of valid full-resolution points before downsampling. */
|
|
55
|
+
downsampledFrom: number;
|
|
56
|
+
}
|
|
57
|
+
export interface ExtractLapPointsOptions {
|
|
58
|
+
/** Maximum points per series after downsampling. Default 500. */
|
|
59
|
+
maxPoints?: number;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Extract the telemetry point series for one normalized lap.
|
|
63
|
+
*
|
|
64
|
+
* Iterates lap.firstSampleIndex..lastSampleIndex; when the Lap / SessionNum
|
|
65
|
+
* channels are available, only indices whose Lap equals lap.lapNumber (and
|
|
66
|
+
* whose SessionNum matches the lap's own session, read at firstSampleIndex)
|
|
67
|
+
* are honored. Without those channels the whole contiguous range is accepted.
|
|
68
|
+
*/
|
|
69
|
+
export declare function extractLapPoints(source: IbtSource, lap: NormalizedLap, opts?: ExtractLapPointsOptions): ExtractLapPointsResult;
|
|
70
|
+
//# sourceMappingURL=lap-points.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// src/lap-points.ts
|
|
2
|
+
var DEFAULT_MAX_POINTS = 500;
|
|
3
|
+
function channel(source, key) {
|
|
4
|
+
const typed = source.getAllTyped(key);
|
|
5
|
+
if (typed)
|
|
6
|
+
return typed;
|
|
7
|
+
return source.getAll(key);
|
|
8
|
+
}
|
|
9
|
+
function channelWithFallback(source, ...keys) {
|
|
10
|
+
for (const key of keys) {
|
|
11
|
+
const c = channel(source, key);
|
|
12
|
+
if (c)
|
|
13
|
+
return c;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
function readNum(arr, i) {
|
|
18
|
+
if (!arr)
|
|
19
|
+
return null;
|
|
20
|
+
const v = arr[i];
|
|
21
|
+
return typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
22
|
+
}
|
|
23
|
+
function readBool(arr, i) {
|
|
24
|
+
if (!arr)
|
|
25
|
+
return null;
|
|
26
|
+
const v = arr[i];
|
|
27
|
+
if (typeof v === "boolean")
|
|
28
|
+
return v;
|
|
29
|
+
if (v === 1)
|
|
30
|
+
return true;
|
|
31
|
+
if (v === 0)
|
|
32
|
+
return false;
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function extractLapPoints(source, lap, opts) {
|
|
36
|
+
const maxPoints = Math.max(1, Math.floor(opts?.maxPoints ?? DEFAULT_MAX_POINTS));
|
|
37
|
+
const timeArr = channel(source, "SessionTime");
|
|
38
|
+
const distArr = channel(source, "LapDist");
|
|
39
|
+
if (!timeArr || !distArr || source.recordCount === 0) {
|
|
40
|
+
return { byDist: [], byTime: [], downsampledFrom: 0 };
|
|
41
|
+
}
|
|
42
|
+
const lapArr = channel(source, "Lap");
|
|
43
|
+
const sessArr = channel(source, "SessionNum");
|
|
44
|
+
const speedArr = channel(source, "Speed");
|
|
45
|
+
const throttleArr = channel(source, "Throttle");
|
|
46
|
+
const brakeArr = channel(source, "Brake");
|
|
47
|
+
const absArr = channel(source, "BrakeABSactive");
|
|
48
|
+
const gearArr = channel(source, "Gear");
|
|
49
|
+
const rpmArr = channel(source, "RPM");
|
|
50
|
+
const steerArr = channel(source, "SteeringWheelAngle");
|
|
51
|
+
const latArr = channel(source, "Lat");
|
|
52
|
+
const lonArr = channel(source, "Lon");
|
|
53
|
+
const altArr = channel(source, "Alt");
|
|
54
|
+
const tempLF = channelWithFallback(source, "LFtempM", "LFtempCM");
|
|
55
|
+
const tempRF = channelWithFallback(source, "RFtempM", "RFtempCM");
|
|
56
|
+
const tempLR = channelWithFallback(source, "LRtempM", "LRtempCM");
|
|
57
|
+
const tempRR = channelWithFallback(source, "RRtempM", "RRtempCM");
|
|
58
|
+
const pressLF = channel(source, "LFpressure");
|
|
59
|
+
const pressRF = channel(source, "RFpressure");
|
|
60
|
+
const pressLR = channel(source, "LRpressure");
|
|
61
|
+
const pressRR = channel(source, "RRpressure");
|
|
62
|
+
const wearLF = channel(source, "LFwearM");
|
|
63
|
+
const wearRF = channel(source, "RFwearM");
|
|
64
|
+
const wearLR = channel(source, "LRwearM");
|
|
65
|
+
const wearRR = channel(source, "RRwearM");
|
|
66
|
+
const first = Math.max(0, lap.firstSampleIndex);
|
|
67
|
+
const last = Math.min(source.recordCount - 1, lap.lastSampleIndex);
|
|
68
|
+
const expectedSess = readNum(sessArr, first);
|
|
69
|
+
const indices = [];
|
|
70
|
+
for (let i = first;i <= last; i++) {
|
|
71
|
+
if (lapArr) {
|
|
72
|
+
const l = readNum(lapArr, i);
|
|
73
|
+
if (l != null && l !== lap.lapNumber)
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (sessArr && expectedSess != null) {
|
|
77
|
+
const s = readNum(sessArr, i);
|
|
78
|
+
if (s != null && s !== expectedSess)
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
indices.push(i);
|
|
82
|
+
}
|
|
83
|
+
let minTime = Infinity;
|
|
84
|
+
let minDist = Infinity;
|
|
85
|
+
for (const i of indices) {
|
|
86
|
+
const t = readNum(timeArr, i);
|
|
87
|
+
const d = readNum(distArr, i);
|
|
88
|
+
if (t == null || d == null)
|
|
89
|
+
continue;
|
|
90
|
+
if (t < minTime)
|
|
91
|
+
minTime = t;
|
|
92
|
+
if (d < minDist)
|
|
93
|
+
minDist = d;
|
|
94
|
+
}
|
|
95
|
+
if (!Number.isFinite(minTime) || !Number.isFinite(minDist)) {
|
|
96
|
+
return { byDist: [], byTime: [], downsampledFrom: 0 };
|
|
97
|
+
}
|
|
98
|
+
const points = [];
|
|
99
|
+
for (const i of indices) {
|
|
100
|
+
const t = readNum(timeArr, i);
|
|
101
|
+
const d = readNum(distArr, i);
|
|
102
|
+
if (t == null || d == null)
|
|
103
|
+
continue;
|
|
104
|
+
const speed = readNum(speedArr, i);
|
|
105
|
+
const throttle = readNum(throttleArr, i);
|
|
106
|
+
const brake = readNum(brakeArr, i);
|
|
107
|
+
const steerRad = readNum(steerArr, i);
|
|
108
|
+
const wLF = readNum(wearLF, i);
|
|
109
|
+
const wRF = readNum(wearRF, i);
|
|
110
|
+
const wLR = readNum(wearLR, i);
|
|
111
|
+
const wRR = readNum(wearRR, i);
|
|
112
|
+
points.push({
|
|
113
|
+
distanceKm: (d - minDist) / 1000,
|
|
114
|
+
timeSec: t - minTime,
|
|
115
|
+
speedKmh: speed != null ? speed * 3.6 : null,
|
|
116
|
+
throttlePct: throttle != null ? throttle * 100 : null,
|
|
117
|
+
brakePct: brake != null ? brake * 100 : null,
|
|
118
|
+
brakeABSactive: readBool(absArr, i),
|
|
119
|
+
gear: readNum(gearArr, i),
|
|
120
|
+
rpm: readNum(rpmArr, i),
|
|
121
|
+
steeringDeg: steerRad != null ? steerRad * 180 / Math.PI : null,
|
|
122
|
+
lat: readNum(latArr, i),
|
|
123
|
+
lon: readNum(lonArr, i),
|
|
124
|
+
altitudeM: readNum(altArr, i),
|
|
125
|
+
tireTempLF: readNum(tempLF, i),
|
|
126
|
+
tireTempRF: readNum(tempRF, i),
|
|
127
|
+
tireTempLR: readNum(tempLR, i),
|
|
128
|
+
tireTempRR: readNum(tempRR, i),
|
|
129
|
+
tirePressureLF: readNum(pressLF, i),
|
|
130
|
+
tirePressureRF: readNum(pressRF, i),
|
|
131
|
+
tirePressureLR: readNum(pressLR, i),
|
|
132
|
+
tirePressureRR: readNum(pressRR, i),
|
|
133
|
+
tireWearLF: wLF != null ? wLF * 100 : null,
|
|
134
|
+
tireWearRF: wRF != null ? wRF * 100 : null,
|
|
135
|
+
tireWearLR: wLR != null ? wLR * 100 : null,
|
|
136
|
+
tireWearRR: wRR != null ? wRR * 100 : null
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const valid = points.filter((p) => Number.isFinite(p.distanceKm) && Number.isFinite(p.timeSec) && p.distanceKm >= 0 && p.timeSec >= 0);
|
|
140
|
+
const byDistFull = [...valid].sort((a, b) => a.distanceKm - b.distanceKm);
|
|
141
|
+
const byTimeFull = [...valid].sort((a, b) => a.timeSec - b.timeSec);
|
|
142
|
+
const stride = Math.max(1, Math.ceil(valid.length / maxPoints));
|
|
143
|
+
const byDist = stride > 1 ? byDistFull.filter((_, i) => i % stride === 0) : byDistFull;
|
|
144
|
+
const byTime = stride > 1 ? byTimeFull.filter((_, i) => i % stride === 0) : byTimeFull;
|
|
145
|
+
return { byDist, byTime, downsampledFrom: valid.length };
|
|
146
|
+
}
|
|
147
|
+
export {
|
|
148
|
+
extractLapPoints
|
|
149
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotlap.ai/telemetry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
"import": "./dist/neutral/session-yaml.js",
|
|
33
33
|
"default": "./dist/neutral/session-yaml.js"
|
|
34
34
|
},
|
|
35
|
+
"./points": {
|
|
36
|
+
"types": "./dist/lap-points.d.ts",
|
|
37
|
+
"import": "./dist/neutral/lap-points.js",
|
|
38
|
+
"default": "./dist/neutral/lap-points.js"
|
|
39
|
+
},
|
|
35
40
|
"./live": {
|
|
36
41
|
"types": "./dist/live.d.ts",
|
|
37
42
|
"import": "./dist/live.js",
|
|
@@ -55,7 +60,7 @@
|
|
|
55
60
|
"scripts": {
|
|
56
61
|
"build": "bun run build:js && bun run build:types",
|
|
57
62
|
"build:js": "bun build ./src/index.ts ./src/ibt.ts ./src/live.ts ./src/constants.ts ./src/cli.ts ./src/telemetry-server.ts ./src/types.ts ./src/binary-reader.ts --outdir ./dist --target bun --splitting && bun run build:neutral",
|
|
58
|
-
"build:neutral": "bun build ./src/ibt.ts ./src/normalize.ts ./src/session-yaml.ts --outdir ./dist/neutral --target browser --splitting",
|
|
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",
|
|
59
64
|
"build:types": "tsc --emitDeclarationOnly --declaration --declarationMap --outDir ./dist",
|
|
60
65
|
"build:server": "bun build ./src/telemetry-server.ts --compile --outfile ./dist/hotlap-telemetry",
|
|
61
66
|
"clean": "rm -rf dist",
|