@ziztechnology/dial-library 0.0.10 → 0.0.12

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/index.mjs CHANGED
@@ -45,40 +45,59 @@ const dialSdkDebugWarn = (moduleName, scope, phase, data) => {
45
45
  //#endregion
46
46
  //#region src/sensor/check_status.ts
47
47
  const DEBUG_MODULE$5 = "check_status.ts";
48
+ const DEVICE_RIGHT = {
49
+ x: 1,
50
+ y: 0,
51
+ z: 0
52
+ };
53
+ const ZERO_VECTOR$1 = {
54
+ x: 0,
55
+ y: 0,
56
+ z: 0
57
+ };
48
58
  const DEFAULT_DRIVING_STATUS_TUNING = Object.freeze({
49
- version: "driving-status-v1",
59
+ version: "driving-status-calibrated-2026-07",
50
60
  maxSampleAgeMs: 1e3,
51
- rapidLongitudinalThreshold: 3,
52
- longitudinalThreshold: 1,
53
- turnYawThreshold: .5,
54
- turnLateralThreshold: 2.5,
61
+ gravityFilterTimeConstantMs: 1e3,
62
+ biasFilterTimeConstantMs: 1e4,
63
+ vehicleRightMinimumNorm: .2,
55
64
  motionWindowMs: 1e3,
56
65
  motionMinimumSamples: 5,
57
- drivingMotionThreshold: .4,
58
- stoppedMotionThreshold: .25,
59
- stoppedGyroThreshold: .1,
60
- hysteresisRatio: .75,
61
- smoothingWindowSize: 3,
62
- confirmationSamples: Object.freeze({
63
- STOPPED: 5,
64
- STEADY_DRIVING: 3,
65
- ACCELERATION: 2,
66
- RAPID_ACCELERATION: 1,
67
- BRAKING: 2,
68
- SUDDEN_BRAKING: 1,
69
- LEFT_TURN: 2,
70
- RIGHT_TURN: 2
71
- }),
72
- minimumDisplayMs: Object.freeze({
73
- STOPPED: 1200,
74
- STEADY_DRIVING: 1200,
75
- ACCELERATION: 700,
76
- RAPID_ACCELERATION: 1200,
77
- BRAKING: 700,
78
- SUDDEN_BRAKING: 1200,
79
- LEFT_TURN: 800,
80
- RIGHT_TURN: 800
81
- })
66
+ stoppedMotionThreshold: .12,
67
+ drivingMotionThreshold: .18,
68
+ stoppedGyroThreshold: .025,
69
+ stoppedRawGyroThreshold: .065,
70
+ drivingGyroThreshold: .04,
71
+ drivingRawGyroThreshold: .08,
72
+ stoppedConfirmationMs: 5e3,
73
+ drivingConfirmationMs: 600,
74
+ longitudinalWindowMs: 800,
75
+ longitudinalMinimumSamples: 5,
76
+ longitudinalThreshold: .7,
77
+ longitudinalExitThreshold: .35,
78
+ longitudinalSignCoherence: .8,
79
+ longitudinalReleaseMs: 500,
80
+ rapidLongitudinalWindowMs: 600,
81
+ rapidLongitudinalMinimumSamples: 4,
82
+ rapidLongitudinalPeakThreshold: 2.2,
83
+ rapidLongitudinalMeanThreshold: 1.4,
84
+ rapidLongitudinalSignCoherence: .75,
85
+ rapidMinimumDisplayMs: 500,
86
+ verticalBumpRatio: 1.25,
87
+ longitudinalReversalWindowMs: 400,
88
+ turnWindowMs: 1500,
89
+ turnMinimumSamples: 4,
90
+ turnYawThreshold: .12,
91
+ turnConfirmationMs: 600,
92
+ turnIntegratedAngleThreshold: .12,
93
+ turnSignCoherence: .7,
94
+ turnYawDominanceThreshold: .6,
95
+ turnLateralEvidenceThreshold: .25,
96
+ turnVerticalNoiseFloor: .1,
97
+ turnVerticalBumpRatio: 1.25,
98
+ turnExitYawThreshold: .04,
99
+ turnExitConfirmationMs: 700,
100
+ turnClosureWindowMs: 1500
82
101
  });
83
102
  const classifyDrivingSnapshot = (info, tuningOverrides) => {
84
103
  dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingSnapshot", "called", {
@@ -87,52 +106,95 @@ const classifyDrivingSnapshot = (info, tuningOverrides) => {
87
106
  });
88
107
  const tuning = resolveDrivingStatusTuning(tuningOverrides);
89
108
  const metrics = extractDrivingSnapshotMetrics(info, tuning);
90
- const result = metrics === null ? null : classifyDrivingMetrics(metrics, tuning);
91
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingSnapshot", "returned", {
92
- tuning,
109
+ if (metrics === null) {
110
+ dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingSnapshot", "returned", { result: null });
111
+ return null;
112
+ }
113
+ const { candidateStatus, rejectionReason } = classifyInstantaneousEvidence(metrics, tuning);
114
+ const result = {
115
+ capturedAtMs: info.capturedAtMs,
93
116
  metrics,
94
- result
95
- });
117
+ candidateStatus,
118
+ rejectionReason,
119
+ quality: {
120
+ frameValid: true,
121
+ maximumSampleAgeMs: Math.max(info.capturedAtMs - metrics.linearAccelerationSampledAtMs, info.capturedAtMs - metrics.gyroscopeSampledAtMs, info.capturedAtMs - metrics.gravitySampledAtMs)
122
+ }
123
+ };
124
+ dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingSnapshot", "returned", { result });
96
125
  return result;
97
126
  };
98
- const checkRunningStatus = classifyDrivingSnapshot;
99
127
  const resolveDrivingStatusTuning = (overrides = {}) => {
100
128
  dialSdkDebugWarn(DEBUG_MODULE$5, "resolveDrivingStatusTuning", "called", { overrides });
101
129
  const tuning = {
102
130
  ...DEFAULT_DRIVING_STATUS_TUNING,
103
- ...overrides,
104
- confirmationSamples: {
105
- ...DEFAULT_DRIVING_STATUS_TUNING.confirmationSamples,
106
- ...overrides.confirmationSamples
107
- },
108
- minimumDisplayMs: {
109
- ...DEFAULT_DRIVING_STATUS_TUNING.minimumDisplayMs,
110
- ...overrides.minimumDisplayMs
111
- }
131
+ ...overrides
112
132
  };
113
- assertPositiveFinite("maxSampleAgeMs", tuning.maxSampleAgeMs);
114
- assertPositiveFinite("rapidLongitudinalThreshold", tuning.rapidLongitudinalThreshold);
115
- assertPositiveFinite("longitudinalThreshold", tuning.longitudinalThreshold);
116
- assertPositiveFinite("turnYawThreshold", tuning.turnYawThreshold);
117
- assertPositiveFinite("turnLateralThreshold", tuning.turnLateralThreshold);
118
- assertPositiveFinite("motionWindowMs", tuning.motionWindowMs);
119
- if (!Number.isInteger(tuning.motionMinimumSamples) || tuning.motionMinimumSamples < 1) throw new RangeError("motionMinimumSamples must be a positive integer");
120
- assertPositiveFinite("drivingMotionThreshold", tuning.drivingMotionThreshold);
121
- assertPositiveFinite("stoppedMotionThreshold", tuning.stoppedMotionThreshold);
122
- assertPositiveFinite("stoppedGyroThreshold", tuning.stoppedGyroThreshold);
123
- if (tuning.drivingMotionThreshold <= tuning.stoppedMotionThreshold) throw new RangeError("drivingMotionThreshold must be greater than stoppedMotionThreshold");
124
- if (!Number.isInteger(tuning.smoothingWindowSize) || tuning.smoothingWindowSize < 1) throw new RangeError("smoothingWindowSize must be a positive integer");
125
- if (tuning.rapidLongitudinalThreshold < tuning.longitudinalThreshold) throw new RangeError("rapidLongitudinalThreshold must be greater than or equal to longitudinalThreshold");
126
- if (!Number.isFinite(tuning.hysteresisRatio) || tuning.hysteresisRatio <= 0 || tuning.hysteresisRatio >= 1) throw new RangeError("hysteresisRatio must be greater than 0 and less than 1");
127
- for (const [status, value] of Object.entries(tuning.confirmationSamples)) if (!Number.isInteger(value) || value < 1) throw new RangeError(`confirmationSamples.${status} must be a positive integer`);
128
- for (const [status, value] of Object.entries(tuning.minimumDisplayMs)) if (!Number.isFinite(value) || value < 0) throw new RangeError(`minimumDisplayMs.${status} must be a non-negative finite number`);
133
+ for (const field of [
134
+ "maxSampleAgeMs",
135
+ "gravityFilterTimeConstantMs",
136
+ "biasFilterTimeConstantMs",
137
+ "vehicleRightMinimumNorm",
138
+ "motionWindowMs",
139
+ "stoppedMotionThreshold",
140
+ "drivingMotionThreshold",
141
+ "stoppedGyroThreshold",
142
+ "stoppedRawGyroThreshold",
143
+ "drivingGyroThreshold",
144
+ "drivingRawGyroThreshold",
145
+ "stoppedConfirmationMs",
146
+ "drivingConfirmationMs",
147
+ "longitudinalWindowMs",
148
+ "longitudinalThreshold",
149
+ "longitudinalExitThreshold",
150
+ "longitudinalReleaseMs",
151
+ "rapidLongitudinalWindowMs",
152
+ "rapidLongitudinalPeakThreshold",
153
+ "rapidLongitudinalMeanThreshold",
154
+ "rapidMinimumDisplayMs",
155
+ "verticalBumpRatio",
156
+ "longitudinalReversalWindowMs",
157
+ "turnWindowMs",
158
+ "turnYawThreshold",
159
+ "turnConfirmationMs",
160
+ "turnIntegratedAngleThreshold",
161
+ "turnLateralEvidenceThreshold",
162
+ "turnVerticalNoiseFloor",
163
+ "turnVerticalBumpRatio",
164
+ "turnExitYawThreshold",
165
+ "turnExitConfirmationMs",
166
+ "turnClosureWindowMs"
167
+ ]) assertPositiveFinite(field, tuning[field]);
168
+ for (const field of [
169
+ "motionMinimumSamples",
170
+ "longitudinalMinimumSamples",
171
+ "rapidLongitudinalMinimumSamples",
172
+ "turnMinimumSamples"
173
+ ]) if (!Number.isInteger(tuning[field]) || tuning[field] < 1) throwLoggedRangeError("resolveDrivingStatusTuning", `${field} must be a positive integer`);
174
+ for (const field of [
175
+ "longitudinalSignCoherence",
176
+ "rapidLongitudinalSignCoherence",
177
+ "turnSignCoherence",
178
+ "turnYawDominanceThreshold"
179
+ ]) {
180
+ const value = tuning[field];
181
+ if (!Number.isFinite(value) || value <= .5 || value > 1) throwLoggedRangeError("resolveDrivingStatusTuning", `${field} must be greater than 0.5 and less than or equal to 1`);
182
+ }
183
+ if (tuning.drivingMotionThreshold <= tuning.stoppedMotionThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "drivingMotionThreshold must be greater than stoppedMotionThreshold");
184
+ if (tuning.drivingGyroThreshold <= tuning.stoppedGyroThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "drivingGyroThreshold must be greater than stoppedGyroThreshold");
185
+ if (tuning.drivingRawGyroThreshold <= tuning.stoppedRawGyroThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "drivingRawGyroThreshold must be greater than stoppedRawGyroThreshold");
186
+ if (tuning.longitudinalThreshold <= tuning.longitudinalExitThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "longitudinalThreshold must be greater than longitudinalExitThreshold");
187
+ if (tuning.rapidLongitudinalMeanThreshold <= tuning.longitudinalThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "rapidLongitudinalMeanThreshold must be greater than longitudinalThreshold");
188
+ if (tuning.rapidLongitudinalPeakThreshold < tuning.rapidLongitudinalMeanThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "rapidLongitudinalPeakThreshold must be greater than or equal to rapidLongitudinalMeanThreshold");
189
+ if (tuning.turnYawThreshold <= tuning.turnExitYawThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "turnYawThreshold must be greater than turnExitYawThreshold");
129
190
  dialSdkDebugWarn(DEBUG_MODULE$5, "resolveDrivingStatusTuning", "returned", { tuning });
130
191
  return tuning;
131
192
  };
132
- const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUNING) => {
193
+ const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUNING, correction = {}) => {
133
194
  dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "called", {
134
195
  info,
135
- tuning
196
+ tuning,
197
+ correction
136
198
  });
137
199
  if (!info || !Number.isFinite(info.capturedAtMs)) {
138
200
  dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned invalid capture time", { info });
@@ -140,149 +202,164 @@ const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUN
140
202
  }
141
203
  const linearAcceleration = availableFreshVector(info.linearAcceleration, info.capturedAtMs, tuning.maxSampleAgeMs);
142
204
  const gyroscope = availableFreshVector(info.gyroscope, info.capturedAtMs, tuning.maxSampleAgeMs);
143
- if (linearAcceleration === null || gyroscope === null) {
205
+ const gravityMetric = availableFreshVector(info.gravity, info.capturedAtMs, tuning.maxSampleAgeMs);
206
+ const gravity = availableCorrectedGravity(correction, info.capturedAtMs, tuning.maxSampleAgeMs) ?? (correction.filteredGravity !== void 0 && gravityMetric !== null ? {
207
+ value: correction.filteredGravity,
208
+ sampledAtMs: gravityMetric.sampledAtMs
209
+ } : gravityMetric);
210
+ if (linearAcceleration === null || gyroscope === null || gravity === null) {
144
211
  dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned unavailable vectors", {
145
212
  linearAcceleration,
146
- gyroscope
213
+ gyroscope,
214
+ gravity
147
215
  });
148
216
  return null;
149
217
  }
150
- const longitudinal = -linearAcceleration.z;
151
- const lateral = linearAcceleration.x;
152
- const yaw = gyroscope.y;
153
- const motionIntensity = info.motionIntensity === void 0 ? vectorMagnitude(linearAcceleration) : availableFreshNumber(info.motionIntensity, info.capturedAtMs, tuning.maxSampleAgeMs);
154
- if (motionIntensity === null) {
155
- dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned unavailable motion intensity", { motionIntensity: info.motionIntensity });
218
+ const frame = buildVehicleFrame(gravity.value, tuning.vehicleRightMinimumNorm);
219
+ if (frame === null) {
220
+ dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned invalid vehicle frame", {
221
+ gravity: gravity.value,
222
+ vehicleRightMinimumNorm: tuning.vehicleRightMinimumNorm
223
+ });
156
224
  return null;
157
225
  }
226
+ const correctedLinear = subtract(linearAcceleration.value, correction.linearAccelerationBias ?? ZERO_VECTOR$1);
227
+ const correctedGyroscope = subtract(gyroscope.value, correction.gyroscopeBias ?? ZERO_VECTOR$1);
158
228
  const result = {
159
229
  capturedAtMs: info.capturedAtMs,
160
- longitudinal,
161
- lateral,
162
- yaw,
163
- motionIntensity,
164
- gyroMagnitude: vectorMagnitude(gyroscope)
230
+ linearAccelerationSampledAtMs: linearAcceleration.sampledAtMs,
231
+ gyroscopeSampledAtMs: gyroscope.sampledAtMs,
232
+ gravitySampledAtMs: gravity.sampledAtMs,
233
+ longitudinal: dot(correctedLinear, frame.forward),
234
+ lateral: dot(correctedLinear, frame.right),
235
+ vertical: dot(correctedLinear, frame.up),
236
+ yaw: dot(correctedGyroscope, frame.up),
237
+ gyroMagnitude: magnitude(correctedGyroscope),
238
+ rawGyroMagnitude: magnitude(gyroscope.value),
239
+ rawLinearAcceleration: linearAcceleration.value,
240
+ rawGyroscope: gyroscope.value,
241
+ frame
165
242
  };
166
243
  dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned", { result });
167
244
  return result;
168
245
  };
169
- const classifyDrivingMetrics = (metrics, tuning = DEFAULT_DRIVING_STATUS_TUNING) => {
170
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingMetrics", "called", {
171
- metrics,
172
- tuning
246
+ const buildVehicleFrame = (gravity, rightMinimumNorm = DEFAULT_DRIVING_STATUS_TUNING.vehicleRightMinimumNorm) => {
247
+ dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "called", {
248
+ gravity,
249
+ rightMinimumNorm
173
250
  });
174
- const { longitudinal, lateral, yaw, motionIntensity, gyroMagnitude } = metrics;
175
- if (longitudinal <= -tuning.rapidLongitudinalThreshold) {
176
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingMetrics", "returned", { result: "SUDDEN_BRAKING" });
177
- return "SUDDEN_BRAKING";
178
- }
179
- if (longitudinal >= tuning.rapidLongitudinalThreshold) {
180
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingMetrics", "returned", { result: "RAPID_ACCELERATION" });
181
- return "RAPID_ACCELERATION";
182
- }
183
- if (Math.abs(yaw) >= tuning.turnYawThreshold) {
184
- const result = yaw > 0 ? "LEFT_TURN" : "RIGHT_TURN";
185
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingMetrics", "returned", {
186
- result,
187
- basis: "yaw"
188
- });
189
- return result;
190
- }
191
- if (Math.abs(lateral) >= tuning.turnLateralThreshold) {
192
- const result = lateral < 0 ? "LEFT_TURN" : "RIGHT_TURN";
193
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingMetrics", "returned", {
194
- result,
195
- basis: "lateral"
196
- });
197
- return result;
251
+ const gravityDirection = normalize(gravity);
252
+ if (gravityDirection === null) {
253
+ dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "returned invalid gravity", { result: null });
254
+ return null;
198
255
  }
199
- if (longitudinal <= -tuning.longitudinalThreshold) {
200
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingMetrics", "returned", { result: "BRAKING" });
201
- return "BRAKING";
256
+ const up = gravityDirection;
257
+ const projectedRight = subtract(DEVICE_RIGHT, scale(up, dot(DEVICE_RIGHT, up)));
258
+ if (magnitude(projectedRight) < rightMinimumNorm) {
259
+ dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "returned degenerate right axis", { result: null });
260
+ return null;
202
261
  }
203
- if (longitudinal >= tuning.longitudinalThreshold) {
204
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingMetrics", "returned", { result: "ACCELERATION" });
205
- return "ACCELERATION";
262
+ const right = normalize(projectedRight);
263
+ if (right === null) {
264
+ dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "returned invalid right axis", { result: null });
265
+ return null;
206
266
  }
207
- if (motionIntensity < tuning.drivingMotionThreshold && gyroMagnitude <= tuning.stoppedGyroThreshold) {
208
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingMetrics", "returned", { result: "STOPPED" });
209
- return "STOPPED";
267
+ const forward = normalize(cross(right, up));
268
+ const result = forward === null ? null : {
269
+ up,
270
+ right,
271
+ forward
272
+ };
273
+ dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "returned", { result });
274
+ return result;
275
+ };
276
+ const classifyInstantaneousEvidence = (metrics, tuning) => {
277
+ const longitudinalMagnitude = Math.abs(metrics.longitudinal);
278
+ if (longitudinalMagnitude >= tuning.rapidLongitudinalPeakThreshold) {
279
+ if (Math.abs(metrics.vertical) > longitudinalMagnitude * tuning.verticalBumpRatio) return {
280
+ candidateStatus: null,
281
+ rejectionReason: "vertical_bump"
282
+ };
283
+ return {
284
+ candidateStatus: metrics.longitudinal > 0 ? "RAPID_ACCELERATION" : "SUDDEN_BRAKING",
285
+ rejectionReason: null
286
+ };
210
287
  }
211
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingMetrics", "returned", { result: "STEADY_DRIVING" });
212
- return "STEADY_DRIVING";
288
+ if (Math.abs(metrics.yaw) >= tuning.turnYawThreshold) return {
289
+ candidateStatus: metrics.yaw > 0 ? "LEFT_TURN" : "RIGHT_TURN",
290
+ rejectionReason: null
291
+ };
292
+ if (longitudinalMagnitude >= tuning.longitudinalThreshold) return {
293
+ candidateStatus: metrics.longitudinal > 0 ? "ACCELERATION" : "BRAKING",
294
+ rejectionReason: null
295
+ };
296
+ return {
297
+ candidateStatus: null,
298
+ rejectionReason: null
299
+ };
213
300
  };
214
301
  const availableFreshVector = (metric, capturedAtMs, maxSampleAgeMs) => {
215
- dialSdkDebugWarn(DEBUG_MODULE$5, "availableFreshVector", "called", {
216
- metric,
217
- capturedAtMs,
218
- maxSampleAgeMs
219
- });
220
- if (!metric?.available || !isFresh(metric.sampledAtMs, capturedAtMs, maxSampleAgeMs)) {
221
- dialSdkDebugWarn(DEBUG_MODULE$5, "availableFreshVector", "returned", { result: null });
222
- return null;
223
- }
302
+ if (!metric?.available || !isFresh(metric.sampledAtMs, capturedAtMs, maxSampleAgeMs)) return null;
224
303
  const { x, y, z } = metric.value;
225
- const result = [
304
+ return [
226
305
  x,
227
306
  y,
228
307
  z
229
- ].every(Number.isFinite) ? metric.value : null;
230
- dialSdkDebugWarn(DEBUG_MODULE$5, "availableFreshVector", "returned", { result });
231
- return result;
232
- };
233
- const availableFreshNumber = (metric, capturedAtMs, maxSampleAgeMs) => {
234
- dialSdkDebugWarn(DEBUG_MODULE$5, "availableFreshNumber", "called", {
235
- metric,
236
- capturedAtMs,
237
- maxSampleAgeMs
238
- });
239
- if (!metric?.available || !isFresh(metric.sampledAtMs, capturedAtMs, maxSampleAgeMs)) {
240
- dialSdkDebugWarn(DEBUG_MODULE$5, "availableFreshNumber", "returned", { result: null });
241
- return null;
242
- }
243
- const result = Number.isFinite(metric.value) && metric.value >= 0 ? metric.value : null;
244
- dialSdkDebugWarn(DEBUG_MODULE$5, "availableFreshNumber", "returned", { result });
245
- return result;
308
+ ].every(Number.isFinite) ? {
309
+ value: metric.value,
310
+ sampledAtMs: metric.sampledAtMs
311
+ } : null;
312
+ };
313
+ const availableCorrectedGravity = (correction, capturedAtMs, maxSampleAgeMs) => {
314
+ const { filteredGravity, filteredGravitySampledAtMs } = correction;
315
+ if (filteredGravity === void 0 || filteredGravitySampledAtMs === void 0 || !isFresh(filteredGravitySampledAtMs, capturedAtMs, maxSampleAgeMs)) return null;
316
+ const { x, y, z } = filteredGravity;
317
+ return [
318
+ x,
319
+ y,
320
+ z
321
+ ].every(Number.isFinite) ? {
322
+ value: filteredGravity,
323
+ sampledAtMs: filteredGravitySampledAtMs
324
+ } : null;
246
325
  };
247
326
  const isFresh = (sampledAtMs, capturedAtMs, maxSampleAgeMs) => {
248
- dialSdkDebugWarn(DEBUG_MODULE$5, "isFresh", "called", {
249
- sampledAtMs,
250
- capturedAtMs,
251
- maxSampleAgeMs
252
- });
253
- if (sampledAtMs === null || !Number.isFinite(sampledAtMs)) {
254
- dialSdkDebugWarn(DEBUG_MODULE$5, "isFresh", "returned", { result: false });
255
- return false;
256
- }
327
+ if (sampledAtMs === null || !Number.isFinite(sampledAtMs)) return false;
257
328
  const age = capturedAtMs - sampledAtMs;
258
- const result = age >= 0 && age <= maxSampleAgeMs;
259
- dialSdkDebugWarn(DEBUG_MODULE$5, "isFresh", "returned", {
260
- age,
261
- result
262
- });
263
- return result;
329
+ return age >= 0 && age <= maxSampleAgeMs;
264
330
  };
265
- const vectorMagnitude = ({ x, y, z }) => {
266
- const result = Math.hypot(x, y, z);
267
- dialSdkDebugWarn(DEBUG_MODULE$5, "vectorMagnitude", "returned", {
268
- x,
269
- y,
270
- z,
271
- result
272
- });
273
- return result;
331
+ const dot = (left, right) => left.x * right.x + left.y * right.y + left.z * right.z;
332
+ const cross = (left, right) => ({
333
+ x: left.y * right.z - left.z * right.y,
334
+ y: left.z * right.x - left.x * right.z,
335
+ z: left.x * right.y - left.y * right.x
336
+ });
337
+ const subtract = (left, right) => ({
338
+ x: left.x - right.x,
339
+ y: left.y - right.y,
340
+ z: left.z - right.z
341
+ });
342
+ const scale = (value, factor) => ({
343
+ x: value.x * factor,
344
+ y: value.y * factor,
345
+ z: value.z * factor
346
+ });
347
+ const magnitude = (value) => Math.hypot(value.x, value.y, value.z);
348
+ const normalize = (value) => {
349
+ const length = magnitude(value);
350
+ return Number.isFinite(length) && length > Number.EPSILON ? scale(value, 1 / length) : null;
274
351
  };
275
352
  const assertPositiveFinite = (name, value) => {
276
- dialSdkDebugWarn(DEBUG_MODULE$5, "assertPositiveFinite", "called", {
277
- name,
278
- value
279
- });
280
353
  if (!Number.isFinite(value) || value <= 0) {
281
354
  const error = /* @__PURE__ */ new RangeError(`${name} must be a positive finite number`);
282
355
  dialSdkDebugWarn(DEBUG_MODULE$5, "assertPositiveFinite", "threw", error);
283
356
  throw error;
284
357
  }
285
- dialSdkDebugWarn(DEBUG_MODULE$5, "assertPositiveFinite", "returned");
358
+ };
359
+ const throwLoggedRangeError = (scope, message) => {
360
+ const error = new RangeError(message);
361
+ dialSdkDebugWarn(DEBUG_MODULE$5, scope, "threw", error);
362
+ throw error;
286
363
  };
287
364
  //#endregion
288
365
  //#region src/sensor/unified_info.ts
@@ -1272,6 +1349,11 @@ const safeInvoke = (callback) => {
1272
1349
  //#endregion
1273
1350
  //#region src/sensor/driving_status_controller.ts
1274
1351
  const DEBUG_MODULE$1 = "driving_status_controller.ts";
1352
+ const ZERO_VECTOR = {
1353
+ x: 0,
1354
+ y: 0,
1355
+ z: 0
1356
+ };
1275
1357
  const createDrivingStatusController = (options = {}) => {
1276
1358
  dialSdkDebugWarn(DEBUG_MODULE$1, "createDrivingStatusController", "called", { options });
1277
1359
  const result = new DefaultDrivingStatusController(options);
@@ -1281,14 +1363,42 @@ const createDrivingStatusController = (options = {}) => {
1281
1363
  var DefaultDrivingStatusController = class {
1282
1364
  constructor(options) {
1283
1365
  this.listeners = /* @__PURE__ */ new Set();
1284
- this.metricWindow = [];
1366
+ this.longitudinalWindow = [];
1367
+ this.yawWindow = [];
1285
1368
  this.motionWindow = [];
1286
1369
  this.candidate = null;
1287
- this.candidateConfirmationCount = 0;
1370
+ this.candidateSinceMs = null;
1288
1371
  this.lastAvailableAtMs = null;
1289
1372
  this.unavailableSinceMs = null;
1290
1373
  this.fallbackApplied = false;
1291
- this.heldCandidate = null;
1374
+ this.filteredGravity = null;
1375
+ this.lastGravitySampledAtMs = null;
1376
+ this.linearAccelerationBias = { ...ZERO_VECTOR };
1377
+ this.gyroscopeBias = { ...ZERO_VECTOR };
1378
+ this.biasCalibrated = false;
1379
+ this.lastBiasSampledAtMs = null;
1380
+ this.stoppedConditionSinceMs = null;
1381
+ this.drivingConditionSinceMs = null;
1382
+ this.stationaryConfirmed = false;
1383
+ this.urgentStatus = null;
1384
+ this.urgentStartedAtMs = null;
1385
+ this.normalLongitudinalStatus = null;
1386
+ this.longitudinalClearSinceMs = null;
1387
+ this.turnStatus = null;
1388
+ this.turnQuietSinceMs = null;
1389
+ this.oppositeTurnSinceMs = null;
1390
+ this.detectorCandidates = {
1391
+ base: null,
1392
+ rapid_longitudinal: null,
1393
+ longitudinal: null,
1394
+ turn: null
1395
+ };
1396
+ this.detectorRejections = {
1397
+ base: null,
1398
+ rapid_longitudinal: null,
1399
+ longitudinal: null,
1400
+ turn: null
1401
+ };
1292
1402
  this.requestedRunning = false;
1293
1403
  this.polling = false;
1294
1404
  this.lifecycleSuspended = false;
@@ -1297,12 +1407,12 @@ var DefaultDrivingStatusController = class {
1297
1407
  this.pendingRead = null;
1298
1408
  this.unsubscribePageLifecycle = null;
1299
1409
  this.handleLifecyclePause = () => {
1300
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.handleLifecyclePause", "called", {
1301
- requestedRunning: this.requestedRunning,
1302
- lifecycleSuspended: this.lifecycleSuspended
1303
- });
1410
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.handleLifecyclePause", "called");
1304
1411
  if (!this.requestedRunning || this.lifecycleSuspended) {
1305
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.handleLifecyclePause", "ignored");
1412
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.handleLifecyclePause", "ignored", {
1413
+ requestedRunning: this.requestedRunning,
1414
+ lifecycleSuspended: this.lifecycleSuspended
1415
+ });
1306
1416
  return;
1307
1417
  }
1308
1418
  this.lifecycleSuspended = true;
@@ -1311,13 +1421,15 @@ var DefaultDrivingStatusController = class {
1311
1421
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.handleLifecyclePause", "returned");
1312
1422
  };
1313
1423
  this.handleLifecycleResume = () => {
1314
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.handleLifecycleResume", "called", {
1315
- requestedRunning: this.requestedRunning,
1316
- lifecycleSuspended: this.lifecycleSuspended,
1317
- destroyed: this.destroyed
1318
- });
1319
- if (!this.requestedRunning || !this.lifecycleSuspended || this.destroyed || isDrivingPageHidden()) {
1320
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.handleLifecycleResume", "ignored");
1424
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.handleLifecycleResume", "called");
1425
+ const pageHidden = this.requestedRunning && this.lifecycleSuspended && !this.destroyed ? isDrivingPageHidden() : false;
1426
+ if (!this.requestedRunning || !this.lifecycleSuspended || this.destroyed || pageHidden) {
1427
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.handleLifecycleResume", "ignored", {
1428
+ requestedRunning: this.requestedRunning,
1429
+ lifecycleSuspended: this.lifecycleSuspended,
1430
+ destroyed: this.destroyed,
1431
+ pageHidden
1432
+ });
1321
1433
  return;
1322
1434
  }
1323
1435
  this.lifecycleSuspended = false;
@@ -1333,18 +1445,15 @@ var DefaultDrivingStatusController = class {
1333
1445
  this.onDiagnostic = options.onDiagnostic;
1334
1446
  this.managePageLifecycle = options.managePageLifecycle ?? true;
1335
1447
  const initialStatus = options.initialStatus ?? "STOPPED";
1336
- if (!CAR_RUNNING_STATUSES.includes(initialStatus)) throw new TypeError(`Unsupported initialStatus: ${initialStatus}`);
1448
+ if (!CAR_RUNNING_STATUSES.includes(initialStatus)) {
1449
+ const error = /* @__PURE__ */ new TypeError(`Unsupported initialStatus: ${initialStatus}`);
1450
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.constructor", "threw", error);
1451
+ throw error;
1452
+ }
1337
1453
  this.status = initialStatus;
1338
1454
  this.baseStatus = isBaseStatus(initialStatus) ? initialStatus : "STOPPED";
1339
1455
  this.statusCommittedAtMs = this.clock.now();
1340
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.constructor", "created", {
1341
- initialStatus,
1342
- baseStatus: this.baseStatus,
1343
- pollIntervalMs: this.pollIntervalMs,
1344
- unavailableFallbackMs: this.unavailableFallbackMs,
1345
- tuning: this.tuning,
1346
- managePageLifecycle: this.managePageLifecycle
1347
- });
1456
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.constructor", "created", this.getSnapshot());
1348
1457
  }
1349
1458
  start() {
1350
1459
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.start", "called", this.getSnapshot());
@@ -1389,16 +1498,17 @@ var DefaultDrivingStatusController = class {
1389
1498
  }
1390
1499
  this.destroyed = true;
1391
1500
  this.listeners.clear();
1392
- this.metricWindow.length = 0;
1393
- this.motionWindow.length = 0;
1501
+ this.resetTransientRecognition();
1394
1502
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.destroy", "returned", this.getSnapshot());
1395
1503
  }
1396
1504
  getSnapshot() {
1397
1505
  const result = {
1398
1506
  lifecycle: this.destroyed ? "destroyed" : this.polling ? "running" : "stopped",
1399
1507
  status: this.status,
1508
+ baseStatus: this.baseStatus,
1509
+ activeTurn: this.turnStatus,
1400
1510
  candidate: this.candidate,
1401
- candidateConfirmationCount: this.candidateConfirmationCount,
1511
+ candidateSinceMs: this.candidateSinceMs,
1402
1512
  statusCommittedAtMs: this.statusCommittedAtMs,
1403
1513
  lastAvailableAtMs: this.lastAvailableAtMs,
1404
1514
  unavailableSinceMs: this.unavailableSinceMs,
@@ -1409,16 +1519,16 @@ var DefaultDrivingStatusController = class {
1409
1519
  }
1410
1520
  subscribe(listener) {
1411
1521
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.subscribe", "called", {
1412
- destroyed: this.destroyed,
1413
- listenerCount: this.listeners.size
1522
+ listener,
1523
+ destroyed: this.destroyed
1414
1524
  });
1415
1525
  if (this.destroyed) {
1416
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.subscribe", "returned noop");
1526
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.subscribe", "ignored destroyed controller");
1417
1527
  return noop;
1418
1528
  }
1419
1529
  this.listeners.add(listener);
1530
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.subscribe", "returned", { listenerCount: this.listeners.size });
1420
1531
  let subscribed = true;
1421
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.subscribe", "subscribed", { listenerCount: this.listeners.size });
1422
1532
  return () => {
1423
1533
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.unsubscribe", "called", { subscribed });
1424
1534
  if (!subscribed) {
@@ -1431,15 +1541,14 @@ var DefaultDrivingStatusController = class {
1431
1541
  };
1432
1542
  }
1433
1543
  activatePolling() {
1434
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.activatePolling", "called", {
1435
- destroyed: this.destroyed,
1436
- polling: this.polling,
1437
- requestedRunning: this.requestedRunning,
1438
- lifecycleSuspended: this.lifecycleSuspended,
1439
- generation: this.generation
1440
- });
1544
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.activatePolling", "called");
1441
1545
  if (this.destroyed || this.polling || !this.requestedRunning || this.lifecycleSuspended) {
1442
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.activatePolling", "ignored");
1546
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.activatePolling", "ignored", {
1547
+ destroyed: this.destroyed,
1548
+ polling: this.polling,
1549
+ requestedRunning: this.requestedRunning,
1550
+ lifecycleSuspended: this.lifecycleSuspended
1551
+ });
1443
1552
  return;
1444
1553
  }
1445
1554
  this.polling = true;
@@ -1449,14 +1558,10 @@ var DefaultDrivingStatusController = class {
1449
1558
  }
1450
1559
  deactivatePolling() {
1451
1560
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.deactivatePolling", "called", {
1452
- polling: this.polling,
1453
- timer: this.timer,
1561
+ generation: this.generation,
1562
+ hasTimer: this.timer !== void 0,
1454
1563
  hasPendingRead: this.pendingRead !== null
1455
1564
  });
1456
- if (!this.polling && this.timer === void 0 && this.pendingRead === null) {
1457
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.deactivatePolling", "ignored");
1458
- return;
1459
- }
1460
1565
  this.polling = false;
1461
1566
  this.generation += 1;
1462
1567
  if (this.timer !== void 0) {
@@ -1468,72 +1573,110 @@ var DefaultDrivingStatusController = class {
1468
1573
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.deactivatePolling", "returned", { generation: this.generation });
1469
1574
  }
1470
1575
  schedulePoll(delayMs, generation) {
1471
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.schedulePoll", "called", {
1472
- delayMs,
1473
- generation,
1474
- currentGeneration: this.generation,
1475
- polling: this.polling,
1476
- timer: this.timer
1477
- });
1478
- if (!this.polling || generation !== this.generation || this.timer !== void 0) {
1479
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.schedulePoll", "ignored");
1480
- return;
1481
- }
1576
+ if (!this.polling || generation !== this.generation || this.timer !== void 0) return;
1482
1577
  this.timer = this.clock.setTimeout(() => {
1483
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.schedulePoll.callback", "called", { generation });
1484
1578
  this.timer = void 0;
1485
1579
  this.poll(generation);
1486
1580
  }, delayMs);
1487
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.schedulePoll", "scheduled", {
1488
- delayMs,
1489
- generation,
1490
- timer: this.timer
1491
- });
1492
1581
  }
1493
1582
  async poll(generation) {
1494
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "called", {
1495
- generation,
1496
- currentGeneration: this.generation,
1497
- polling: this.polling,
1498
- hasPendingRead: this.pendingRead !== null
1499
- });
1583
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "called", { generation });
1500
1584
  if (!this.polling || generation !== this.generation || this.pendingRead !== null) {
1501
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "ignored");
1585
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "ignored", {
1586
+ polling: this.polling,
1587
+ generation,
1588
+ currentGeneration: this.generation,
1589
+ hasPendingRead: this.pendingRead !== null
1590
+ });
1502
1591
  return;
1503
1592
  }
1504
1593
  const pendingRead = new AbortController();
1505
1594
  this.pendingRead = pendingRead;
1506
1595
  try {
1507
1596
  const payload = await this.sensorProvider(pendingRead.signal);
1508
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "provider resolved", { payload });
1509
1597
  if (!this.polling || generation !== this.generation || pendingRead.signal.aborted) {
1510
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "discarded provider result");
1598
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "discarded provider result", {
1599
+ generation,
1600
+ currentGeneration: this.generation,
1601
+ polling: this.polling,
1602
+ aborted: pendingRead.signal.aborted
1603
+ });
1511
1604
  return;
1512
1605
  }
1513
- const metrics = extractDrivingSnapshotMetrics(payload, this.tuning);
1514
- const accelerometerSample = extractFreshAccelerometerSample(payload, this.tuning.maxSampleAgeMs);
1515
- if (metrics === null || accelerometerSample === null) this.processUnavailable("invalid_or_stale_snapshot");
1516
- else this.processAvailable(metrics, accelerometerSample);
1606
+ const gravity = this.resolveGravityFrame(payload);
1607
+ const metrics = gravity === null ? null : extractDrivingSnapshotMetrics(payload, this.tuning, {
1608
+ filteredGravity: gravity.value,
1609
+ filteredGravitySampledAtMs: gravity.sampledAtMs,
1610
+ linearAccelerationBias: this.linearAccelerationBias,
1611
+ gyroscopeBias: this.gyroscopeBias
1612
+ });
1613
+ const accelerometerSample = extractFreshVectorMetric(payload.accelerometer, payload.capturedAtMs, this.tuning.maxSampleAgeMs);
1614
+ if (gravity === null || metrics === null || accelerometerSample === null) {
1615
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "invalid or stale snapshot", {
1616
+ metrics,
1617
+ accelerometerSample
1618
+ });
1619
+ this.processUnavailable("invalid_or_stale_snapshot");
1620
+ } else {
1621
+ this.processAvailable(metrics, accelerometerSample, gravity);
1622
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "processed sample", { capturedAtMs: metrics.capturedAtMs });
1623
+ }
1517
1624
  } catch (error) {
1518
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "provider rejected", error);
1519
1625
  if (!this.polling || generation !== this.generation || pendingRead.signal.aborted || isAbortError$1(error)) {
1520
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "ignored rejection");
1626
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "ignored provider error", { error });
1521
1627
  return;
1522
1628
  }
1629
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "caught provider error", { error });
1523
1630
  this.processUnavailable("provider_error");
1524
1631
  } finally {
1525
1632
  if (this.pendingRead === pendingRead) this.pendingRead = null;
1526
1633
  if (this.polling && generation === this.generation) this.schedulePoll(this.pollIntervalMs, generation);
1527
1634
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "completed", {
1528
1635
  generation,
1529
- polling: this.polling,
1530
- currentGeneration: this.generation
1636
+ polling: this.polling
1531
1637
  });
1532
1638
  }
1533
1639
  }
1534
- processAvailable(rawMetrics, accelerometerSample) {
1640
+ resolveGravityFrame(info) {
1641
+ const sample = extractFreshVectorMetric(info.gravity, info.capturedAtMs, this.tuning.maxSampleAgeMs);
1642
+ if (sample === null) {
1643
+ if (this.filteredGravity === null || this.lastGravitySampledAtMs === null) return null;
1644
+ const ageMs = info.capturedAtMs - this.lastGravitySampledAtMs;
1645
+ return ageMs >= 0 && ageMs <= this.tuning.maxSampleAgeMs ? {
1646
+ value: this.filteredGravity,
1647
+ sampledAtMs: this.lastGravitySampledAtMs,
1648
+ source: "cached"
1649
+ } : null;
1650
+ }
1651
+ if (sample.sampledAtMs === this.lastGravitySampledAtMs && this.filteredGravity !== null) return {
1652
+ value: this.filteredGravity,
1653
+ sampledAtMs: sample.sampledAtMs,
1654
+ source: "live"
1655
+ };
1656
+ if (this.filteredGravity === null || this.lastGravitySampledAtMs === null) this.filteredGravity = { ...sample.value };
1657
+ else {
1658
+ if (sample.sampledAtMs < this.lastGravitySampledAtMs) {
1659
+ const ageMs = info.capturedAtMs - this.lastGravitySampledAtMs;
1660
+ return ageMs >= 0 && ageMs <= this.tuning.maxSampleAgeMs ? {
1661
+ value: this.filteredGravity,
1662
+ sampledAtMs: this.lastGravitySampledAtMs,
1663
+ source: "cached"
1664
+ } : null;
1665
+ }
1666
+ const elapsedMs = Math.max(0, sample.sampledAtMs - this.lastGravitySampledAtMs);
1667
+ const alpha = 1 - Math.exp(-elapsedMs / this.tuning.gravityFilterTimeConstantMs);
1668
+ this.filteredGravity = lerpVector(this.filteredGravity, sample.value, alpha);
1669
+ }
1670
+ this.lastGravitySampledAtMs = sample.sampledAtMs;
1671
+ return {
1672
+ value: this.filteredGravity,
1673
+ sampledAtMs: sample.sampledAtMs,
1674
+ source: "live"
1675
+ };
1676
+ }
1677
+ processAvailable(metrics, accelerometerSample, gravity) {
1535
1678
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processAvailable", "called", {
1536
- rawMetrics,
1679
+ metrics,
1537
1680
  accelerometerSample
1538
1681
  });
1539
1682
  const now = this.clock.now();
@@ -1543,56 +1686,211 @@ var DefaultDrivingStatusController = class {
1543
1686
  this.emitDiagnostic({
1544
1687
  type: "sample_available",
1545
1688
  atMs: now,
1546
- capturedAtMs: rawMetrics.capturedAtMs,
1689
+ capturedAtMs: metrics.capturedAtMs,
1690
+ frameSource: gravity.source,
1691
+ frameAgeMs: metrics.capturedAtMs - gravity.sampledAtMs,
1547
1692
  tuningVersion: this.tuning.version
1548
1693
  });
1549
- this.metricWindow.push(rawMetrics);
1550
- while (this.metricWindow.length > Math.floor(this.tuning.smoothingWindowSize)) this.metricWindow.shift();
1551
- const smoothedMetrics = averageMetrics(this.metricWindow);
1552
- const motionIntensity = this.recordMotionSample(accelerometerSample, rawMetrics.capturedAtMs);
1553
- const nextCandidate = classifyControllerCandidate(rawMetrics, smoothedMetrics, motionIntensity, this.status, this.baseStatus, this.tuning);
1554
- this.processCandidate(nextCandidate, now);
1555
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processAvailable", "returned", {
1556
- rawMetrics,
1557
- smoothedMetrics,
1558
- motionIntensity,
1559
- nextCandidate,
1560
- snapshot: this.getSnapshot()
1694
+ const addedLongitudinal = pushUniqueMetric(this.longitudinalWindow, metrics, "linearAccelerationSampledAtMs", Math.max(this.tuning.longitudinalWindowMs, this.tuning.rapidLongitudinalWindowMs));
1695
+ const addedYaw = pushUniqueMetric(this.yawWindow, metrics, "gyroscopeSampledAtMs", Math.max(this.tuning.turnWindowMs, this.tuning.turnClosureWindowMs));
1696
+ const addedMotion = pushUniqueVector(this.motionWindow, accelerometerSample, this.tuning.motionWindowMs);
1697
+ if (!addedLongitudinal && !addedYaw && !addedMotion) {
1698
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processAvailable", "ignored duplicate sample");
1699
+ return;
1700
+ }
1701
+ const motionMad = calculateCombinedAxisMad(this.motionWindow, this.tuning.motionMinimumSamples);
1702
+ const previousBaseStatus = this.baseStatus;
1703
+ const previousUrgentStatus = this.urgentStatus;
1704
+ const previousLongitudinalStatus = this.normalLongitudinalStatus;
1705
+ const previousTurnStatus = this.turnStatus;
1706
+ const baseCondition = this.updateBaseDetector(motionMad, metrics, accelerometerSample.sampledAtMs, addedMotion);
1707
+ const longitudinalEvidence = detectLongitudinalEvidence(this.longitudinalWindow, this.tuning);
1708
+ if (addedLongitudinal) this.updateLongitudinalDetector(longitudinalEvidence, metrics.linearAccelerationSampledAtMs);
1709
+ const turnEvidence = detectTurnEvidence(this.yawWindow, this.tuning);
1710
+ if (addedYaw) this.updateTurnDetector(turnEvidence);
1711
+ if (this.urgentStatus !== null || this.turnStatus !== null || this.normalLongitudinalStatus !== null) {
1712
+ this.baseStatus = "STEADY_DRIVING";
1713
+ this.stoppedConditionSinceMs = null;
1714
+ this.stationaryConfirmed = false;
1715
+ }
1716
+ if (this.stationaryConfirmed) this.updateBias(metrics);
1717
+ const statistics = {
1718
+ base: {
1719
+ sampleCount: this.motionWindow.length,
1720
+ durationMs: vectorWindowDuration(this.motionWindow),
1721
+ motionMad,
1722
+ gyroMagnitude: metrics.gyroMagnitude,
1723
+ rawGyroMagnitude: metrics.rawGyroMagnitude
1724
+ },
1725
+ longitudinal: longitudinalEvidence.normalStatistics,
1726
+ rapidLongitudinal: longitudinalEvidence.rapidStatistics,
1727
+ turn: turnEvidence.statistics
1728
+ };
1729
+ const baseCandidate = baseCondition === "stationary" ? "STOPPED" : baseCondition === "moving" ? "STEADY_DRIVING" : null;
1730
+ this.updateDetectorCandidate("base", baseCandidate, null, statistics, now);
1731
+ this.updateDetectorCandidate("rapid_longitudinal", longitudinalEvidence.rapidCandidate, longitudinalEvidence.rejectionReason, statistics, now);
1732
+ this.updateDetectorCandidate("longitudinal", longitudinalEvidence.normalCandidate, null, statistics, now);
1733
+ this.updateDetectorCandidate("turn", turnEvidence.candidate, turnEvidence.rejectionReason, statistics, now);
1734
+ this.emitDetectorConfirmation("base", previousBaseStatus, this.baseStatus, statistics, now);
1735
+ this.emitDetectorConfirmation("rapid_longitudinal", previousUrgentStatus, this.urgentStatus, statistics, now);
1736
+ this.emitDetectorConfirmation("longitudinal", previousLongitudinalStatus, this.normalLongitudinalStatus, statistics, now);
1737
+ this.emitDetectorConfirmation("turn", previousTurnStatus, this.turnStatus, statistics, now);
1738
+ const pendingCandidate = instantaneousCandidate(metrics, this.tuning);
1739
+ this.setCandidate(pendingCandidate === this.status ? null : pendingCandidate, observationTime(metrics));
1740
+ this.emitDiagnostic({
1741
+ type: "detector_evidence",
1742
+ atMs: now,
1743
+ motionMad,
1744
+ baseCondition,
1745
+ rapidCandidate: longitudinalEvidence.rapidCandidate,
1746
+ longitudinalCandidate: longitudinalEvidence.normalCandidate,
1747
+ turnCandidate: turnEvidence.candidate,
1748
+ rejectionReason: longitudinalEvidence.rejectionReason ?? turnEvidence.rejectionReason,
1749
+ frameSource: gravity.source,
1750
+ frameAgeMs: metrics.capturedAtMs - gravity.sampledAtMs,
1751
+ statistics,
1752
+ tuningVersion: this.tuning.version
1561
1753
  });
1754
+ const hasAction = this.urgentStatus !== null || this.turnStatus !== null || this.normalLongitudinalStatus !== null;
1755
+ if (motionMad !== null || hasAction) this.commitSelectedStatus(now);
1756
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processAvailable", "returned", {
1757
+ motionMad,
1758
+ baseCondition,
1759
+ status: this.status,
1760
+ baseStatus: this.baseStatus,
1761
+ urgentStatus: this.urgentStatus,
1762
+ turnStatus: this.turnStatus,
1763
+ normalLongitudinalStatus: this.normalLongitudinalStatus
1764
+ });
1765
+ }
1766
+ updateBaseDetector(motionMad, metrics, sampledAtMs, isNewMotionSample) {
1767
+ if (motionMad === null || !isNewMotionSample) return "warming_up";
1768
+ const gyroForStop = this.biasCalibrated ? metrics.gyroMagnitude : metrics.rawGyroMagnitude;
1769
+ const stoppedGyroThreshold = this.biasCalibrated ? this.tuning.stoppedGyroThreshold : this.tuning.stoppedRawGyroThreshold;
1770
+ const drivingGyroThreshold = this.biasCalibrated ? this.tuning.drivingGyroThreshold : this.tuning.drivingRawGyroThreshold;
1771
+ const stationary = motionMad <= this.tuning.stoppedMotionThreshold && gyroForStop <= stoppedGyroThreshold;
1772
+ const moving = motionMad >= this.tuning.drivingMotionThreshold || gyroForStop >= drivingGyroThreshold;
1773
+ if (stationary) {
1774
+ this.drivingConditionSinceMs = null;
1775
+ if (this.stoppedConditionSinceMs === null) this.stoppedConditionSinceMs = this.motionWindow[0]?.sampledAtMs ?? sampledAtMs;
1776
+ if (sampledAtMs - this.stoppedConditionSinceMs >= this.tuning.stoppedConfirmationMs) {
1777
+ this.baseStatus = "STOPPED";
1778
+ this.stationaryConfirmed = true;
1779
+ }
1780
+ return "stationary";
1781
+ }
1782
+ this.stoppedConditionSinceMs = null;
1783
+ this.stationaryConfirmed = false;
1784
+ if (moving) {
1785
+ if (this.drivingConditionSinceMs === null) this.drivingConditionSinceMs = this.motionWindow[0]?.sampledAtMs ?? sampledAtMs;
1786
+ if (sampledAtMs - this.drivingConditionSinceMs >= this.tuning.drivingConfirmationMs) this.baseStatus = "STEADY_DRIVING";
1787
+ return "moving";
1788
+ }
1789
+ this.drivingConditionSinceMs = null;
1790
+ return "hysteresis";
1791
+ }
1792
+ updateLongitudinalDetector(evidence, sampledAtMs) {
1793
+ if (evidence.rapidCandidate !== null) {
1794
+ if (this.urgentStatus !== evidence.rapidCandidate) {
1795
+ this.urgentStatus = evidence.rapidCandidate;
1796
+ this.urgentStartedAtMs = sampledAtMs;
1797
+ }
1798
+ } else if (this.urgentStatus !== null && this.urgentStartedAtMs !== null && sampledAtMs - this.urgentStartedAtMs >= this.tuning.rapidMinimumDisplayMs) {
1799
+ this.urgentStatus = null;
1800
+ this.urgentStartedAtMs = null;
1801
+ }
1802
+ if (evidence.normalCandidate !== null) {
1803
+ this.normalLongitudinalStatus = evidence.normalCandidate;
1804
+ this.longitudinalClearSinceMs = null;
1805
+ } else if (this.normalLongitudinalStatus !== null) if (evidence.retainedNormal) this.longitudinalClearSinceMs = null;
1806
+ else {
1807
+ if (this.longitudinalClearSinceMs === null) this.longitudinalClearSinceMs = sampledAtMs;
1808
+ if (sampledAtMs - this.longitudinalClearSinceMs >= this.tuning.longitudinalReleaseMs) {
1809
+ this.normalLongitudinalStatus = null;
1810
+ this.longitudinalClearSinceMs = null;
1811
+ }
1812
+ }
1562
1813
  }
1563
- recordMotionSample(sample, capturedAtMs) {
1564
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.recordMotionSample", "called", {
1565
- sample,
1566
- capturedAtMs,
1567
- motionWindow: this.motionWindow
1568
- });
1569
- if (!this.motionWindow.some((existing) => existing.sampledAtMs === sample.sampledAtMs)) {
1570
- this.motionWindow.push(sample);
1571
- this.motionWindow.sort((left, right) => left.sampledAtMs - right.sampledAtMs);
1814
+ updateTurnDetector(evidence) {
1815
+ if (this.turnStatus === null) {
1816
+ if (evidence.candidate !== null) this.turnStatus = evidence.candidate;
1817
+ this.turnQuietSinceMs = null;
1818
+ this.oppositeTurnSinceMs = null;
1819
+ return;
1572
1820
  }
1573
- const cutoffMs = capturedAtMs - this.tuning.motionWindowMs;
1574
- while (this.motionWindow[0] && this.motionWindow[0].sampledAtMs <= cutoffMs) this.motionWindow.shift();
1575
- if (this.motionWindow.length < this.tuning.motionMinimumSamples) {
1576
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.recordMotionSample", "returned warmup", {
1577
- result: null,
1578
- motionWindow: this.motionWindow
1579
- });
1580
- return null;
1821
+ const direction = evidence.latestYaw > 0 ? "LEFT_TURN" : "RIGHT_TURN";
1822
+ if (!(Math.abs(evidence.latestYaw) >= this.tuning.turnExitYawThreshold)) {
1823
+ this.oppositeTurnSinceMs = null;
1824
+ if (this.turnQuietSinceMs === null) this.turnQuietSinceMs = evidence.sampledAtMs;
1825
+ if (evidence.sampledAtMs - this.turnQuietSinceMs >= this.tuning.turnExitConfirmationMs) {
1826
+ this.turnStatus = null;
1827
+ this.turnQuietSinceMs = null;
1828
+ this.yawWindow.length = 0;
1829
+ }
1830
+ return;
1581
1831
  }
1582
- const result = combinedAxisStandardDeviation(this.motionWindow);
1583
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.recordMotionSample", "returned", {
1584
- result,
1585
- motionWindow: this.motionWindow
1832
+ this.turnQuietSinceMs = null;
1833
+ if (direction === this.turnStatus) {
1834
+ this.oppositeTurnSinceMs = null;
1835
+ return;
1836
+ }
1837
+ if (this.oppositeTurnSinceMs === null) this.oppositeTurnSinceMs = evidence.sampledAtMs;
1838
+ if (evidence.sampledAtMs - this.oppositeTurnSinceMs >= this.tuning.turnClosureWindowMs) {
1839
+ this.turnStatus = direction;
1840
+ this.oppositeTurnSinceMs = null;
1841
+ }
1842
+ }
1843
+ updateBias(metrics) {
1844
+ const sampledAtMs = Math.min(metrics.linearAccelerationSampledAtMs, metrics.gyroscopeSampledAtMs);
1845
+ if (sampledAtMs === this.lastBiasSampledAtMs) return;
1846
+ if (this.lastBiasSampledAtMs !== null && sampledAtMs < this.lastBiasSampledAtMs) return;
1847
+ if (!this.biasCalibrated || this.lastBiasSampledAtMs === null) {
1848
+ this.linearAccelerationBias = { ...metrics.rawLinearAcceleration };
1849
+ this.gyroscopeBias = { ...metrics.rawGyroscope };
1850
+ this.biasCalibrated = true;
1851
+ } else {
1852
+ const elapsedMs = Math.max(0, sampledAtMs - this.lastBiasSampledAtMs);
1853
+ const alpha = 1 - Math.exp(-elapsedMs / this.tuning.biasFilterTimeConstantMs);
1854
+ this.linearAccelerationBias = lerpVector(this.linearAccelerationBias, metrics.rawLinearAcceleration, alpha);
1855
+ this.gyroscopeBias = lerpVector(this.gyroscopeBias, metrics.rawGyroscope, alpha);
1856
+ }
1857
+ this.lastBiasSampledAtMs = sampledAtMs;
1858
+ }
1859
+ updateDetectorCandidate(detector, candidate, rejectionReason, statistics, atMs) {
1860
+ const previousCandidate = this.detectorCandidates[detector];
1861
+ if (previousCandidate !== candidate) {
1862
+ if (previousCandidate !== null) this.emitDetectorTransition(detector, "candidate_exited", previousCandidate, null, statistics, atMs);
1863
+ if (candidate !== null) this.emitDetectorTransition(detector, "candidate_entered", candidate, null, statistics, atMs);
1864
+ this.detectorCandidates[detector] = candidate;
1865
+ }
1866
+ if (rejectionReason !== null && rejectionReason !== this.detectorRejections[detector]) this.emitDetectorTransition(detector, "candidate_rejected", null, rejectionReason, statistics, atMs);
1867
+ this.detectorRejections[detector] = rejectionReason;
1868
+ }
1869
+ emitDetectorConfirmation(detector, previousStatus, status, statistics, atMs) {
1870
+ if (status !== null && status !== previousStatus) this.emitDetectorTransition(detector, "candidate_confirmed", status, null, statistics, atMs);
1871
+ }
1872
+ emitDetectorTransition(detector, phase, status, rejectionReason, statistics, atMs) {
1873
+ this.emitDiagnostic({
1874
+ type: "detector_transition",
1875
+ atMs,
1876
+ detector,
1877
+ phase,
1878
+ status,
1879
+ rejectionReason,
1880
+ statistics,
1881
+ tuningVersion: this.tuning.version
1586
1882
  });
1587
- return result;
1883
+ }
1884
+ commitSelectedStatus(now) {
1885
+ const selected = this.urgentStatus ?? this.turnStatus ?? this.normalLongitudinalStatus ?? this.baseStatus;
1886
+ this.commit(selected, now, "candidate_confirmed");
1588
1887
  }
1589
1888
  processUnavailable(category) {
1590
1889
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processUnavailable", "called", { category });
1591
1890
  const now = this.clock.now();
1592
1891
  if (this.unavailableSinceMs === null) this.unavailableSinceMs = now;
1593
1892
  const unavailableForMs = Math.max(0, now - this.unavailableSinceMs);
1594
- this.metricWindow.length = 0;
1595
- this.motionWindow.length = 0;
1893
+ this.clearRecognitionWindows();
1596
1894
  this.setCandidate(null, now);
1597
1895
  this.emitDiagnostic({
1598
1896
  type: "sample_unavailable",
@@ -1602,97 +1900,39 @@ var DefaultDrivingStatusController = class {
1602
1900
  tuningVersion: this.tuning.version
1603
1901
  });
1604
1902
  if (unavailableForMs < this.unavailableFallbackMs || this.fallbackApplied) {
1605
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processUnavailable", "returned while holding", {
1903
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processUnavailable", "holding previous status", {
1904
+ category,
1606
1905
  unavailableForMs,
1607
1906
  fallbackApplied: this.fallbackApplied
1608
1907
  });
1609
1908
  return;
1610
1909
  }
1611
1910
  this.fallbackApplied = true;
1911
+ this.baseStatus = "STOPPED";
1612
1912
  this.emitDiagnostic({
1613
1913
  type: "fallback_to_stopped",
1614
1914
  atMs: now,
1615
1915
  unavailableForMs,
1616
1916
  tuningVersion: this.tuning.version
1617
1917
  });
1618
- this.commit("STOPPED", now, "unavailable_fallback", true);
1619
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processUnavailable", "returned after fallback", {
1918
+ this.commit("STOPPED", now, "unavailable_fallback");
1919
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processUnavailable", "returned fallback", {
1620
1920
  unavailableForMs,
1621
- snapshot: this.getSnapshot()
1622
- });
1623
- }
1624
- processCandidate(nextCandidate, now) {
1625
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processCandidate", "called", {
1626
- nextCandidate,
1627
- now,
1628
- currentStatus: this.status,
1629
- candidate: this.candidate,
1630
- candidateConfirmationCount: this.candidateConfirmationCount
1921
+ status: this.status
1631
1922
  });
1632
- if (nextCandidate === this.status) {
1633
- this.setCandidate(null, now);
1634
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processCandidate", "returned current status");
1635
- return;
1636
- }
1637
- if (nextCandidate !== this.candidate) {
1638
- this.setCandidate(nextCandidate, now);
1639
- this.candidateConfirmationCount = 1;
1640
- this.heldCandidate = null;
1641
- } else this.candidateConfirmationCount += 1;
1642
- if (this.candidateConfirmationCount < this.tuning.confirmationSamples[nextCandidate]) {
1643
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processCandidate", "awaiting confirmation", {
1644
- nextCandidate,
1645
- candidateConfirmationCount: this.candidateConfirmationCount,
1646
- required: this.tuning.confirmationSamples[nextCandidate]
1647
- });
1648
- return;
1649
- }
1650
- const elapsedMs = Math.max(0, now - this.statusCommittedAtMs);
1651
- const remainingMs = Math.max(0, this.tuning.minimumDisplayMs[this.status] - elapsedMs);
1652
- const canPreempt = CAR_RUNNING_STATUS_PRIORITY[nextCandidate] > CAR_RUNNING_STATUS_PRIORITY[this.status];
1653
- if (remainingMs > 0 && !canPreempt) {
1654
- if (this.heldCandidate !== nextCandidate) {
1655
- this.heldCandidate = nextCandidate;
1656
- this.emitDiagnostic({
1657
- type: "status_held_by_min_duration",
1658
- atMs: now,
1659
- status: this.status,
1660
- candidate: nextCandidate,
1661
- remainingMs,
1662
- tuningVersion: this.tuning.version
1663
- });
1664
- }
1665
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processCandidate", "held by minimum duration", {
1666
- nextCandidate,
1667
- remainingMs
1668
- });
1669
- return;
1670
- }
1671
- this.commit(nextCandidate, now, "candidate_confirmed");
1672
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processCandidate", "returned after commit", { snapshot: this.getSnapshot() });
1673
1923
  }
1674
- commit(nextStatus, now, reason, force = false) {
1675
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.commit", "called", {
1676
- nextStatus,
1677
- now,
1678
- reason,
1679
- force,
1680
- currentStatus: this.status
1681
- });
1682
- if (!force && nextStatus === this.status) {
1683
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.commit", "ignored unchanged status");
1684
- return;
1685
- }
1924
+ commit(nextStatus, now, reason) {
1686
1925
  if (nextStatus === this.status) {
1687
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.commit", "ignored forced unchanged status");
1926
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.commit", "ignored unchanged status", {
1927
+ status: this.status,
1928
+ reason
1929
+ });
1688
1930
  return;
1689
1931
  }
1690
1932
  const previousStatus = this.status;
1691
1933
  this.status = nextStatus;
1692
- if (isBaseStatus(nextStatus)) this.baseStatus = nextStatus;
1693
1934
  this.statusCommittedAtMs = now;
1694
1935
  this.setCandidate(null, now);
1695
- this.heldCandidate = null;
1696
1936
  const event = {
1697
1937
  type: "status_changed",
1698
1938
  status: nextStatus,
@@ -1709,53 +1949,53 @@ var DefaultDrivingStatusController = class {
1709
1949
  reason,
1710
1950
  tuningVersion: this.tuning.version
1711
1951
  });
1712
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.commit", "returned", {
1713
- event,
1714
- listenerCount: this.listeners.size,
1715
- snapshot: this.getSnapshot()
1716
- });
1952
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.commit", "status changed", { event });
1717
1953
  }
1718
- setCandidate(candidate, now) {
1719
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.setCandidate", "called", {
1720
- candidate,
1721
- now,
1722
- currentCandidate: this.candidate
1723
- });
1724
- if (candidate === this.candidate) {
1725
- if (candidate === null) this.candidateConfirmationCount = 0;
1726
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.setCandidate", "returned unchanged", { candidateConfirmationCount: this.candidateConfirmationCount });
1727
- return;
1728
- }
1954
+ setCandidate(candidate, atMs) {
1955
+ if (candidate === this.candidate) return;
1729
1956
  const previousCandidate = this.candidate;
1730
1957
  this.candidate = candidate;
1731
- this.candidateConfirmationCount = 0;
1732
- this.heldCandidate = null;
1958
+ this.candidateSinceMs = candidate === null ? null : atMs;
1733
1959
  this.emitDiagnostic({
1734
1960
  type: "candidate_changed",
1735
- atMs: now,
1961
+ atMs: this.clock.now(),
1736
1962
  candidate,
1737
1963
  previousCandidate,
1738
1964
  tuningVersion: this.tuning.version
1739
1965
  });
1740
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.setCandidate", "returned", {
1966
+ dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.setCandidate", "candidate changed", {
1741
1967
  candidate,
1742
- previousCandidate
1968
+ previousCandidate,
1969
+ candidateSinceMs: this.candidateSinceMs
1743
1970
  });
1744
1971
  }
1745
- resetTransientRecognition() {
1746
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.resetTransientRecognition", "called", {
1747
- metricWindow: this.metricWindow,
1748
- motionWindow: this.motionWindow,
1749
- candidate: this.candidate
1750
- });
1751
- this.metricWindow.length = 0;
1972
+ clearRecognitionWindows() {
1973
+ this.longitudinalWindow.length = 0;
1974
+ this.yawWindow.length = 0;
1752
1975
  this.motionWindow.length = 0;
1976
+ this.stoppedConditionSinceMs = null;
1977
+ this.drivingConditionSinceMs = null;
1978
+ this.stationaryConfirmed = false;
1979
+ this.urgentStatus = null;
1980
+ this.urgentStartedAtMs = null;
1981
+ this.normalLongitudinalStatus = null;
1982
+ this.longitudinalClearSinceMs = null;
1983
+ this.turnStatus = null;
1984
+ this.turnQuietSinceMs = null;
1985
+ this.oppositeTurnSinceMs = null;
1986
+ for (const detector of Object.keys(this.detectorCandidates)) {
1987
+ this.detectorCandidates[detector] = null;
1988
+ this.detectorRejections[detector] = null;
1989
+ }
1990
+ }
1991
+ resetTransientRecognition() {
1992
+ this.clearRecognitionWindows();
1753
1993
  this.candidate = null;
1754
- this.candidateConfirmationCount = 0;
1994
+ this.candidateSinceMs = null;
1755
1995
  this.unavailableSinceMs = null;
1756
1996
  this.fallbackApplied = false;
1757
- this.heldCandidate = null;
1758
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.resetTransientRecognition", "returned");
1997
+ this.filteredGravity = null;
1998
+ this.lastGravitySampledAtMs = null;
1759
1999
  }
1760
2000
  emitDiagnostic(event) {
1761
2001
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.emitDiagnostic", "called", {
@@ -1763,315 +2003,256 @@ var DefaultDrivingStatusController = class {
1763
2003
  hasListener: this.onDiagnostic !== void 0
1764
2004
  });
1765
2005
  if (this.onDiagnostic) safeCall(this.onDiagnostic, event);
1766
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.emitDiagnostic", "returned", { event });
1767
2006
  }
1768
2007
  attachLifecycleListeners() {
1769
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.attachLifecycleListeners", "called", {
1770
- managePageLifecycle: this.managePageLifecycle,
1771
- attached: this.unsubscribePageLifecycle !== null
1772
- });
1773
- if (!this.managePageLifecycle || this.unsubscribePageLifecycle !== null) {
1774
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.attachLifecycleListeners", "ignored");
1775
- return;
1776
- }
2008
+ if (!this.managePageLifecycle || this.unsubscribePageLifecycle !== null) return;
1777
2009
  this.unsubscribePageLifecycle = subscribeDrivingPageLifecycle(this.handleLifecyclePause, this.handleLifecycleResume);
1778
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.attachLifecycleListeners", "returned");
1779
2010
  }
1780
2011
  detachLifecycleListeners() {
1781
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.detachLifecycleListeners", "called", { attached: this.unsubscribePageLifecycle !== null });
1782
2012
  this.unsubscribePageLifecycle?.();
1783
2013
  this.unsubscribePageLifecycle = null;
1784
- dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.detachLifecycleListeners", "returned");
1785
2014
  }
1786
2015
  };
1787
- const classifyControllerCandidate = (raw, smoothed, motionIntensity, current, baseStatus, tuning) => {
1788
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyControllerCandidate", "called", {
1789
- raw,
1790
- smoothed,
1791
- motionIntensity,
1792
- current,
1793
- baseStatus,
1794
- tuning
1795
- });
1796
- const entry = classifyEntryCandidate(raw, smoothed, motionIntensity, baseStatus, tuning);
1797
- const retained = retainsCurrentStatus(raw, smoothed, motionIntensity, current, tuning);
1798
- const result = !retained || CAR_RUNNING_STATUS_PRIORITY[entry] > CAR_RUNNING_STATUS_PRIORITY[current] || isOppositeTurn(entry, current) || (entry === "STOPPED" || entry === "STEADY_DRIVING") && (current === "STOPPED" || current === "STEADY_DRIVING") ? entry : current;
1799
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyControllerCandidate", "returned", {
1800
- entry,
1801
- retained,
1802
- result
1803
- });
1804
- return result;
2016
+ const detectLongitudinalEvidence = (window, tuning) => {
2017
+ const rapidWindow = recentMetrics(window, tuning.rapidLongitudinalWindowMs, "linearAccelerationSampledAtMs");
2018
+ const normalWindow = recentMetrics(window, tuning.longitudinalWindowMs, "linearAccelerationSampledAtMs");
2019
+ let rapidCandidate = null;
2020
+ let rejectionReason = null;
2021
+ const rapidValues = rapidWindow.map((sample) => sample.longitudinal);
2022
+ const rapidMean = trimmedMean(rapidValues);
2023
+ const rapidDirection = Math.sign(rapidMean);
2024
+ const rapidCoherence = signCoherence(rapidValues, rapidDirection, tuning.longitudinalExitThreshold);
2025
+ const rapidPeak = maximumAbsolute(rapidValues);
2026
+ const longitudinalRms = rootMeanSquare(rapidValues);
2027
+ const verticalRms = rootMeanSquare(rapidWindow.map((sample) => sample.vertical));
2028
+ const reversalWindow = recentMetrics(window, tuning.longitudinalReversalWindowMs, "linearAccelerationSampledAtMs");
2029
+ const hasReversal = containsBothSigns(reversalWindow.map((sample) => sample.longitudinal), tuning.longitudinalThreshold);
2030
+ const rapidStatistics = rapidWindow.length === 0 ? null : {
2031
+ sampleCount: rapidWindow.length,
2032
+ durationMs: metricWindowDuration(rapidWindow, "linearAccelerationSampledAtMs"),
2033
+ mean: rapidMean,
2034
+ peak: rapidPeak,
2035
+ coherence: rapidCoherence,
2036
+ longitudinalRms,
2037
+ verticalRms,
2038
+ hasReversal
2039
+ };
2040
+ if (windowCovers(rapidWindow, tuning.rapidLongitudinalMinimumSamples, tuning.rapidLongitudinalWindowMs, "linearAccelerationSampledAtMs")) {
2041
+ if (rapidPeak >= tuning.rapidLongitudinalPeakThreshold && Math.abs(rapidMean) >= tuning.rapidLongitudinalMeanThreshold) {
2042
+ if (verticalRms > longitudinalRms * tuning.verticalBumpRatio) rejectionReason = "vertical_bump";
2043
+ else if (hasReversal) rejectionReason = "longitudinal_reversal";
2044
+ else if (rapidDirection !== 0 && rapidCoherence >= tuning.rapidLongitudinalSignCoherence) rapidCandidate = rapidDirection > 0 ? "RAPID_ACCELERATION" : "SUDDEN_BRAKING";
2045
+ }
2046
+ }
2047
+ let normalCandidate = null;
2048
+ let retainedNormal = false;
2049
+ let normalStatistics = null;
2050
+ if (normalWindow.length > 0) {
2051
+ const values = normalWindow.map((sample) => sample.longitudinal);
2052
+ const mean = trimmedMean(values);
2053
+ const direction = Math.sign(mean);
2054
+ const coherence = signCoherence(values, direction, tuning.longitudinalExitThreshold);
2055
+ normalStatistics = {
2056
+ sampleCount: normalWindow.length,
2057
+ durationMs: metricWindowDuration(normalWindow, "linearAccelerationSampledAtMs"),
2058
+ mean,
2059
+ peak: maximumAbsolute(values),
2060
+ coherence
2061
+ };
2062
+ retainedNormal = Math.abs(mean) >= tuning.longitudinalExitThreshold;
2063
+ if (windowCovers(normalWindow, tuning.longitudinalMinimumSamples, tuning.longitudinalWindowMs, "linearAccelerationSampledAtMs") && Math.abs(mean) >= tuning.longitudinalThreshold && coherence >= tuning.longitudinalSignCoherence) normalCandidate = direction > 0 ? "ACCELERATION" : "BRAKING";
2064
+ }
2065
+ return {
2066
+ rapidCandidate,
2067
+ normalCandidate,
2068
+ retainedNormal,
2069
+ rejectionReason,
2070
+ normalStatistics,
2071
+ rapidStatistics
2072
+ };
1805
2073
  };
1806
- const classifyEntryCandidate = (raw, smoothed, motionIntensity, baseStatus, tuning) => {
1807
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyEntryCandidate", "called", {
1808
- raw,
1809
- smoothed,
1810
- motionIntensity,
1811
- baseStatus,
1812
- tuning
1813
- });
1814
- if (raw.longitudinal <= -tuning.rapidLongitudinalThreshold) {
1815
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyEntryCandidate", "returned", { result: "SUDDEN_BRAKING" });
1816
- return "SUDDEN_BRAKING";
1817
- }
1818
- if (raw.longitudinal >= tuning.rapidLongitudinalThreshold) {
1819
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyEntryCandidate", "returned", { result: "RAPID_ACCELERATION" });
1820
- return "RAPID_ACCELERATION";
1821
- }
1822
- if (Math.abs(smoothed.yaw) >= tuning.turnYawThreshold) {
1823
- const result = smoothed.yaw > 0 ? "LEFT_TURN" : "RIGHT_TURN";
1824
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyEntryCandidate", "returned", { result });
1825
- return result;
1826
- }
1827
- if (Math.abs(smoothed.lateral) >= tuning.turnLateralThreshold) {
1828
- const result = smoothed.lateral < 0 ? "LEFT_TURN" : "RIGHT_TURN";
1829
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyEntryCandidate", "returned", { result });
1830
- return result;
1831
- }
1832
- if (smoothed.longitudinal <= -tuning.longitudinalThreshold) {
1833
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyEntryCandidate", "returned", { result: "BRAKING" });
1834
- return "BRAKING";
1835
- }
1836
- if (smoothed.longitudinal >= tuning.longitudinalThreshold) {
1837
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyEntryCandidate", "returned", { result: "ACCELERATION" });
1838
- return "ACCELERATION";
1839
- }
1840
- if (motionIntensity === null) {
1841
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyEntryCandidate", "returned base status", { result: baseStatus });
1842
- return baseStatus;
1843
- }
1844
- if (motionIntensity > tuning.drivingMotionThreshold || smoothed.gyroMagnitude > tuning.stoppedGyroThreshold) {
1845
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyEntryCandidate", "returned", { result: "STEADY_DRIVING" });
1846
- return "STEADY_DRIVING";
1847
- }
1848
- if (motionIntensity < tuning.stoppedMotionThreshold) {
1849
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyEntryCandidate", "returned", { result: "STOPPED" });
1850
- return "STOPPED";
1851
- }
1852
- dialSdkDebugWarn(DEBUG_MODULE$1, "classifyEntryCandidate", "returned base status", { result: baseStatus });
1853
- return baseStatus;
2074
+ const detectTurnEvidence = (window, tuning) => {
2075
+ const latest = window[window.length - 1];
2076
+ if (!latest) return {
2077
+ candidate: null,
2078
+ latestYaw: 0,
2079
+ sampledAtMs: 0,
2080
+ rejectionReason: null,
2081
+ statistics: null
2082
+ };
2083
+ const strongWindow = recentMetrics(window, tuning.turnConfirmationMs, "gyroscopeSampledAtMs");
2084
+ const strongMean = trimmedMean(strongWindow.map((sample) => sample.yaw));
2085
+ const strongDirection = Math.sign(strongMean);
2086
+ const strongCoherence = signCoherence(strongWindow.map((sample) => sample.yaw), strongDirection, tuning.turnExitYawThreshold);
2087
+ const strong = windowCovers(strongWindow, tuning.turnMinimumSamples, tuning.turnConfirmationMs, "gyroscopeSampledAtMs") && Math.abs(strongMean) >= tuning.turnYawThreshold && strongCoherence >= tuning.turnSignCoherence;
2088
+ const signedAngle = integrateYaw(window);
2089
+ const gentleDirection = Math.sign(signedAngle);
2090
+ const gentleCoherence = signCoherence(window.map((sample) => sample.yaw), gentleDirection, tuning.turnExitYawThreshold);
2091
+ const yawRms = rootMeanSquare(window.map((sample) => sample.yaw));
2092
+ const rawGyroRms = rootMeanSquare(window.map((sample) => sample.rawGyroMagnitude));
2093
+ const yawDominance = rawGyroRms <= Number.EPSILON ? 0 : Math.min(1, yawRms / rawGyroRms);
2094
+ const lateralRms = rootMeanSquare(window.map((sample) => sample.lateral));
2095
+ const verticalRms = rootMeanSquare(window.map((sample) => sample.vertical));
2096
+ const gentleShape = window.length >= tuning.turnMinimumSamples && Math.abs(signedAngle) >= tuning.turnIntegratedAngleThreshold && gentleCoherence >= tuning.turnSignCoherence;
2097
+ const verticalDominant = verticalRms > Math.max(lateralRms, tuning.turnVerticalNoiseFloor) * tuning.turnVerticalBumpRatio;
2098
+ const yawDominant = yawDominance >= tuning.turnYawDominanceThreshold;
2099
+ const laterallySupported = lateralRms >= tuning.turnLateralEvidenceThreshold;
2100
+ const rejectionReason = !strong && gentleShape && verticalDominant && !yawDominant ? "vertical_bump" : null;
2101
+ const direction = strong ? strongDirection : gentleShape && rejectionReason === null && (yawDominant || laterallySupported) ? gentleDirection : 0;
2102
+ return {
2103
+ candidate: direction === 0 ? null : direction > 0 ? "LEFT_TURN" : "RIGHT_TURN",
2104
+ latestYaw: latest.yaw,
2105
+ sampledAtMs: latest.gyroscopeSampledAtMs,
2106
+ rejectionReason,
2107
+ statistics: {
2108
+ sampleCount: window.length,
2109
+ durationMs: metricWindowDuration(window, "gyroscopeSampledAtMs"),
2110
+ strongMean,
2111
+ strongCoherence,
2112
+ signedAngle,
2113
+ gentleCoherence,
2114
+ yawRms,
2115
+ rawGyroRms,
2116
+ yawDominance,
2117
+ lateralRms,
2118
+ verticalRms
2119
+ }
2120
+ };
1854
2121
  };
1855
- const retainsCurrentStatus = (raw, smoothed, motionIntensity, current, tuning) => {
1856
- dialSdkDebugWarn(DEBUG_MODULE$1, "retainsCurrentStatus", "called", {
1857
- raw,
1858
- smoothed,
1859
- motionIntensity,
1860
- current,
1861
- tuning
1862
- });
1863
- const ratio = tuning.hysteresisRatio;
1864
- let result;
1865
- switch (current) {
1866
- case "SUDDEN_BRAKING":
1867
- result = raw.longitudinal <= -tuning.rapidLongitudinalThreshold * ratio;
1868
- break;
1869
- case "RAPID_ACCELERATION":
1870
- result = raw.longitudinal >= tuning.rapidLongitudinalThreshold * ratio;
1871
- break;
1872
- case "LEFT_TURN":
1873
- result = smoothed.yaw >= tuning.turnYawThreshold * ratio || smoothed.lateral <= -tuning.turnLateralThreshold * ratio;
1874
- break;
1875
- case "RIGHT_TURN":
1876
- result = smoothed.yaw <= -tuning.turnYawThreshold * ratio || smoothed.lateral >= tuning.turnLateralThreshold * ratio;
1877
- break;
1878
- case "BRAKING":
1879
- result = smoothed.longitudinal <= -tuning.longitudinalThreshold * ratio;
1880
- break;
1881
- case "ACCELERATION":
1882
- result = smoothed.longitudinal >= tuning.longitudinalThreshold * ratio;
1883
- break;
1884
- case "STOPPED":
1885
- result = (motionIntensity === null || motionIntensity <= tuning.drivingMotionThreshold) && smoothed.gyroMagnitude <= tuning.stoppedGyroThreshold;
1886
- break;
1887
- case "STEADY_DRIVING":
1888
- result = motionIntensity === null || motionIntensity >= tuning.stoppedMotionThreshold || smoothed.gyroMagnitude > tuning.stoppedGyroThreshold;
1889
- break;
1890
- }
1891
- dialSdkDebugWarn(DEBUG_MODULE$1, "retainsCurrentStatus", "returned", { result });
1892
- return result;
2122
+ const instantaneousCandidate = (metrics, tuning) => {
2123
+ const longitudinalMagnitude = Math.abs(metrics.longitudinal);
2124
+ if (longitudinalMagnitude >= tuning.rapidLongitudinalPeakThreshold && Math.abs(metrics.vertical) <= longitudinalMagnitude * tuning.verticalBumpRatio) return metrics.longitudinal > 0 ? "RAPID_ACCELERATION" : "SUDDEN_BRAKING";
2125
+ if (Math.abs(metrics.yaw) >= tuning.turnYawThreshold) return metrics.yaw > 0 ? "LEFT_TURN" : "RIGHT_TURN";
2126
+ if (longitudinalMagnitude >= tuning.longitudinalThreshold) return metrics.longitudinal > 0 ? "ACCELERATION" : "BRAKING";
2127
+ return null;
2128
+ };
2129
+ const recentMetrics = (values, windowMs, timestampKey) => {
2130
+ const latestAtMs = values[values.length - 1]?.[timestampKey] ?? 0;
2131
+ return values.filter((value) => value[timestampKey] >= latestAtMs - windowMs);
2132
+ };
2133
+ const windowCovers = (values, minimumSamples, durationMs, timestampKey) => {
2134
+ if (values.length < minimumSamples) return false;
2135
+ const first = values[0];
2136
+ return values[values.length - 1][timestampKey] - first[timestampKey] >= durationMs;
2137
+ };
2138
+ const integrateYaw = (values) => {
2139
+ let angle = 0;
2140
+ for (let index = 1; index < values.length; index += 1) {
2141
+ const previous = values[index - 1];
2142
+ const current = values[index];
2143
+ const elapsedSeconds = Math.max(0, current.gyroscopeSampledAtMs - previous.gyroscopeSampledAtMs) / 1e3;
2144
+ angle += (previous.yaw + current.yaw) / 2 * elapsedSeconds;
2145
+ }
2146
+ return angle;
2147
+ };
2148
+ const trimmedMean = (values) => {
2149
+ if (values.length === 0) return 0;
2150
+ if (values.length < 4) return values.reduce((sum, value) => sum + value, 0) / values.length;
2151
+ const sorted = [...values].sort((left, right) => left - right).slice(1, -1);
2152
+ return sorted.reduce((sum, value) => sum + value, 0) / sorted.length;
2153
+ };
2154
+ const signCoherence = (values, direction, deadband) => {
2155
+ if (values.length === 0 || direction === 0) return 0;
2156
+ return values.filter((value) => Math.abs(value) >= deadband && Math.sign(value) === direction).length / values.length;
2157
+ };
2158
+ const containsBothSigns = (values, threshold) => values.some((value) => value >= threshold) && values.some((value) => value <= -threshold);
2159
+ const rootMeanSquare = (values) => values.length === 0 ? 0 : Math.sqrt(values.reduce((sum, value) => sum + value ** 2, 0) / values.length);
2160
+ const maximumAbsolute = (values) => values.length === 0 ? 0 : Math.max(...values.map(Math.abs));
2161
+ const metricWindowDuration = (values, timestampKey) => {
2162
+ const first = values[0];
2163
+ const last = values[values.length - 1];
2164
+ return first && last ? Math.max(0, last[timestampKey] - first[timestampKey]) : 0;
2165
+ };
2166
+ const calculateCombinedAxisMad = (samples, minimumSamples) => {
2167
+ if (samples.length < minimumSamples) return null;
2168
+ const medianX = median(samples.map((sample) => sample.value.x));
2169
+ const medianY = median(samples.map((sample) => sample.value.y));
2170
+ const medianZ = median(samples.map((sample) => sample.value.z));
2171
+ const madX = median(samples.map((sample) => Math.abs(sample.value.x - medianX)));
2172
+ const madY = median(samples.map((sample) => Math.abs(sample.value.y - medianY)));
2173
+ const madZ = median(samples.map((sample) => Math.abs(sample.value.z - medianZ)));
2174
+ return Math.hypot(madX, madY, madZ);
2175
+ };
2176
+ const vectorWindowDuration = (values) => {
2177
+ const first = values[0];
2178
+ const last = values[values.length - 1];
2179
+ return first && last ? Math.max(0, last.sampledAtMs - first.sampledAtMs) : 0;
2180
+ };
2181
+ const median = (values) => {
2182
+ if (values.length === 0) return 0;
2183
+ const sorted = [...values].sort((left, right) => left - right);
2184
+ const middle = Math.floor(sorted.length / 2);
2185
+ return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
2186
+ };
2187
+ const pushUniqueMetric = (window, metrics, timestampKey, windowMs) => {
2188
+ const latest = window[window.length - 1];
2189
+ if (latest && metrics[timestampKey] <= latest[timestampKey]) return false;
2190
+ if (window.some((value) => value[timestampKey] === metrics[timestampKey])) return false;
2191
+ window.push(metrics);
2192
+ const cutoffMs = metrics[timestampKey] - windowMs;
2193
+ while (window[0] && window[0][timestampKey] < cutoffMs) window.shift();
2194
+ return true;
1893
2195
  };
1894
- const averageMetrics = (metrics) => {
1895
- dialSdkDebugWarn(DEBUG_MODULE$1, "averageMetrics", "called", { metrics });
1896
- const result = {
1897
- capturedAtMs: metrics[metrics.length - 1]?.capturedAtMs ?? 0,
1898
- longitudinal: 0,
1899
- lateral: 0,
1900
- yaw: 0,
1901
- motionIntensity: 0,
1902
- gyroMagnitude: 0
1903
- };
1904
- for (const value of metrics) {
1905
- result.longitudinal += value.longitudinal;
1906
- result.lateral += value.lateral;
1907
- result.yaw += value.yaw;
1908
- result.motionIntensity += value.motionIntensity;
1909
- result.gyroMagnitude += value.gyroMagnitude;
1910
- }
1911
- const count = Math.max(1, metrics.length);
1912
- result.longitudinal /= count;
1913
- result.lateral /= count;
1914
- result.yaw /= count;
1915
- result.motionIntensity /= count;
1916
- result.gyroMagnitude /= count;
1917
- dialSdkDebugWarn(DEBUG_MODULE$1, "averageMetrics", "returned", { result });
1918
- return result;
2196
+ const pushUniqueVector = (window, sample, windowMs) => {
2197
+ const latest = window[window.length - 1];
2198
+ if (latest && sample.sampledAtMs <= latest.sampledAtMs) return false;
2199
+ if (window.some((value) => value.sampledAtMs === sample.sampledAtMs)) return false;
2200
+ window.push(sample);
2201
+ const cutoffMs = sample.sampledAtMs - windowMs;
2202
+ while (window[0] && window[0].sampledAtMs < cutoffMs) window.shift();
2203
+ return true;
1919
2204
  };
1920
- const extractFreshAccelerometerSample = (info, maxSampleAgeMs) => {
1921
- dialSdkDebugWarn(DEBUG_MODULE$1, "extractFreshAccelerometerSample", "called", {
1922
- info,
1923
- maxSampleAgeMs
1924
- });
1925
- const metric = info?.accelerometer;
1926
- if (!metric?.available || !Number.isFinite(info.capturedAtMs) || !Number.isFinite(metric.sampledAtMs)) {
1927
- dialSdkDebugWarn(DEBUG_MODULE$1, "extractFreshAccelerometerSample", "returned", { result: null });
1928
- return null;
1929
- }
1930
- const ageMs = info.capturedAtMs - metric.sampledAtMs;
1931
- if (ageMs < 0 || ageMs > maxSampleAgeMs) {
1932
- dialSdkDebugWarn(DEBUG_MODULE$1, "extractFreshAccelerometerSample", "returned stale sample", {
1933
- result: null,
1934
- ageMs
1935
- });
1936
- return null;
1937
- }
2205
+ const extractFreshVectorMetric = (metric, capturedAtMs, maxSampleAgeMs) => {
2206
+ if (!metric?.available || !Number.isFinite(capturedAtMs) || !Number.isFinite(metric.sampledAtMs)) return null;
2207
+ const ageMs = capturedAtMs - metric.sampledAtMs;
2208
+ if (ageMs < 0 || ageMs > maxSampleAgeMs) return null;
1938
2209
  const { x, y, z } = metric.value;
1939
- if (![
2210
+ return [
1940
2211
  x,
1941
2212
  y,
1942
2213
  z
1943
- ].every(Number.isFinite)) {
1944
- dialSdkDebugWarn(DEBUG_MODULE$1, "extractFreshAccelerometerSample", "returned invalid vector", { result: null });
1945
- return null;
1946
- }
1947
- const result = {
2214
+ ].every(Number.isFinite) ? {
1948
2215
  sampledAtMs: metric.sampledAtMs,
1949
2216
  value: metric.value
1950
- };
1951
- dialSdkDebugWarn(DEBUG_MODULE$1, "extractFreshAccelerometerSample", "returned", { result });
1952
- return result;
1953
- };
1954
- const combinedAxisStandardDeviation = (samples) => {
1955
- dialSdkDebugWarn(DEBUG_MODULE$1, "combinedAxisStandardDeviation", "called", { samples });
1956
- const count = samples.length;
1957
- let meanX = 0;
1958
- let meanY = 0;
1959
- let meanZ = 0;
1960
- for (const sample of samples) {
1961
- meanX += sample.value.x;
1962
- meanY += sample.value.y;
1963
- meanZ += sample.value.z;
1964
- }
1965
- meanX /= count;
1966
- meanY /= count;
1967
- meanZ /= count;
1968
- let varianceX = 0;
1969
- let varianceY = 0;
1970
- let varianceZ = 0;
1971
- for (const sample of samples) {
1972
- varianceX += (sample.value.x - meanX) ** 2;
1973
- varianceY += (sample.value.y - meanY) ** 2;
1974
- varianceZ += (sample.value.z - meanZ) ** 2;
1975
- }
1976
- const result = Math.sqrt((varianceX + varianceY + varianceZ) / count);
1977
- dialSdkDebugWarn(DEBUG_MODULE$1, "combinedAxisStandardDeviation", "returned", {
1978
- meanX,
1979
- meanY,
1980
- meanZ,
1981
- varianceX,
1982
- varianceY,
1983
- varianceZ,
1984
- result
1985
- });
1986
- return result;
2217
+ } : null;
1987
2218
  };
2219
+ const observationTime = (metrics) => Math.max(metrics.linearAccelerationSampledAtMs, metrics.gyroscopeSampledAtMs);
2220
+ const lerpVector = (from, to, alpha) => ({
2221
+ x: from.x + (to.x - from.x) * alpha,
2222
+ y: from.y + (to.y - from.y) * alpha,
2223
+ z: from.z + (to.z - from.z) * alpha
2224
+ });
1988
2225
  const defaultSensorProvider = () => {
1989
- dialSdkDebugWarn(DEBUG_MODULE$1, "defaultSensorProvider", "called");
1990
2226
  const runtimeGlobal = globalThis;
1991
- if (typeof runtimeGlobal.unifiedSensorInfo !== "function") {
1992
- const error = /* @__PURE__ */ new Error("window.unifiedSensorInfo is unavailable");
1993
- dialSdkDebugWarn(DEBUG_MODULE$1, "defaultSensorProvider", "rejected", error);
1994
- return Promise.reject(error);
1995
- }
1996
- try {
1997
- const result = runtimeGlobal.unifiedSensorInfo();
1998
- result.then((payload) => dialSdkDebugWarn(DEBUG_MODULE$1, "defaultSensorProvider", "resolved", { payload }), (error) => dialSdkDebugWarn(DEBUG_MODULE$1, "defaultSensorProvider", "rejected", error));
1999
- return result;
2000
- } catch (error) {
2001
- dialSdkDebugWarn(DEBUG_MODULE$1, "defaultSensorProvider", "threw", error);
2002
- throw error;
2003
- }
2227
+ if (typeof runtimeGlobal.unifiedSensorInfo !== "function") return Promise.reject(/* @__PURE__ */ new Error("window.unifiedSensorInfo is unavailable"));
2228
+ return runtimeGlobal.unifiedSensorInfo();
2004
2229
  };
2005
2230
  const systemClock = {
2006
- now: () => {
2007
- const result = Date.now();
2008
- dialSdkDebugWarn(DEBUG_MODULE$1, "systemClock.now", "returned", { result });
2009
- return result;
2010
- },
2011
- setTimeout: (callback, delayMs) => {
2012
- const result = globalThis.setTimeout(callback, delayMs);
2013
- dialSdkDebugWarn(DEBUG_MODULE$1, "systemClock.setTimeout", "returned", {
2014
- delayMs,
2015
- result
2016
- });
2017
- return result;
2018
- },
2019
- clearTimeout: (handle) => {
2020
- dialSdkDebugWarn(DEBUG_MODULE$1, "systemClock.clearTimeout", "called", { handle });
2021
- globalThis.clearTimeout(handle);
2022
- dialSdkDebugWarn(DEBUG_MODULE$1, "systemClock.clearTimeout", "returned");
2023
- }
2024
- };
2025
- const isOppositeTurn = (candidate, current) => {
2026
- const result = candidate === "LEFT_TURN" && current === "RIGHT_TURN" || candidate === "RIGHT_TURN" && current === "LEFT_TURN";
2027
- dialSdkDebugWarn(DEBUG_MODULE$1, "isOppositeTurn", "returned", {
2028
- candidate,
2029
- current,
2030
- result
2031
- });
2032
- return result;
2033
- };
2034
- const isBaseStatus = (status) => {
2035
- const result = status === "STOPPED" || status === "STEADY_DRIVING";
2036
- dialSdkDebugWarn(DEBUG_MODULE$1, "isBaseStatus", "returned", {
2037
- status,
2038
- result
2039
- });
2040
- return result;
2041
- };
2042
- const isAbortError$1 = (error) => {
2043
- const result = error instanceof Error && error.name === "AbortError";
2044
- dialSdkDebugWarn(DEBUG_MODULE$1, "isAbortError", "returned", {
2045
- error,
2046
- result
2047
- });
2048
- return result;
2231
+ now: () => Date.now(),
2232
+ setTimeout: (callback, delayMs) => globalThis.setTimeout(callback, delayMs),
2233
+ clearTimeout: (handle) => globalThis.clearTimeout(handle)
2049
2234
  };
2235
+ const isBaseStatus = (status) => status === "STOPPED" || status === "STEADY_DRIVING";
2236
+ const isAbortError$1 = (error) => error instanceof Error && error.name === "AbortError";
2050
2237
  const requirePositiveFinite = (name, value) => {
2051
- dialSdkDebugWarn(DEBUG_MODULE$1, "requirePositiveFinite", "called", {
2052
- name,
2053
- value
2054
- });
2055
2238
  if (!Number.isFinite(value) || value <= 0) {
2056
2239
  const error = /* @__PURE__ */ new RangeError(`${name} must be a positive finite number`);
2057
2240
  dialSdkDebugWarn(DEBUG_MODULE$1, "requirePositiveFinite", "threw", error);
2058
2241
  throw error;
2059
2242
  }
2060
- dialSdkDebugWarn(DEBUG_MODULE$1, "requirePositiveFinite", "returned", { result: value });
2061
2243
  return value;
2062
2244
  };
2063
2245
  const safeCall = (callback, value) => {
2064
- dialSdkDebugWarn(DEBUG_MODULE$1, "safeCall", "called", { value });
2065
2246
  try {
2066
2247
  callback(value);
2067
- dialSdkDebugWarn(DEBUG_MODULE$1, "safeCall", "returned");
2068
2248
  } catch (error) {
2069
- dialSdkDebugWarn(DEBUG_MODULE$1, "safeCall", "callback threw", error);
2249
+ dialSdkDebugWarn(DEBUG_MODULE$1, "safeCall", "callback threw", {
2250
+ error,
2251
+ value
2252
+ });
2070
2253
  }
2071
2254
  };
2072
- const noop = () => {
2073
- dialSdkDebugWarn(DEBUG_MODULE$1, "noop", "called and returned");
2074
- };
2255
+ const noop = () => void 0;
2075
2256
  //#endregion
2076
2257
  //#region src/sensor/driving_expression_player.ts
2077
2258
  const DEBUG_MODULE = "driving_expression_player.ts";
@@ -3351,4 +3532,4 @@ const isRecord = (value) => {
3351
3532
  return result;
3352
3533
  };
3353
3534
  //#endregion
3354
- export { BATTERY_HEALTH_LABELS, CAR_RUNNING_LABELS, CAR_RUNNING_STATUSES, CAR_RUNNING_STATUS_PRIORITY, DEFAULT_DRIVING_STATUS_TUNING, DrivingExpressionResolutionError, DrivingExpressionsConfigError, OFFICIAL_DRIVING_EXPRESSIONS_CONFIG, OFFICIAL_DRIVING_EXPRESSION_FALLBACKS, checkRunningStatus, classifyDrivingMetrics, classifyDrivingSnapshot, createDrivingExpressionPlayer, createDrivingStatusController, extractDrivingSnapshotMetrics, isPublicDrivingMediaUrl, parseDrivingExpressionsConfig, readDrivingExpressionsConfig, resolveDrivingExpression, resolveDrivingExpressionStrict, resolveDrivingStatusTuning, tryParseDrivingExpressionsConfig, unifiedSensorInfo, waitForDrivingExpressionsConfig };
3535
+ export { BATTERY_HEALTH_LABELS, CAR_RUNNING_LABELS, CAR_RUNNING_STATUSES, CAR_RUNNING_STATUS_PRIORITY, DEFAULT_DRIVING_STATUS_TUNING, DrivingExpressionResolutionError, DrivingExpressionsConfigError, OFFICIAL_DRIVING_EXPRESSIONS_CONFIG, OFFICIAL_DRIVING_EXPRESSION_FALLBACKS, buildVehicleFrame, classifyDrivingSnapshot, createDrivingExpressionPlayer, createDrivingStatusController, extractDrivingSnapshotMetrics, isPublicDrivingMediaUrl, parseDrivingExpressionsConfig, readDrivingExpressionsConfig, resolveDrivingExpression, resolveDrivingExpressionStrict, resolveDrivingStatusTuning, tryParseDrivingExpressionsConfig, unifiedSensorInfo, waitForDrivingExpressionsConfig };