@ziztechnology/dial-library 0.0.11 → 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/README.md +10 -0
- package/dist/index.d.mts +63 -2
- package/dist/index.mjs +214 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -229,6 +229,8 @@ controller.destroy();
|
|
|
229
229
|
|
|
230
230
|
普通加速和刹车要求 0.8 秒内至少 4/5 的样本方向一致;急加速和急刹要求峰值、0.6 秒持续均值及方向一致性同时成立,并拒绝垂直能量占主导或短时间反号的颠簸。转向由偏航持续值或累计转角触发并锁存到回正结束。最终优先级为:急加减速、已锁存转向、普通加减速、基础状态。
|
|
231
231
|
|
|
232
|
+
缓慢转向还要求偏航旋转占陀螺仪能量的主要部分,或存在足够的横向加速度;垂直颠簸占主导且缺少偏航佐证时不会创建转向事件。重力样本短时缺失时,控制器会在 `maxSampleAgeMs` 内复用最后一个滤波后的车辆坐标系;缓存到期后按传感器不可用处理。单独调用 `classifyDrivingSnapshot()` 不会使用控制器缓存。
|
|
233
|
+
|
|
232
234
|
### 读取当前状态
|
|
233
235
|
|
|
234
236
|
订阅只通知后续变化。初次渲染或调试时,可以读取快照:
|
|
@@ -259,12 +261,20 @@ const controller = createDrivingStatusController({
|
|
|
259
261
|
stoppedMotionThreshold: 0.11,
|
|
260
262
|
stoppedConfirmationMs: 5_000,
|
|
261
263
|
longitudinalThreshold: 0.75,
|
|
264
|
+
turnYawDominanceThreshold: 0.65,
|
|
265
|
+
turnLateralEvidenceThreshold: 0.3,
|
|
262
266
|
},
|
|
263
267
|
});
|
|
264
268
|
```
|
|
265
269
|
|
|
266
270
|
`drivingMotionThreshold` 必须大于 `stoppedMotionThreshold`,进入阈值也必须大于相应退出阈值。样本数必须是正整数,方向一致率必须大于 `0.5` 且不大于 `1`,其余窗口和阈值必须是正有限数值。
|
|
267
271
|
|
|
272
|
+
### 识别诊断
|
|
273
|
+
|
|
274
|
+
`onDiagnostic` 的 `sample_available` 事件包含 `frameSource`(`live` 或 `cached`)和 `frameAgeMs`。`detector_evidence` 会报告基础状态 MAD、普通/急加减速的均值、峰值、方向一致率和 RMS,以及转向的累计角度、偏航主导率和横向/垂直 RMS。
|
|
275
|
+
|
|
276
|
+
检测器状态变化时还会发送 `detector_transition`,其 `phase` 为 `candidate_entered`、`candidate_rejected`、`candidate_confirmed` 或 `candidate_exited`。该事件携带当时的完整 `statistics`,可直接用于真车回放调参;同一状态不会逐轮询重复发送。
|
|
277
|
+
|
|
268
278
|
惯性传感器无法严格区分“完全静止”和“没有任何振动的理想匀速直线运动”。默认算法使用稳健振动统计、陀螺仪扰动和 5 秒确认近似判断,修改阈值后应使用完整路测过程回放验证。
|
|
269
279
|
|
|
270
280
|
### 驱动行车表情
|
package/dist/index.d.mts
CHANGED
|
@@ -102,6 +102,10 @@ interface DrivingStatusTuning {
|
|
|
102
102
|
turnConfirmationMs: number;
|
|
103
103
|
turnIntegratedAngleThreshold: number;
|
|
104
104
|
turnSignCoherence: number;
|
|
105
|
+
turnYawDominanceThreshold: number;
|
|
106
|
+
turnLateralEvidenceThreshold: number;
|
|
107
|
+
turnVerticalNoiseFloor: number;
|
|
108
|
+
turnVerticalBumpRatio: number;
|
|
105
109
|
turnExitYawThreshold: number;
|
|
106
110
|
turnExitConfirmationMs: number;
|
|
107
111
|
turnClosureWindowMs: number;
|
|
@@ -115,6 +119,7 @@ interface DrivingVehicleFrame {
|
|
|
115
119
|
}
|
|
116
120
|
interface DrivingMetricsCorrection {
|
|
117
121
|
filteredGravity?: Vector3Value;
|
|
122
|
+
filteredGravitySampledAtMs?: number;
|
|
118
123
|
linearAccelerationBias?: Vector3Value;
|
|
119
124
|
gyroscopeBias?: Vector3Value;
|
|
120
125
|
}
|
|
@@ -247,10 +252,54 @@ interface DrivingStatusEvent {
|
|
|
247
252
|
reason: DrivingStatusChangeReason;
|
|
248
253
|
atMs: number;
|
|
249
254
|
}
|
|
255
|
+
type DrivingFrameSource = 'live' | 'cached';
|
|
256
|
+
type DrivingDetectorName = 'base' | 'rapid_longitudinal' | 'longitudinal' | 'turn';
|
|
257
|
+
type DrivingDetectorTransitionPhase = 'candidate_entered' | 'candidate_rejected' | 'candidate_confirmed' | 'candidate_exited';
|
|
258
|
+
type DrivingDetectorRejectionReason = 'vertical_bump' | 'longitudinal_reversal' | null;
|
|
259
|
+
interface DrivingBaseDetectorStatistics {
|
|
260
|
+
sampleCount: number;
|
|
261
|
+
durationMs: number;
|
|
262
|
+
motionMad: number | null;
|
|
263
|
+
gyroMagnitude: number;
|
|
264
|
+
rawGyroMagnitude: number;
|
|
265
|
+
}
|
|
266
|
+
interface DrivingLongitudinalWindowStatistics {
|
|
267
|
+
sampleCount: number;
|
|
268
|
+
durationMs: number;
|
|
269
|
+
mean: number;
|
|
270
|
+
peak: number;
|
|
271
|
+
coherence: number;
|
|
272
|
+
}
|
|
273
|
+
interface DrivingRapidLongitudinalStatistics extends DrivingLongitudinalWindowStatistics {
|
|
274
|
+
longitudinalRms: number;
|
|
275
|
+
verticalRms: number;
|
|
276
|
+
hasReversal: boolean;
|
|
277
|
+
}
|
|
278
|
+
interface DrivingTurnDetectorStatistics {
|
|
279
|
+
sampleCount: number;
|
|
280
|
+
durationMs: number;
|
|
281
|
+
strongMean: number;
|
|
282
|
+
strongCoherence: number;
|
|
283
|
+
signedAngle: number;
|
|
284
|
+
gentleCoherence: number;
|
|
285
|
+
yawRms: number;
|
|
286
|
+
rawGyroRms: number;
|
|
287
|
+
yawDominance: number;
|
|
288
|
+
lateralRms: number;
|
|
289
|
+
verticalRms: number;
|
|
290
|
+
}
|
|
291
|
+
interface DrivingDetectorStatistics {
|
|
292
|
+
base: DrivingBaseDetectorStatistics;
|
|
293
|
+
longitudinal: DrivingLongitudinalWindowStatistics | null;
|
|
294
|
+
rapidLongitudinal: DrivingRapidLongitudinalStatistics | null;
|
|
295
|
+
turn: DrivingTurnDetectorStatistics | null;
|
|
296
|
+
}
|
|
250
297
|
type DrivingStatusDiagnosticEvent = {
|
|
251
298
|
type: 'sample_available';
|
|
252
299
|
atMs: number;
|
|
253
300
|
capturedAtMs: number;
|
|
301
|
+
frameSource: DrivingFrameSource;
|
|
302
|
+
frameAgeMs: number;
|
|
254
303
|
tuningVersion: string;
|
|
255
304
|
} | {
|
|
256
305
|
type: 'sample_unavailable';
|
|
@@ -266,7 +315,19 @@ type DrivingStatusDiagnosticEvent = {
|
|
|
266
315
|
rapidCandidate: 'RAPID_ACCELERATION' | 'SUDDEN_BRAKING' | null;
|
|
267
316
|
longitudinalCandidate: 'ACCELERATION' | 'BRAKING' | null;
|
|
268
317
|
turnCandidate: 'LEFT_TURN' | 'RIGHT_TURN' | null;
|
|
269
|
-
rejectionReason:
|
|
318
|
+
rejectionReason: DrivingDetectorRejectionReason;
|
|
319
|
+
frameSource: DrivingFrameSource;
|
|
320
|
+
frameAgeMs: number;
|
|
321
|
+
statistics: DrivingDetectorStatistics;
|
|
322
|
+
tuningVersion: string;
|
|
323
|
+
} | {
|
|
324
|
+
type: 'detector_transition';
|
|
325
|
+
atMs: number;
|
|
326
|
+
detector: DrivingDetectorName;
|
|
327
|
+
phase: DrivingDetectorTransitionPhase;
|
|
328
|
+
status: CarRunningStatus | null;
|
|
329
|
+
rejectionReason: DrivingDetectorRejectionReason;
|
|
330
|
+
statistics: DrivingDetectorStatistics;
|
|
270
331
|
tuningVersion: string;
|
|
271
332
|
} | {
|
|
272
333
|
type: 'candidate_changed';
|
|
@@ -373,4 +434,4 @@ interface DrivingExpressionPlayer {
|
|
|
373
434
|
}
|
|
374
435
|
declare const createDrivingExpressionPlayer: (container: HTMLElement, options: DrivingExpressionPlayerOptions) => DrivingExpressionPlayer;
|
|
375
436
|
//#endregion
|
|
376
|
-
export { AvailableSensorMetric, BATTERY_HEALTH_LABELS, BatteryCapacityValue, BatteryHealth, BatteryPlugged, BatteryStateValue, BatteryStatus, CAR_RUNNING_LABELS, CAR_RUNNING_STATUSES, CAR_RUNNING_STATUS_PRIORITY, CarRunningStatus, CompleteDrivingExpressionsConfig, DEFAULT_DRIVING_STATUS_TUNING, DrivingControllerSnapshot, DrivingExpressionMedia, DrivingExpressionPlayer, DrivingExpressionPlayerDiagnosticEvent, DrivingExpressionPlayerErrorCategory, DrivingExpressionPlayerOptions, DrivingExpressionPlayerSnapshot, DrivingExpressionResolutionError, DrivingExpressionsConfig, DrivingExpressionsConfigError, DrivingExpressionsConfigErrorCode, DrivingExpressionsConfigInput, DrivingMetricsCorrection, DrivingSensorProvider, DrivingSnapshotAnalysis, DrivingSnapshotCandidate, DrivingSnapshotMetrics, DrivingSnapshotRejectionReason, DrivingStatusChangeReason, DrivingStatusClock, DrivingStatusController, DrivingStatusControllerOptions, DrivingStatusDiagnosticEvent, DrivingStatusEvent, DrivingStatusTuning, DrivingStatusTuningOverrides, DrivingVehicleFrame, EmojiDrivingExpressionMedia, ImageDrivingExpressionMedia, LivePhotoDrivingExpressionMedia, MediaLibraryRef, OFFICIAL_DRIVING_EXPRESSIONS_CONFIG, OFFICIAL_DRIVING_EXPRESSION_FALLBACKS, OrientationValue, ParseDrivingExpressionsOptions, StructuredDrivingExpressionMedia, TgsDrivingExpressionMedia, UnavailableSensorMetric, UnifiedSensorInfoPayload, UnifiedSensorMetric, UnifiedSensorUnavailableReason, Vector3Value, VideoDrivingExpressionMedia, WaitForDrivingExpressionsConfigOptions, buildVehicleFrame, classifyDrivingSnapshot, createDrivingExpressionPlayer, createDrivingStatusController, extractDrivingSnapshotMetrics, isPublicDrivingMediaUrl, parseDrivingExpressionsConfig, readDrivingExpressionsConfig, resolveDrivingExpression, resolveDrivingExpressionStrict, resolveDrivingStatusTuning, tryParseDrivingExpressionsConfig, unifiedSensorInfo, waitForDrivingExpressionsConfig };
|
|
437
|
+
export { AvailableSensorMetric, BATTERY_HEALTH_LABELS, BatteryCapacityValue, BatteryHealth, BatteryPlugged, BatteryStateValue, BatteryStatus, CAR_RUNNING_LABELS, CAR_RUNNING_STATUSES, CAR_RUNNING_STATUS_PRIORITY, CarRunningStatus, CompleteDrivingExpressionsConfig, DEFAULT_DRIVING_STATUS_TUNING, DrivingBaseDetectorStatistics, DrivingControllerSnapshot, DrivingDetectorName, DrivingDetectorRejectionReason, DrivingDetectorStatistics, DrivingDetectorTransitionPhase, DrivingExpressionMedia, DrivingExpressionPlayer, DrivingExpressionPlayerDiagnosticEvent, DrivingExpressionPlayerErrorCategory, DrivingExpressionPlayerOptions, DrivingExpressionPlayerSnapshot, DrivingExpressionResolutionError, DrivingExpressionsConfig, DrivingExpressionsConfigError, DrivingExpressionsConfigErrorCode, DrivingExpressionsConfigInput, DrivingFrameSource, DrivingLongitudinalWindowStatistics, DrivingMetricsCorrection, DrivingRapidLongitudinalStatistics, DrivingSensorProvider, DrivingSnapshotAnalysis, DrivingSnapshotCandidate, DrivingSnapshotMetrics, DrivingSnapshotRejectionReason, DrivingStatusChangeReason, DrivingStatusClock, DrivingStatusController, DrivingStatusControllerOptions, DrivingStatusDiagnosticEvent, DrivingStatusEvent, DrivingStatusTuning, DrivingStatusTuningOverrides, DrivingTurnDetectorStatistics, DrivingVehicleFrame, EmojiDrivingExpressionMedia, ImageDrivingExpressionMedia, LivePhotoDrivingExpressionMedia, MediaLibraryRef, OFFICIAL_DRIVING_EXPRESSIONS_CONFIG, OFFICIAL_DRIVING_EXPRESSION_FALLBACKS, OrientationValue, ParseDrivingExpressionsOptions, StructuredDrivingExpressionMedia, TgsDrivingExpressionMedia, UnavailableSensorMetric, UnifiedSensorInfoPayload, UnifiedSensorMetric, UnifiedSensorUnavailableReason, Vector3Value, VideoDrivingExpressionMedia, WaitForDrivingExpressionsConfigOptions, buildVehicleFrame, classifyDrivingSnapshot, createDrivingExpressionPlayer, createDrivingStatusController, extractDrivingSnapshotMetrics, isPublicDrivingMediaUrl, parseDrivingExpressionsConfig, readDrivingExpressionsConfig, resolveDrivingExpression, resolveDrivingExpressionStrict, resolveDrivingStatusTuning, tryParseDrivingExpressionsConfig, unifiedSensorInfo, waitForDrivingExpressionsConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -91,6 +91,10 @@ const DEFAULT_DRIVING_STATUS_TUNING = Object.freeze({
|
|
|
91
91
|
turnConfirmationMs: 600,
|
|
92
92
|
turnIntegratedAngleThreshold: .12,
|
|
93
93
|
turnSignCoherence: .7,
|
|
94
|
+
turnYawDominanceThreshold: .6,
|
|
95
|
+
turnLateralEvidenceThreshold: .25,
|
|
96
|
+
turnVerticalNoiseFloor: .1,
|
|
97
|
+
turnVerticalBumpRatio: 1.25,
|
|
94
98
|
turnExitYawThreshold: .04,
|
|
95
99
|
turnExitConfirmationMs: 700,
|
|
96
100
|
turnClosureWindowMs: 1500
|
|
@@ -154,6 +158,9 @@ const resolveDrivingStatusTuning = (overrides = {}) => {
|
|
|
154
158
|
"turnYawThreshold",
|
|
155
159
|
"turnConfirmationMs",
|
|
156
160
|
"turnIntegratedAngleThreshold",
|
|
161
|
+
"turnLateralEvidenceThreshold",
|
|
162
|
+
"turnVerticalNoiseFloor",
|
|
163
|
+
"turnVerticalBumpRatio",
|
|
157
164
|
"turnExitYawThreshold",
|
|
158
165
|
"turnExitConfirmationMs",
|
|
159
166
|
"turnClosureWindowMs"
|
|
@@ -167,7 +174,8 @@ const resolveDrivingStatusTuning = (overrides = {}) => {
|
|
|
167
174
|
for (const field of [
|
|
168
175
|
"longitudinalSignCoherence",
|
|
169
176
|
"rapidLongitudinalSignCoherence",
|
|
170
|
-
"turnSignCoherence"
|
|
177
|
+
"turnSignCoherence",
|
|
178
|
+
"turnYawDominanceThreshold"
|
|
171
179
|
]) {
|
|
172
180
|
const value = tuning[field];
|
|
173
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`);
|
|
@@ -195,19 +203,22 @@ const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUN
|
|
|
195
203
|
const linearAcceleration = availableFreshVector(info.linearAcceleration, info.capturedAtMs, tuning.maxSampleAgeMs);
|
|
196
204
|
const gyroscope = availableFreshVector(info.gyroscope, info.capturedAtMs, tuning.maxSampleAgeMs);
|
|
197
205
|
const gravityMetric = availableFreshVector(info.gravity, info.capturedAtMs, tuning.maxSampleAgeMs);
|
|
198
|
-
|
|
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) {
|
|
199
211
|
dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned unavailable vectors", {
|
|
200
212
|
linearAcceleration,
|
|
201
213
|
gyroscope,
|
|
202
|
-
gravity
|
|
214
|
+
gravity
|
|
203
215
|
});
|
|
204
216
|
return null;
|
|
205
217
|
}
|
|
206
|
-
const
|
|
207
|
-
const frame = buildVehicleFrame(gravity, tuning.vehicleRightMinimumNorm);
|
|
218
|
+
const frame = buildVehicleFrame(gravity.value, tuning.vehicleRightMinimumNorm);
|
|
208
219
|
if (frame === null) {
|
|
209
220
|
dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned invalid vehicle frame", {
|
|
210
|
-
gravity,
|
|
221
|
+
gravity: gravity.value,
|
|
211
222
|
vehicleRightMinimumNorm: tuning.vehicleRightMinimumNorm
|
|
212
223
|
});
|
|
213
224
|
return null;
|
|
@@ -218,7 +229,7 @@ const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUN
|
|
|
218
229
|
capturedAtMs: info.capturedAtMs,
|
|
219
230
|
linearAccelerationSampledAtMs: linearAcceleration.sampledAtMs,
|
|
220
231
|
gyroscopeSampledAtMs: gyroscope.sampledAtMs,
|
|
221
|
-
gravitySampledAtMs:
|
|
232
|
+
gravitySampledAtMs: gravity.sampledAtMs,
|
|
222
233
|
longitudinal: dot(correctedLinear, frame.forward),
|
|
223
234
|
lateral: dot(correctedLinear, frame.right),
|
|
224
235
|
vertical: dot(correctedLinear, frame.up),
|
|
@@ -299,6 +310,19 @@ const availableFreshVector = (metric, capturedAtMs, maxSampleAgeMs) => {
|
|
|
299
310
|
sampledAtMs: metric.sampledAtMs
|
|
300
311
|
} : null;
|
|
301
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;
|
|
325
|
+
};
|
|
302
326
|
const isFresh = (sampledAtMs, capturedAtMs, maxSampleAgeMs) => {
|
|
303
327
|
if (sampledAtMs === null || !Number.isFinite(sampledAtMs)) return false;
|
|
304
328
|
const age = capturedAtMs - sampledAtMs;
|
|
@@ -1363,6 +1387,18 @@ var DefaultDrivingStatusController = class {
|
|
|
1363
1387
|
this.turnStatus = null;
|
|
1364
1388
|
this.turnQuietSinceMs = null;
|
|
1365
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
|
+
};
|
|
1366
1402
|
this.requestedRunning = false;
|
|
1367
1403
|
this.polling = false;
|
|
1368
1404
|
this.lifecycleSuspended = false;
|
|
@@ -1567,21 +1603,22 @@ var DefaultDrivingStatusController = class {
|
|
|
1567
1603
|
});
|
|
1568
1604
|
return;
|
|
1569
1605
|
}
|
|
1570
|
-
const gravity = this.
|
|
1606
|
+
const gravity = this.resolveGravityFrame(payload);
|
|
1571
1607
|
const metrics = gravity === null ? null : extractDrivingSnapshotMetrics(payload, this.tuning, {
|
|
1572
|
-
filteredGravity: gravity,
|
|
1608
|
+
filteredGravity: gravity.value,
|
|
1609
|
+
filteredGravitySampledAtMs: gravity.sampledAtMs,
|
|
1573
1610
|
linearAccelerationBias: this.linearAccelerationBias,
|
|
1574
1611
|
gyroscopeBias: this.gyroscopeBias
|
|
1575
1612
|
});
|
|
1576
1613
|
const accelerometerSample = extractFreshVectorMetric(payload.accelerometer, payload.capturedAtMs, this.tuning.maxSampleAgeMs);
|
|
1577
|
-
if (metrics === null || accelerometerSample === null) {
|
|
1614
|
+
if (gravity === null || metrics === null || accelerometerSample === null) {
|
|
1578
1615
|
dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "invalid or stale snapshot", {
|
|
1579
1616
|
metrics,
|
|
1580
1617
|
accelerometerSample
|
|
1581
1618
|
});
|
|
1582
1619
|
this.processUnavailable("invalid_or_stale_snapshot");
|
|
1583
1620
|
} else {
|
|
1584
|
-
this.processAvailable(metrics, accelerometerSample);
|
|
1621
|
+
this.processAvailable(metrics, accelerometerSample, gravity);
|
|
1585
1622
|
dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "processed sample", { capturedAtMs: metrics.capturedAtMs });
|
|
1586
1623
|
}
|
|
1587
1624
|
} catch (error) {
|
|
@@ -1600,21 +1637,44 @@ var DefaultDrivingStatusController = class {
|
|
|
1600
1637
|
});
|
|
1601
1638
|
}
|
|
1602
1639
|
}
|
|
1603
|
-
|
|
1640
|
+
resolveGravityFrame(info) {
|
|
1604
1641
|
const sample = extractFreshVectorMetric(info.gravity, info.capturedAtMs, this.tuning.maxSampleAgeMs);
|
|
1605
|
-
if (sample === null)
|
|
1606
|
-
|
|
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
|
+
};
|
|
1607
1656
|
if (this.filteredGravity === null || this.lastGravitySampledAtMs === null) this.filteredGravity = { ...sample.value };
|
|
1608
1657
|
else {
|
|
1609
|
-
if (sample.sampledAtMs < this.lastGravitySampledAtMs)
|
|
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
|
+
}
|
|
1610
1666
|
const elapsedMs = Math.max(0, sample.sampledAtMs - this.lastGravitySampledAtMs);
|
|
1611
1667
|
const alpha = 1 - Math.exp(-elapsedMs / this.tuning.gravityFilterTimeConstantMs);
|
|
1612
1668
|
this.filteredGravity = lerpVector(this.filteredGravity, sample.value, alpha);
|
|
1613
1669
|
}
|
|
1614
1670
|
this.lastGravitySampledAtMs = sample.sampledAtMs;
|
|
1615
|
-
return
|
|
1671
|
+
return {
|
|
1672
|
+
value: this.filteredGravity,
|
|
1673
|
+
sampledAtMs: sample.sampledAtMs,
|
|
1674
|
+
source: "live"
|
|
1675
|
+
};
|
|
1616
1676
|
}
|
|
1617
|
-
processAvailable(metrics, accelerometerSample) {
|
|
1677
|
+
processAvailable(metrics, accelerometerSample, gravity) {
|
|
1618
1678
|
dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processAvailable", "called", {
|
|
1619
1679
|
metrics,
|
|
1620
1680
|
accelerometerSample
|
|
@@ -1627,6 +1687,8 @@ var DefaultDrivingStatusController = class {
|
|
|
1627
1687
|
type: "sample_available",
|
|
1628
1688
|
atMs: now,
|
|
1629
1689
|
capturedAtMs: metrics.capturedAtMs,
|
|
1690
|
+
frameSource: gravity.source,
|
|
1691
|
+
frameAgeMs: metrics.capturedAtMs - gravity.sampledAtMs,
|
|
1630
1692
|
tuningVersion: this.tuning.version
|
|
1631
1693
|
});
|
|
1632
1694
|
const addedLongitudinal = pushUniqueMetric(this.longitudinalWindow, metrics, "linearAccelerationSampledAtMs", Math.max(this.tuning.longitudinalWindowMs, this.tuning.rapidLongitudinalWindowMs));
|
|
@@ -1637,6 +1699,10 @@ var DefaultDrivingStatusController = class {
|
|
|
1637
1699
|
return;
|
|
1638
1700
|
}
|
|
1639
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;
|
|
1640
1706
|
const baseCondition = this.updateBaseDetector(motionMad, metrics, accelerometerSample.sampledAtMs, addedMotion);
|
|
1641
1707
|
const longitudinalEvidence = detectLongitudinalEvidence(this.longitudinalWindow, this.tuning);
|
|
1642
1708
|
if (addedLongitudinal) this.updateLongitudinalDetector(longitudinalEvidence, metrics.linearAccelerationSampledAtMs);
|
|
@@ -1648,6 +1714,27 @@ var DefaultDrivingStatusController = class {
|
|
|
1648
1714
|
this.stationaryConfirmed = false;
|
|
1649
1715
|
}
|
|
1650
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);
|
|
1651
1738
|
const pendingCandidate = instantaneousCandidate(metrics, this.tuning);
|
|
1652
1739
|
this.setCandidate(pendingCandidate === this.status ? null : pendingCandidate, observationTime(metrics));
|
|
1653
1740
|
this.emitDiagnostic({
|
|
@@ -1658,7 +1745,10 @@ var DefaultDrivingStatusController = class {
|
|
|
1658
1745
|
rapidCandidate: longitudinalEvidence.rapidCandidate,
|
|
1659
1746
|
longitudinalCandidate: longitudinalEvidence.normalCandidate,
|
|
1660
1747
|
turnCandidate: turnEvidence.candidate,
|
|
1661
|
-
rejectionReason: longitudinalEvidence.rejectionReason,
|
|
1748
|
+
rejectionReason: longitudinalEvidence.rejectionReason ?? turnEvidence.rejectionReason,
|
|
1749
|
+
frameSource: gravity.source,
|
|
1750
|
+
frameAgeMs: metrics.capturedAtMs - gravity.sampledAtMs,
|
|
1751
|
+
statistics,
|
|
1662
1752
|
tuningVersion: this.tuning.version
|
|
1663
1753
|
});
|
|
1664
1754
|
const hasAction = this.urgentStatus !== null || this.turnStatus !== null || this.normalLongitudinalStatus !== null;
|
|
@@ -1766,6 +1856,31 @@ var DefaultDrivingStatusController = class {
|
|
|
1766
1856
|
}
|
|
1767
1857
|
this.lastBiasSampledAtMs = sampledAtMs;
|
|
1768
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
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1769
1884
|
commitSelectedStatus(now) {
|
|
1770
1885
|
const selected = this.urgentStatus ?? this.turnStatus ?? this.normalLongitudinalStatus ?? this.baseStatus;
|
|
1771
1886
|
this.commit(selected, now, "candidate_confirmed");
|
|
@@ -1868,6 +1983,10 @@ var DefaultDrivingStatusController = class {
|
|
|
1868
1983
|
this.turnStatus = null;
|
|
1869
1984
|
this.turnQuietSinceMs = null;
|
|
1870
1985
|
this.oppositeTurnSinceMs = null;
|
|
1986
|
+
for (const detector of Object.keys(this.detectorCandidates)) {
|
|
1987
|
+
this.detectorCandidates[detector] = null;
|
|
1988
|
+
this.detectorRejections[detector] = null;
|
|
1989
|
+
}
|
|
1871
1990
|
}
|
|
1872
1991
|
resetTransientRecognition() {
|
|
1873
1992
|
this.clearRecognitionWindows();
|
|
@@ -1899,36 +2018,57 @@ const detectLongitudinalEvidence = (window, tuning) => {
|
|
|
1899
2018
|
const normalWindow = recentMetrics(window, tuning.longitudinalWindowMs, "linearAccelerationSampledAtMs");
|
|
1900
2019
|
let rapidCandidate = null;
|
|
1901
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
|
+
};
|
|
1902
2040
|
if (windowCovers(rapidWindow, tuning.rapidLongitudinalMinimumSamples, tuning.rapidLongitudinalWindowMs, "linearAccelerationSampledAtMs")) {
|
|
1903
|
-
|
|
1904
|
-
const direction = Math.sign(trimmedMean(values));
|
|
1905
|
-
const coherence = signCoherence(values, direction, tuning.longitudinalExitThreshold);
|
|
1906
|
-
const peak = Math.max(...values.map(Math.abs));
|
|
1907
|
-
const mean = Math.abs(trimmedMean(values));
|
|
1908
|
-
const longitudinalRms = rootMeanSquare(values);
|
|
1909
|
-
const verticalRms = rootMeanSquare(rapidWindow.map((sample) => sample.vertical));
|
|
1910
|
-
const reversalWindow = recentMetrics(window, tuning.longitudinalReversalWindowMs, "linearAccelerationSampledAtMs");
|
|
1911
|
-
const hasReversal = containsBothSigns(reversalWindow.map((sample) => sample.longitudinal), tuning.longitudinalThreshold);
|
|
1912
|
-
if (peak >= tuning.rapidLongitudinalPeakThreshold && mean >= tuning.rapidLongitudinalMeanThreshold) {
|
|
2041
|
+
if (rapidPeak >= tuning.rapidLongitudinalPeakThreshold && Math.abs(rapidMean) >= tuning.rapidLongitudinalMeanThreshold) {
|
|
1913
2042
|
if (verticalRms > longitudinalRms * tuning.verticalBumpRatio) rejectionReason = "vertical_bump";
|
|
1914
2043
|
else if (hasReversal) rejectionReason = "longitudinal_reversal";
|
|
1915
|
-
else if (
|
|
2044
|
+
else if (rapidDirection !== 0 && rapidCoherence >= tuning.rapidLongitudinalSignCoherence) rapidCandidate = rapidDirection > 0 ? "RAPID_ACCELERATION" : "SUDDEN_BRAKING";
|
|
1916
2045
|
}
|
|
1917
2046
|
}
|
|
1918
2047
|
let normalCandidate = null;
|
|
1919
2048
|
let retainedNormal = false;
|
|
2049
|
+
let normalStatistics = null;
|
|
1920
2050
|
if (normalWindow.length > 0) {
|
|
1921
2051
|
const values = normalWindow.map((sample) => sample.longitudinal);
|
|
1922
2052
|
const mean = trimmedMean(values);
|
|
1923
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
|
+
};
|
|
1924
2062
|
retainedNormal = Math.abs(mean) >= tuning.longitudinalExitThreshold;
|
|
1925
|
-
if (windowCovers(normalWindow, tuning.longitudinalMinimumSamples, tuning.longitudinalWindowMs, "linearAccelerationSampledAtMs") && Math.abs(mean) >= tuning.longitudinalThreshold &&
|
|
2063
|
+
if (windowCovers(normalWindow, tuning.longitudinalMinimumSamples, tuning.longitudinalWindowMs, "linearAccelerationSampledAtMs") && Math.abs(mean) >= tuning.longitudinalThreshold && coherence >= tuning.longitudinalSignCoherence) normalCandidate = direction > 0 ? "ACCELERATION" : "BRAKING";
|
|
1926
2064
|
}
|
|
1927
2065
|
return {
|
|
1928
2066
|
rapidCandidate,
|
|
1929
2067
|
normalCandidate,
|
|
1930
2068
|
retainedNormal,
|
|
1931
|
-
rejectionReason
|
|
2069
|
+
rejectionReason,
|
|
2070
|
+
normalStatistics,
|
|
2071
|
+
rapidStatistics
|
|
1932
2072
|
};
|
|
1933
2073
|
};
|
|
1934
2074
|
const detectTurnEvidence = (window, tuning) => {
|
|
@@ -1936,20 +2076,47 @@ const detectTurnEvidence = (window, tuning) => {
|
|
|
1936
2076
|
if (!latest) return {
|
|
1937
2077
|
candidate: null,
|
|
1938
2078
|
latestYaw: 0,
|
|
1939
|
-
sampledAtMs: 0
|
|
2079
|
+
sampledAtMs: 0,
|
|
2080
|
+
rejectionReason: null,
|
|
2081
|
+
statistics: null
|
|
1940
2082
|
};
|
|
1941
2083
|
const strongWindow = recentMetrics(window, tuning.turnConfirmationMs, "gyroscopeSampledAtMs");
|
|
1942
2084
|
const strongMean = trimmedMean(strongWindow.map((sample) => sample.yaw));
|
|
1943
2085
|
const strongDirection = Math.sign(strongMean);
|
|
1944
|
-
const
|
|
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;
|
|
1945
2088
|
const signedAngle = integrateYaw(window);
|
|
1946
2089
|
const gentleDirection = Math.sign(signedAngle);
|
|
1947
|
-
const
|
|
1948
|
-
const
|
|
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;
|
|
1949
2102
|
return {
|
|
1950
2103
|
candidate: direction === 0 ? null : direction > 0 ? "LEFT_TURN" : "RIGHT_TURN",
|
|
1951
2104
|
latestYaw: latest.yaw,
|
|
1952
|
-
sampledAtMs: latest.gyroscopeSampledAtMs
|
|
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
|
+
}
|
|
1953
2120
|
};
|
|
1954
2121
|
};
|
|
1955
2122
|
const instantaneousCandidate = (metrics, tuning) => {
|
|
@@ -1990,6 +2157,12 @@ const signCoherence = (values, direction, deadband) => {
|
|
|
1990
2157
|
};
|
|
1991
2158
|
const containsBothSigns = (values, threshold) => values.some((value) => value >= threshold) && values.some((value) => value <= -threshold);
|
|
1992
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
|
+
};
|
|
1993
2166
|
const calculateCombinedAxisMad = (samples, minimumSamples) => {
|
|
1994
2167
|
if (samples.length < minimumSamples) return null;
|
|
1995
2168
|
const medianX = median(samples.map((sample) => sample.value.x));
|
|
@@ -2000,6 +2173,11 @@ const calculateCombinedAxisMad = (samples, minimumSamples) => {
|
|
|
2000
2173
|
const madZ = median(samples.map((sample) => Math.abs(sample.value.z - medianZ)));
|
|
2001
2174
|
return Math.hypot(madX, madY, madZ);
|
|
2002
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
|
+
};
|
|
2003
2181
|
const median = (values) => {
|
|
2004
2182
|
if (values.length === 0) return 0;
|
|
2005
2183
|
const sorted = [...values].sort((left, right) => left - right);
|