@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 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: 'vertical_bump' | 'longitudinal_reversal' | null;
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
- if (linearAcceleration === null || gyroscope === null || gravityMetric === null) {
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: gravityMetric
214
+ gravity
203
215
  });
204
216
  return null;
205
217
  }
206
- const gravity = correction.filteredGravity ?? gravityMetric.value;
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: gravityMetric.sampledAtMs,
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.updateFilteredGravity(payload);
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
- updateFilteredGravity(info) {
1640
+ resolveGravityFrame(info) {
1604
1641
  const sample = extractFreshVectorMetric(info.gravity, info.capturedAtMs, this.tuning.maxSampleAgeMs);
1605
- if (sample === null) return null;
1606
- if (sample.sampledAtMs === this.lastGravitySampledAtMs) return this.filteredGravity;
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) return this.filteredGravity;
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 this.filteredGravity;
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
- const values = rapidWindow.map((sample) => sample.longitudinal);
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 (direction !== 0 && coherence >= tuning.rapidLongitudinalSignCoherence) rapidCandidate = direction > 0 ? "RAPID_ACCELERATION" : "SUDDEN_BRAKING";
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 && signCoherence(values, direction, tuning.longitudinalExitThreshold) >= tuning.longitudinalSignCoherence) normalCandidate = direction > 0 ? "ACCELERATION" : "BRAKING";
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 strong = windowCovers(strongWindow, tuning.turnMinimumSamples, tuning.turnConfirmationMs, "gyroscopeSampledAtMs") && Math.abs(strongMean) >= tuning.turnYawThreshold && signCoherence(strongWindow.map((sample) => sample.yaw), strongDirection, tuning.turnExitYawThreshold) >= tuning.turnSignCoherence;
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 gentle = window.length >= tuning.turnMinimumSamples && Math.abs(signedAngle) >= tuning.turnIntegratedAngleThreshold && signCoherence(window.map((sample) => sample.yaw), gentleDirection, tuning.turnExitYawThreshold) >= tuning.turnSignCoherence;
1948
- const direction = strong ? strongDirection : gentle ? gentleDirection : 0;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziztechnology/dial-library",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "private": false,
5
5
  "description": "Tooony 表盘工具函数、方法",
6
6
  "files": [