@ziztechnology/dial-library 0.0.13 → 0.0.14

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
@@ -3,7 +3,7 @@
3
3
  `@ziztechnology/dial-library` 为 Toooony 表盘提供统一的设备信息和行车状态 API。你可以用它读取传感器,也可以直接订阅经过平滑、确认和回退处理后的行车状态。
4
4
 
5
5
  > [!NOTE]
6
- > SDK 最低支持 Toooony Runtime `v1.5.20`。行车状态传感器 Bridge 仅向审核通过的 `PACKAGED_H5` 开放。第三方表盘应使用本 SDK,不要直接依赖 Runtime 注入到 `window` 上的内部方法。
6
+ > SDK 最低支持 Toooony Runtime `v1.5.22`。行车状态传感器 Bridge 仅向审核通过的 `PACKAGED_H5` 开放。第三方表盘应使用本 SDK,不要直接依赖 Runtime 注入到 `window` 上的内部方法。
7
7
 
8
8
  ## 安装
9
9
 
@@ -223,9 +223,9 @@ unsubscribe();
223
223
  controller.destroy();
224
224
  ```
225
225
 
226
- 控制器默认每 200ms 读取一次,并保证上一次读取结束后才开始下一次。所有确认都使用传感器时间而不是轮询次数;相同 `sampledAtMs` 的重复数据不会推进窗口。
226
+ 控制器默认以 200ms 为目标节拍读取,并保证同一时间只有一次 Bridge 读取。下一轮会扣除上一轮 Bridge 耗时;Bridge 本身超过 200ms 时则在返回后立即开始下一轮,避免再叠加固定等待。所有确认都使用传感器时间而不是轮询次数;相同 `sampledAtMs` 的重复数据不会推进窗口。正常 5Hz 采样仍需满足配置的 5/4/3 个样本,较慢的 Bridge 可以用至少 3 个独立观测覆盖同一确认时长;相邻有效观测间隔达到 1 秒时会重新建立动作窗口,不能跨越长数据空洞确认。
227
227
 
228
- 静止和匀速使用 accelerometer 的 1 秒三轴 MAD 稳健波动窗口,至少需要 5 个唯一样本。运动量不高于 `0.12 m/s²` 且陀螺仪足够稳定,连续 3 秒才进入 `STOPPED`;运动量达到 `0.18 m/s²` 或陀螺仪出现持续运动,连续 0.6 秒进入 `STEADY_DRIVING`。中间区间保持原基础状态。
228
+ 静止和匀速使用 accelerometer 的 1 秒三轴 MAD 稳健波动窗口;正常 5Hz 采样至少需要 5 个唯一样本,慢 Bridge 使用相同的时间覆盖规则。运动量不高于 `0.12 m/s²` 且陀螺仪足够稳定,连续 1.5 秒才进入 `STOPPED`;运动量达到 `0.18 m/s²` 或陀螺仪出现持续运动后,从当前运动样本开始连续 0.6 秒才进入 `STEADY_DRIVING`。中间区间保持原基础状态,停车时一次短促晃动不会立即切回行驶。
229
229
 
230
230
  普通加速和刹车要求 0.8 秒内至少 4/5 的样本方向一致;急加速和急刹要求峰值、0.6 秒持续均值及方向一致性同时成立,并拒绝垂直能量占主导或短时间反号的颠簸。车辆刚出现方向稳定的物理偏航后,强转向路径要求 0.4 秒内至少 3 个唯一样本,窗口均值达到 `0.08 rad/s`;在默认轮询频率下通常会在 0.4–0.6 秒内触发。转向也可以由累计转角触发,并锁存到回正结束。算法不会预测车辆尚未产生转动时的驾驶意图。最终优先级为:急加减速、已锁存转向、普通加减速、基础状态。
231
231
 
@@ -261,7 +261,7 @@ const controller = createDrivingStatusController({
261
261
  motionMinimumSamples: 6,
262
262
  drivingMotionThreshold: 0.2,
263
263
  stoppedMotionThreshold: 0.11,
264
- stoppedConfirmationMs: 3_000,
264
+ stoppedConfirmationMs: 1_500,
265
265
  longitudinalThreshold: 0.75,
266
266
  turnYawDominanceThreshold: 0.65,
267
267
  turnLateralEvidenceThreshold: 0.3,
@@ -277,7 +277,7 @@ const controller = createDrivingStatusController({
277
277
 
278
278
  检测器状态变化时还会发送 `detector_transition`,其 `phase` 为 `candidate_entered`、`candidate_rejected`、`candidate_confirmed` 或 `candidate_exited`。该事件携带当时的完整 `statistics`,可直接用于真车回放调参;同一状态不会逐轮询重复发送。
279
279
 
280
- 惯性传感器无法严格区分“完全静止”和“没有任何振动的理想匀速直线运动”。默认算法使用稳健振动统计、陀螺仪扰动和 5 秒确认近似判断,修改阈值后应使用完整路测过程回放验证。
280
+ 惯性传感器无法严格区分“完全静止”和“没有任何振动的理想匀速直线运动”。默认算法使用稳健振动统计、陀螺仪扰动和 1.5 秒确认近似判断,修改阈值后应使用完整路测过程回放验证。
281
281
 
282
282
  ### 驱动行车表情
283
283
 
@@ -328,7 +328,7 @@ TGS 使用 SDK 内置的 SVG light 播放器,并且只在首次显示 TGS 素
328
328
 
329
329
  ### 传感器不可用时回退到静止
330
330
 
331
- 控制器遇到一次读取失败、非法快照或过期样本时,会暂时保持最后一个已提交状态,避免页面闪烁。连续不可用达到 3 秒后,控制器才提交 `STOPPED`:
331
+ 控制器遇到一次读取失败、非法快照或过期样本时,会暂时保持最后一个已提交状态和仍在有效时间范围内的识别窗口,避免页面闪烁或丢失转向、刹车证据。连续不可用达到 3 秒后,控制器才清空窗口并提交 `STOPPED`:
332
332
 
333
333
  ```ts
334
334
  const controller = createDrivingStatusController({
@@ -347,7 +347,7 @@ controller.subscribe((event) => {
347
347
  });
348
348
  ```
349
349
 
350
- 传感器恢复后,控制器会清除不可用状态并重新确认候选,不会立即把单个样本提交给 UI。`unavailableFallbackMs` 必须是大于 0 的有限数值。
350
+ 传感器恢复后,控制器会清除不可用状态;短缺样前后的时间窗仍需满足独立观测数、持续时间和小于 1 秒的相邻间隔约束,不会把跨越长空洞的数据或单个样本提交给 UI。`unavailableFallbackMs` 必须是大于 0 的有限数值。
351
351
 
352
352
  ### 素材失败时使用显式回退
353
353
 
@@ -398,7 +398,7 @@ const player = createDrivingExpressionPlayer(container, {
398
398
 
399
399
  ### 配置本身无效时
400
400
 
401
- `readDrivingExpressionsConfig()` 优先读取 Runtime 注入的 `__TOOOONY_DRIVING_EXPRESSIONS__`,并兼容旧的 `__TOOOONY_FACE_CONFIG__` 路径。配置不存在或无效时返回 `null`,不会替你补齐默认值:
401
+ `readDrivingExpressionsConfig()` 只读取 Runtime 注入的 `__TOOOONY_DRIVING_EXPRESSIONS__`。配置不存在或无效时返回 `null`,不会替你补齐默认值:
402
402
 
403
403
  ```ts
404
404
  const config = readDrivingExpressionsConfig();
@@ -408,7 +408,7 @@ if (!config) {
408
408
  }
409
409
  ```
410
410
 
411
- 正常的 PACKAGED_H5 Runtime 会在 document-start 阶段完成配置注入,因此优先使用上述同步读取。若表盘还需兼容旧 Runtime 或异常的晚注入时序,可以进行一次有上限的异步等待:
411
+ Runtime v1.5.22 及以上的 PACKAGED_H5 会在 document-start 阶段完成配置注入,因此优先使用上述同步读取。若需要容忍异常的晚注入时序,可以进行一次有上限的异步等待:
412
412
 
413
413
  ```ts
414
414
  import { waitForDrivingExpressionsConfig } from '@ziztechnology/dial-library';
package/dist/index.d.mts CHANGED
@@ -4,10 +4,6 @@ type BatteryStatus = 'CHARGING' | 'DISCHARGING' | 'FULL' | 'NOT_CHARGING' | 'UNK
4
4
  type BatteryPlugged = 'AC' | 'USB' | 'WIRELESS' | 'DOCK' | 'NONE';
5
5
  type BatteryHealth = 'GOOD' | 'OVERHEAT' | 'DEAD' | 'OVER_VOLTAGE' | 'UNSPECIFIED_FAILURE' | 'COLD' | 'UNKNOWN';
6
6
  declare const BATTERY_HEALTH_LABELS: Record<BatteryHealth, string>;
7
- type CarRunningStatus = 'STOPPED' | 'STEADY_DRIVING' | 'ACCELERATION' | 'RAPID_ACCELERATION' | 'BRAKING' | 'SUDDEN_BRAKING' | 'LEFT_TURN' | 'RIGHT_TURN';
8
- declare const CAR_RUNNING_LABELS: Record<CarRunningStatus, string>;
9
- declare const CAR_RUNNING_STATUSES: readonly ["STOPPED", "STEADY_DRIVING", "ACCELERATION", "RAPID_ACCELERATION", "BRAKING", "SUDDEN_BRAKING", "LEFT_TURN", "RIGHT_TURN"];
10
- declare const CAR_RUNNING_STATUS_PRIORITY: Readonly<Record<CarRunningStatus, number>>;
11
7
  //#endregion
12
8
  //#region src/sensor/unified_info.d.ts
13
9
  interface Vector3Value {
@@ -65,7 +61,13 @@ interface UnifiedSensorInfoPayload {
65
61
  }
66
62
  declare const unifiedSensorInfo: () => Promise<UnifiedSensorInfoPayload>;
67
63
  //#endregion
68
- //#region src/sensor/check_status.d.ts
64
+ //#region src/driving_expression/constants.d.ts
65
+ type CarRunningStatus = 'STOPPED' | 'STEADY_DRIVING' | 'ACCELERATION' | 'RAPID_ACCELERATION' | 'BRAKING' | 'SUDDEN_BRAKING' | 'LEFT_TURN' | 'RIGHT_TURN';
66
+ declare const CAR_RUNNING_LABELS: Record<CarRunningStatus, string>;
67
+ declare const CAR_RUNNING_STATUSES: readonly ["STOPPED", "STEADY_DRIVING", "ACCELERATION", "RAPID_ACCELERATION", "BRAKING", "SUDDEN_BRAKING", "LEFT_TURN", "RIGHT_TURN"];
68
+ declare const CAR_RUNNING_STATUS_PRIORITY: Readonly<Record<CarRunningStatus, number>>;
69
+ //#endregion
70
+ //#region src/driving_expression/check_status.d.ts
69
71
  interface DrivingStatusTuning {
70
72
  version: string;
71
73
  maxSampleAgeMs: number;
@@ -155,7 +157,7 @@ declare const resolveDrivingStatusTuning: (overrides?: DrivingStatusTuningOverri
155
157
  declare const extractDrivingSnapshotMetrics: (info: UnifiedSensorInfoPayload, tuning?: Pick<DrivingStatusTuning, "maxSampleAgeMs" | "vehicleRightMinimumNorm">, correction?: DrivingMetricsCorrection) => DrivingSnapshotMetrics | null;
156
158
  declare const buildVehicleFrame: (gravity: Vector3Value, rightMinimumNorm?: number) => DrivingVehicleFrame | null;
157
159
  //#endregion
158
- //#region src/sensor/driving_expressions.d.ts
160
+ //#region src/driving_expression/driving_expressions.d.ts
159
161
  interface MediaLibraryRef {
160
162
  mediaAssetId?: number;
161
163
  }
@@ -228,7 +230,7 @@ interface WaitForDrivingExpressionsConfigOptions {
228
230
  declare const parseDrivingExpressionsConfig: (raw: unknown, options?: ParseDrivingExpressionsOptions) => CompleteDrivingExpressionsConfig;
229
231
  declare const tryParseDrivingExpressionsConfig: (raw: unknown, options?: ParseDrivingExpressionsOptions) => CompleteDrivingExpressionsConfig | null;
230
232
  /**
231
- * Reads Runtime's typed injection first, then the legacy face-config value.
233
+ * Reads Runtime's typed driving-expressions injection.
232
234
  * It never installs official fallbacks implicitly.
233
235
  */
234
236
  declare const readDrivingExpressionsConfig: (raw?: unknown, options?: ParseDrivingExpressionsOptions) => CompleteDrivingExpressionsConfig | null;
@@ -243,7 +245,7 @@ declare const resolveDrivingExpression: (config: DrivingExpressionsConfigInput,
243
245
  declare const resolveDrivingExpressionStrict: (config: DrivingExpressionsConfigInput, status: CarRunningStatus, fallbacks?: Partial<Readonly<Record<CarRunningStatus, StructuredDrivingExpressionMedia>>>) => DrivingExpressionMedia;
244
246
  declare const isPublicDrivingMediaUrl: (value: string) => boolean;
245
247
  //#endregion
246
- //#region src/sensor/driving_status_controller.d.ts
248
+ //#region src/driving_expression/driving_status_controller.d.ts
247
249
  type DrivingStatusChangeReason = 'candidate_confirmed' | 'unavailable_fallback';
248
250
  interface DrivingStatusEvent {
249
251
  type: 'status_changed';
@@ -386,7 +388,7 @@ interface DrivingStatusController {
386
388
  }
387
389
  declare const createDrivingStatusController: (options?: DrivingStatusControllerOptions) => DrivingStatusController;
388
390
  //#endregion
389
- //#region src/sensor/driving_expression_player.d.ts
391
+ //#region src/driving_expression/driving_expression_player.d.ts
390
392
  type DrivingExpressionPlayerErrorCategory = 'invalid_config' | 'network' | 'timeout' | 'decode' | 'unsupported';
391
393
  type DrivingExpressionPlayerDiagnosticEvent = {
392
394
  type: 'media_ready';
package/dist/index.mjs CHANGED
@@ -43,6 +43,67 @@ const dialSdkDebugWarn = (moduleName, scope, phase, data) => {
43
43
  } catch {}
44
44
  };
45
45
  //#endregion
46
+ //#region src/sensor/unified_info.ts
47
+ const DEBUG_MODULE$5 = "unified_info.ts";
48
+ const unifiedSensorInfo = () => {
49
+ dialSdkDebugWarn(DEBUG_MODULE$5, "unifiedSensorInfo", "called");
50
+ try {
51
+ return window.unifiedSensorInfo().then((payload) => {
52
+ dialSdkDebugWarn(DEBUG_MODULE$5, "unifiedSensorInfo", "resolved", { payload });
53
+ return payload;
54
+ }, (error) => {
55
+ dialSdkDebugWarn(DEBUG_MODULE$5, "unifiedSensorInfo", "rejected", error);
56
+ throw error;
57
+ });
58
+ } catch (error) {
59
+ dialSdkDebugWarn(DEBUG_MODULE$5, "unifiedSensorInfo", "threw", error);
60
+ throw error;
61
+ }
62
+ };
63
+ //#endregion
64
+ //#region src/sensor/constants.ts
65
+ const BATTERY_HEALTH_LABELS = {
66
+ GOOD: "良好",
67
+ OVERHEAT: "过热",
68
+ DEAD: "失效",
69
+ OVER_VOLTAGE: "电压过高",
70
+ UNSPECIFIED_FAILURE: "异常",
71
+ COLD: "温度过低",
72
+ UNKNOWN: "未知"
73
+ };
74
+ //#endregion
75
+ //#region src/driving_expression/constants.ts
76
+ const CAR_RUNNING_LABELS = {
77
+ STOPPED: "静止",
78
+ STEADY_DRIVING: "匀速行驶",
79
+ SUDDEN_BRAKING: "急刹车",
80
+ BRAKING: "刹车",
81
+ LEFT_TURN: "左转",
82
+ RIGHT_TURN: "右转",
83
+ ACCELERATION: "加速",
84
+ RAPID_ACCELERATION: "急加速"
85
+ };
86
+ const CAR_RUNNING_STATUSES = [
87
+ "STOPPED",
88
+ "STEADY_DRIVING",
89
+ "ACCELERATION",
90
+ "RAPID_ACCELERATION",
91
+ "BRAKING",
92
+ "SUDDEN_BRAKING",
93
+ "LEFT_TURN",
94
+ "RIGHT_TURN"
95
+ ];
96
+ const CAR_RUNNING_STATUS_PRIORITY = {
97
+ STOPPED: 0,
98
+ STEADY_DRIVING: 0,
99
+ ACCELERATION: 1,
100
+ BRAKING: 2,
101
+ LEFT_TURN: 3,
102
+ RIGHT_TURN: 3,
103
+ RAPID_ACCELERATION: 4,
104
+ SUDDEN_BRAKING: 5
105
+ };
106
+ //#endregion
46
107
  //#region src/helper/sensor.ts
47
108
  const extractFreshVector = (metric, capturedAtMs, maxSampleAgeMs) => {
48
109
  if (!metric?.available || !Number.isFinite(capturedAtMs) || !Number.isFinite(metric.sampledAtMs)) return null;
@@ -82,8 +143,8 @@ const isRecord = (value, debug) => {
82
143
  return result;
83
144
  };
84
145
  //#endregion
85
- //#region src/sensor/check_status.ts
86
- const DEBUG_MODULE$5 = "check_status.ts";
146
+ //#region src/driving_expression/check_status.ts
147
+ const DEBUG_MODULE$4 = "check_status.ts";
87
148
  const DEVICE_RIGHT = {
88
149
  x: 1,
89
150
  y: 0,
@@ -95,7 +156,7 @@ const ZERO_VECTOR$1 = {
95
156
  z: 0
96
157
  };
97
158
  const DEFAULT_DRIVING_STATUS_TUNING = Object.freeze({
98
- version: "driving-status-calibrated-2026-07-17",
159
+ version: "driving-status-calibrated-2026-07-17-r2",
99
160
  maxSampleAgeMs: 1e3,
100
161
  gravityFilterTimeConstantMs: 1e3,
101
162
  biasFilterTimeConstantMs: 1e4,
@@ -108,7 +169,7 @@ const DEFAULT_DRIVING_STATUS_TUNING = Object.freeze({
108
169
  stoppedRawGyroThreshold: .065,
109
170
  drivingGyroThreshold: .04,
110
171
  drivingRawGyroThreshold: .08,
111
- stoppedConfirmationMs: 3e3,
172
+ stoppedConfirmationMs: 1500,
112
173
  drivingConfirmationMs: 600,
113
174
  longitudinalWindowMs: 800,
114
175
  longitudinalMinimumSamples: 5,
@@ -139,14 +200,14 @@ const DEFAULT_DRIVING_STATUS_TUNING = Object.freeze({
139
200
  turnClosureWindowMs: 1500
140
201
  });
141
202
  const classifyDrivingSnapshot = (info, tuningOverrides) => {
142
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingSnapshot", "called", {
203
+ dialSdkDebugWarn(DEBUG_MODULE$4, "classifyDrivingSnapshot", "called", {
143
204
  info,
144
205
  tuningOverrides
145
206
  });
146
207
  const tuning = resolveDrivingStatusTuning(tuningOverrides);
147
208
  const metrics = extractDrivingSnapshotMetrics(info, tuning);
148
209
  if (metrics === null) {
149
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingSnapshot", "returned", { result: null });
210
+ dialSdkDebugWarn(DEBUG_MODULE$4, "classifyDrivingSnapshot", "returned", { result: null });
150
211
  return null;
151
212
  }
152
213
  const { candidateStatus, rejectionReason } = classifyInstantaneousEvidence(metrics, tuning);
@@ -160,11 +221,11 @@ const classifyDrivingSnapshot = (info, tuningOverrides) => {
160
221
  maximumSampleAgeMs: Math.max(info.capturedAtMs - metrics.linearAccelerationSampledAtMs, info.capturedAtMs - metrics.gyroscopeSampledAtMs, info.capturedAtMs - metrics.gravitySampledAtMs)
161
222
  }
162
223
  };
163
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingSnapshot", "returned", { result });
224
+ dialSdkDebugWarn(DEBUG_MODULE$4, "classifyDrivingSnapshot", "returned", { result });
164
225
  return result;
165
226
  };
166
227
  const resolveDrivingStatusTuning = (overrides = {}) => {
167
- dialSdkDebugWarn(DEBUG_MODULE$5, "resolveDrivingStatusTuning", "called", { overrides });
228
+ dialSdkDebugWarn(DEBUG_MODULE$4, "resolveDrivingStatusTuning", "called", { overrides });
168
229
  const tuning = {
169
230
  ...DEFAULT_DRIVING_STATUS_TUNING,
170
231
  ...overrides
@@ -204,7 +265,7 @@ const resolveDrivingStatusTuning = (overrides = {}) => {
204
265
  "turnExitConfirmationMs",
205
266
  "turnClosureWindowMs"
206
267
  ]) requirePositiveFinite(field, tuning[field], {
207
- moduleName: DEBUG_MODULE$5,
268
+ moduleName: DEBUG_MODULE$4,
208
269
  scope: "assertPositiveFinite"
209
270
  });
210
271
  for (const field of [
@@ -229,17 +290,17 @@ const resolveDrivingStatusTuning = (overrides = {}) => {
229
290
  if (tuning.rapidLongitudinalMeanThreshold <= tuning.longitudinalThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "rapidLongitudinalMeanThreshold must be greater than longitudinalThreshold");
230
291
  if (tuning.rapidLongitudinalPeakThreshold < tuning.rapidLongitudinalMeanThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "rapidLongitudinalPeakThreshold must be greater than or equal to rapidLongitudinalMeanThreshold");
231
292
  if (tuning.turnYawThreshold <= tuning.turnExitYawThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "turnYawThreshold must be greater than turnExitYawThreshold");
232
- dialSdkDebugWarn(DEBUG_MODULE$5, "resolveDrivingStatusTuning", "returned", { tuning });
293
+ dialSdkDebugWarn(DEBUG_MODULE$4, "resolveDrivingStatusTuning", "returned", { tuning });
233
294
  return tuning;
234
295
  };
235
296
  const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUNING, correction = {}) => {
236
- dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "called", {
297
+ dialSdkDebugWarn(DEBUG_MODULE$4, "extractDrivingSnapshotMetrics", "called", {
237
298
  info,
238
299
  tuning,
239
300
  correction
240
301
  });
241
302
  if (!info || !Number.isFinite(info.capturedAtMs)) {
242
- dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned invalid capture time", { info });
303
+ dialSdkDebugWarn(DEBUG_MODULE$4, "extractDrivingSnapshotMetrics", "returned invalid capture time", { info });
243
304
  return null;
244
305
  }
245
306
  const linearAcceleration = extractFreshVector(info.linearAcceleration, info.capturedAtMs, tuning.maxSampleAgeMs);
@@ -250,7 +311,7 @@ const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUN
250
311
  sampledAtMs: gravityMetric.sampledAtMs
251
312
  } : gravityMetric);
252
313
  if (linearAcceleration === null || gyroscope === null || gravity === null) {
253
- dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned unavailable vectors", {
314
+ dialSdkDebugWarn(DEBUG_MODULE$4, "extractDrivingSnapshotMetrics", "returned unavailable vectors", {
254
315
  linearAcceleration,
255
316
  gyroscope,
256
317
  gravity
@@ -259,7 +320,7 @@ const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUN
259
320
  }
260
321
  const frame = buildVehicleFrame(gravity.value, tuning.vehicleRightMinimumNorm);
261
322
  if (frame === null) {
262
- dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned invalid vehicle frame", {
323
+ dialSdkDebugWarn(DEBUG_MODULE$4, "extractDrivingSnapshotMetrics", "returned invalid vehicle frame", {
263
324
  gravity: gravity.value,
264
325
  vehicleRightMinimumNorm: tuning.vehicleRightMinimumNorm
265
326
  });
@@ -282,28 +343,28 @@ const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUN
282
343
  rawGyroscope: gyroscope.value,
283
344
  frame
284
345
  };
285
- dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned", { result });
346
+ dialSdkDebugWarn(DEBUG_MODULE$4, "extractDrivingSnapshotMetrics", "returned", { result });
286
347
  return result;
287
348
  };
288
349
  const buildVehicleFrame = (gravity, rightMinimumNorm = DEFAULT_DRIVING_STATUS_TUNING.vehicleRightMinimumNorm) => {
289
- dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "called", {
350
+ dialSdkDebugWarn(DEBUG_MODULE$4, "buildVehicleFrame", "called", {
290
351
  gravity,
291
352
  rightMinimumNorm
292
353
  });
293
354
  const gravityDirection = normalize(gravity);
294
355
  if (gravityDirection === null) {
295
- dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "returned invalid gravity", { result: null });
356
+ dialSdkDebugWarn(DEBUG_MODULE$4, "buildVehicleFrame", "returned invalid gravity", { result: null });
296
357
  return null;
297
358
  }
298
359
  const up = gravityDirection;
299
360
  const projectedRight = subtract(DEVICE_RIGHT, scale(up, dot(DEVICE_RIGHT, up)));
300
361
  if (magnitude(projectedRight) < rightMinimumNorm) {
301
- dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "returned degenerate right axis", { result: null });
362
+ dialSdkDebugWarn(DEBUG_MODULE$4, "buildVehicleFrame", "returned degenerate right axis", { result: null });
302
363
  return null;
303
364
  }
304
365
  const right = normalize(projectedRight);
305
366
  if (right === null) {
306
- dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "returned invalid right axis", { result: null });
367
+ dialSdkDebugWarn(DEBUG_MODULE$4, "buildVehicleFrame", "returned invalid right axis", { result: null });
307
368
  return null;
308
369
  }
309
370
  const forward = normalize(cross(right, up));
@@ -312,7 +373,7 @@ const buildVehicleFrame = (gravity, rightMinimumNorm = DEFAULT_DRIVING_STATUS_TU
312
373
  right,
313
374
  forward
314
375
  };
315
- dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "returned", { result });
376
+ dialSdkDebugWarn(DEBUG_MODULE$4, "buildVehicleFrame", "returned", { result });
316
377
  return result;
317
378
  };
318
379
  const classifyInstantaneousEvidence = (metrics, tuning) => {
@@ -381,69 +442,10 @@ const normalize = (value) => {
381
442
  };
382
443
  const throwLoggedRangeError = (scope, message) => {
383
444
  const error = new RangeError(message);
384
- dialSdkDebugWarn(DEBUG_MODULE$5, scope, "threw", error);
445
+ dialSdkDebugWarn(DEBUG_MODULE$4, scope, "threw", error);
385
446
  throw error;
386
447
  };
387
448
  //#endregion
388
- //#region src/sensor/unified_info.ts
389
- const DEBUG_MODULE$4 = "unified_info.ts";
390
- const unifiedSensorInfo = () => {
391
- dialSdkDebugWarn(DEBUG_MODULE$4, "unifiedSensorInfo", "called");
392
- try {
393
- return window.unifiedSensorInfo().then((payload) => {
394
- dialSdkDebugWarn(DEBUG_MODULE$4, "unifiedSensorInfo", "resolved", { payload });
395
- return payload;
396
- }, (error) => {
397
- dialSdkDebugWarn(DEBUG_MODULE$4, "unifiedSensorInfo", "rejected", error);
398
- throw error;
399
- });
400
- } catch (error) {
401
- dialSdkDebugWarn(DEBUG_MODULE$4, "unifiedSensorInfo", "threw", error);
402
- throw error;
403
- }
404
- };
405
- //#endregion
406
- //#region src/sensor/constants.ts
407
- const BATTERY_HEALTH_LABELS = {
408
- GOOD: "良好",
409
- OVERHEAT: "过热",
410
- DEAD: "失效",
411
- OVER_VOLTAGE: "电压过高",
412
- UNSPECIFIED_FAILURE: "异常",
413
- COLD: "温度过低",
414
- UNKNOWN: "未知"
415
- };
416
- const CAR_RUNNING_LABELS = {
417
- STOPPED: "静止",
418
- STEADY_DRIVING: "匀速行驶",
419
- SUDDEN_BRAKING: "急刹车",
420
- BRAKING: "刹车",
421
- LEFT_TURN: "左转",
422
- RIGHT_TURN: "右转",
423
- ACCELERATION: "加速",
424
- RAPID_ACCELERATION: "急加速"
425
- };
426
- const CAR_RUNNING_STATUSES = [
427
- "STOPPED",
428
- "STEADY_DRIVING",
429
- "ACCELERATION",
430
- "RAPID_ACCELERATION",
431
- "BRAKING",
432
- "SUDDEN_BRAKING",
433
- "LEFT_TURN",
434
- "RIGHT_TURN"
435
- ];
436
- const CAR_RUNNING_STATUS_PRIORITY = {
437
- STOPPED: 0,
438
- STEADY_DRIVING: 0,
439
- ACCELERATION: 1,
440
- BRAKING: 2,
441
- LEFT_TURN: 3,
442
- RIGHT_TURN: 3,
443
- RAPID_ACCELERATION: 4,
444
- SUDDEN_BRAKING: 5
445
- };
446
- //#endregion
447
449
  //#region src/helper/async.ts
448
450
  const createAbortError = (message, debug) => {
449
451
  dialSdkDebugWarn(debug.moduleName, debug.scope, "called");
@@ -475,7 +477,7 @@ const invokeSafely = (callback, args, onError) => {
475
477
  }
476
478
  };
477
479
  //#endregion
478
- //#region src/sensor/driving_expressions.ts
480
+ //#region src/driving_expression/driving_expressions.ts
479
481
  const DEBUG_MODULE$3 = "driving_expressions.ts";
480
482
  const IS_RECORD_DEBUG$1 = {
481
483
  moduleName: DEBUG_MODULE$3,
@@ -591,7 +593,7 @@ const tryParseDrivingExpressionsConfig = (raw, options) => {
591
593
  }
592
594
  };
593
595
  /**
594
- * Reads Runtime's typed injection first, then the legacy face-config value.
596
+ * Reads Runtime's typed driving-expressions injection.
595
597
  * It never installs official fallbacks implicitly.
596
598
  */
597
599
  const readDrivingExpressionsConfig = (raw, options) => {
@@ -1097,24 +1099,8 @@ const parseJsonObject = (raw) => {
1097
1099
  };
1098
1100
  const readRuntimeDrivingExpressionsValue = () => {
1099
1101
  dialSdkDebugWarn(DEBUG_MODULE$3, "readRuntimeDrivingExpressionsValue", "called");
1100
- const runtimeGlobal = globalThis;
1101
- if (runtimeGlobal.__TOOOONY_DRIVING_EXPRESSIONS__ !== void 0) {
1102
- const result = runtimeGlobal.__TOOOONY_DRIVING_EXPRESSIONS__;
1103
- dialSdkDebugWarn(DEBUG_MODULE$3, "readRuntimeDrivingExpressionsValue", "returned typed injection", { result });
1104
- return result;
1105
- }
1106
- const faceConfig = runtimeGlobal.__TOOOONY_FACE_CONFIG__;
1107
- if (!isRecord(faceConfig, IS_RECORD_DEBUG$1)) {
1108
- dialSdkDebugWarn(DEBUG_MODULE$3, "readRuntimeDrivingExpressionsValue", "returned", { result: void 0 });
1109
- return;
1110
- }
1111
- if (faceConfig.drivingExpressions !== void 0) {
1112
- const result = faceConfig.drivingExpressions;
1113
- dialSdkDebugWarn(DEBUG_MODULE$3, "readRuntimeDrivingExpressionsValue", "returned face config", { result });
1114
- return result;
1115
- }
1116
- const result = isRecord(faceConfig.values, IS_RECORD_DEBUG$1) ? faceConfig.values.drivingExpressions : void 0;
1117
- dialSdkDebugWarn(DEBUG_MODULE$3, "readRuntimeDrivingExpressionsValue", "returned legacy face config", { result });
1102
+ const result = globalThis.__TOOOONY_DRIVING_EXPRESSIONS__;
1103
+ dialSdkDebugWarn(DEBUG_MODULE$3, "readRuntimeDrivingExpressionsValue", "returned typed injection", { result });
1118
1104
  return result;
1119
1105
  };
1120
1106
  const parseMediaAssetId = (raw, status) => {
@@ -1249,7 +1235,7 @@ const requireNonNegativeFinite = (name, value) => {
1249
1235
  return value;
1250
1236
  };
1251
1237
  //#endregion
1252
- //#region src/sensor/page_lifecycle.ts
1238
+ //#region src/driving_expression/page_lifecycle.ts
1253
1239
  const DEBUG_MODULE$2 = "page_lifecycle.ts";
1254
1240
  const subscribers = /* @__PURE__ */ new Set();
1255
1241
  let installedWindow = null;
@@ -1387,7 +1373,7 @@ const safeInvoke = (callback) => {
1387
1373
  })) dialSdkDebugWarn(DEBUG_MODULE$2, "safeInvoke", "returned");
1388
1374
  };
1389
1375
  //#endregion
1390
- //#region src/sensor/driving_status_controller.ts
1376
+ //#region src/driving_expression/driving_status_controller.ts
1391
1377
  const DEBUG_MODULE$1 = "driving_status_controller.ts";
1392
1378
  const ZERO_VECTOR = {
1393
1379
  x: 0,
@@ -1624,6 +1610,7 @@ var DefaultDrivingStatusController = class {
1624
1610
  }, delayMs);
1625
1611
  }
1626
1612
  async poll(generation) {
1613
+ const pollStartedAtMs = this.clock.now();
1627
1614
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "called", { generation });
1628
1615
  if (!this.polling || generation !== this.generation || this.pendingRead !== null) {
1629
1616
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "ignored", {
@@ -1674,7 +1661,10 @@ var DefaultDrivingStatusController = class {
1674
1661
  this.processUnavailable("provider_error");
1675
1662
  } finally {
1676
1663
  if (this.pendingRead === pendingRead) this.pendingRead = null;
1677
- if (this.polling && generation === this.generation) this.schedulePoll(this.pollIntervalMs, generation);
1664
+ if (this.polling && generation === this.generation) {
1665
+ const elapsedMs = Math.max(0, this.clock.now() - pollStartedAtMs);
1666
+ this.schedulePoll(Math.max(0, this.pollIntervalMs - elapsedMs), generation);
1667
+ }
1678
1668
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "completed", {
1679
1669
  generation,
1680
1670
  polling: this.polling
@@ -1735,19 +1725,23 @@ var DefaultDrivingStatusController = class {
1735
1725
  frameAgeMs: metrics.capturedAtMs - gravity.sampledAtMs,
1736
1726
  tuningVersion: this.tuning.version
1737
1727
  });
1738
- const addedLongitudinal = pushUniqueMetric(this.longitudinalWindow, metrics, "linearAccelerationSampledAtMs", Math.max(this.tuning.longitudinalWindowMs, this.tuning.rapidLongitudinalWindowMs));
1739
- const addedYaw = pushUniqueMetric(this.yawWindow, metrics, "gyroscopeSampledAtMs", Math.max(this.tuning.turnWindowMs, this.tuning.turnClosureWindowMs));
1740
- const addedMotion = pushUniqueVector(this.motionWindow, accelerometerSample, this.tuning.motionWindowMs);
1728
+ const longitudinalRetentionMs = Math.max(recognitionRetentionMs(this.tuning.longitudinalWindowMs, this.tuning.longitudinalMinimumSamples), recognitionRetentionMs(this.tuning.rapidLongitudinalWindowMs, this.tuning.rapidLongitudinalMinimumSamples));
1729
+ const addedLongitudinal = pushUniqueMetric(this.longitudinalWindow, metrics, "linearAccelerationSampledAtMs", longitudinalRetentionMs);
1730
+ const yawRetentionMs = Math.max(this.tuning.turnWindowMs, this.tuning.turnClosureWindowMs, recognitionRetentionMs(this.tuning.turnConfirmationMs, this.tuning.turnMinimumSamples));
1731
+ const addedYaw = pushUniqueMetric(this.yawWindow, metrics, "gyroscopeSampledAtMs", yawRetentionMs);
1732
+ const addedMotion = pushUniqueVector(this.motionWindow, accelerometerSample, recognitionRetentionMs(this.tuning.motionWindowMs, this.tuning.motionMinimumSamples));
1741
1733
  if (!addedLongitudinal && !addedYaw && !addedMotion) {
1742
1734
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processAvailable", "ignored duplicate sample");
1743
1735
  return;
1744
1736
  }
1745
- const motionMad = calculateCombinedAxisMad(this.motionWindow, this.tuning.motionMinimumSamples);
1737
+ const motionCoverageWindow = selectCoverageWindow(this.motionWindow, this.tuning.motionMinimumSamples, this.tuning.motionWindowMs, (sample) => sample.sampledAtMs);
1738
+ const motionStatisticsWindow = motionCoverageWindow ?? recentVectors(this.motionWindow, this.tuning.motionWindowMs);
1739
+ const motionMad = motionCoverageWindow === null ? null : calculateCombinedAxisMad(motionCoverageWindow, minimumTemporalSamples(this.tuning.motionMinimumSamples));
1746
1740
  const previousBaseStatus = this.baseStatus;
1747
1741
  const previousUrgentStatus = this.urgentStatus;
1748
1742
  const previousLongitudinalStatus = this.normalLongitudinalStatus;
1749
1743
  const previousTurnStatus = this.turnStatus;
1750
- const baseCondition = this.updateBaseDetector(motionMad, metrics, accelerometerSample.sampledAtMs, addedMotion);
1744
+ const baseCondition = this.updateBaseDetector(motionMad, metrics, accelerometerSample.sampledAtMs, addedMotion, motionCoverageWindow?.[0]?.sampledAtMs ?? accelerometerSample.sampledAtMs);
1751
1745
  const longitudinalEvidence = detectLongitudinalEvidence(this.longitudinalWindow, this.tuning);
1752
1746
  if (addedLongitudinal) this.updateLongitudinalDetector(longitudinalEvidence, metrics.linearAccelerationSampledAtMs);
1753
1747
  const turnEvidence = detectTurnEvidence(this.yawWindow, this.tuning);
@@ -1760,8 +1754,8 @@ var DefaultDrivingStatusController = class {
1760
1754
  if (this.stationaryConfirmed) this.updateBias(metrics);
1761
1755
  const statistics = {
1762
1756
  base: {
1763
- sampleCount: this.motionWindow.length,
1764
- durationMs: vectorWindowDuration(this.motionWindow),
1757
+ sampleCount: motionStatisticsWindow.length,
1758
+ durationMs: vectorWindowDuration(motionStatisticsWindow),
1765
1759
  motionMad,
1766
1760
  gyroMagnitude: metrics.gyroMagnitude,
1767
1761
  rawGyroMagnitude: metrics.rawGyroMagnitude
@@ -1807,7 +1801,7 @@ var DefaultDrivingStatusController = class {
1807
1801
  normalLongitudinalStatus: this.normalLongitudinalStatus
1808
1802
  });
1809
1803
  }
1810
- updateBaseDetector(motionMad, metrics, sampledAtMs, isNewMotionSample) {
1804
+ updateBaseDetector(motionMad, metrics, sampledAtMs, isNewMotionSample, conditionStartedAtMs) {
1811
1805
  if (motionMad === null || !isNewMotionSample) return "warming_up";
1812
1806
  const gyroForStop = this.biasCalibrated ? metrics.gyroMagnitude : metrics.rawGyroMagnitude;
1813
1807
  const stoppedGyroThreshold = this.biasCalibrated ? this.tuning.stoppedGyroThreshold : this.tuning.stoppedRawGyroThreshold;
@@ -1816,7 +1810,7 @@ var DefaultDrivingStatusController = class {
1816
1810
  const moving = motionMad >= this.tuning.drivingMotionThreshold || gyroForStop >= drivingGyroThreshold;
1817
1811
  if (stationary) {
1818
1812
  this.drivingConditionSinceMs = null;
1819
- if (this.stoppedConditionSinceMs === null) this.stoppedConditionSinceMs = this.motionWindow[0]?.sampledAtMs ?? sampledAtMs;
1813
+ if (this.stoppedConditionSinceMs === null) this.stoppedConditionSinceMs = conditionStartedAtMs;
1820
1814
  if (sampledAtMs - this.stoppedConditionSinceMs >= this.tuning.stoppedConfirmationMs) {
1821
1815
  this.baseStatus = "STOPPED";
1822
1816
  this.stationaryConfirmed = true;
@@ -1826,7 +1820,7 @@ var DefaultDrivingStatusController = class {
1826
1820
  this.stoppedConditionSinceMs = null;
1827
1821
  this.stationaryConfirmed = false;
1828
1822
  if (moving) {
1829
- if (this.drivingConditionSinceMs === null) this.drivingConditionSinceMs = this.motionWindow[0]?.sampledAtMs ?? sampledAtMs;
1823
+ if (this.drivingConditionSinceMs === null) this.drivingConditionSinceMs = sampledAtMs;
1830
1824
  if (sampledAtMs - this.drivingConditionSinceMs >= this.tuning.drivingConfirmationMs) this.baseStatus = "STEADY_DRIVING";
1831
1825
  return "moving";
1832
1826
  }
@@ -1942,7 +1936,6 @@ var DefaultDrivingStatusController = class {
1942
1936
  const now = this.clock.now();
1943
1937
  if (this.unavailableSinceMs === null) this.unavailableSinceMs = now;
1944
1938
  const unavailableForMs = Math.max(0, now - this.unavailableSinceMs);
1945
- this.clearRecognitionWindows();
1946
1939
  this.setCandidate(null, now);
1947
1940
  this.emitDiagnostic({
1948
1941
  type: "sample_unavailable",
@@ -1960,6 +1953,7 @@ var DefaultDrivingStatusController = class {
1960
1953
  return;
1961
1954
  }
1962
1955
  this.fallbackApplied = true;
1956
+ this.clearRecognitionWindows();
1963
1957
  this.baseStatus = "STOPPED";
1964
1958
  this.emitDiagnostic({
1965
1959
  type: "fallback_to_stopped",
@@ -2076,8 +2070,12 @@ var DefaultDrivingStatusController = class {
2076
2070
  }
2077
2071
  };
2078
2072
  const detectLongitudinalEvidence = (window, tuning) => {
2079
- const rapidWindow = recentMetrics(window, tuning.rapidLongitudinalWindowMs, "linearAccelerationSampledAtMs");
2080
- const normalWindow = recentMetrics(window, tuning.longitudinalWindowMs, "linearAccelerationSampledAtMs");
2073
+ const recentRapidWindow = recentMetrics(window, tuning.rapidLongitudinalWindowMs, "linearAccelerationSampledAtMs");
2074
+ const rapidCoverageWindow = selectCoverageWindow(window, tuning.rapidLongitudinalMinimumSamples, tuning.rapidLongitudinalWindowMs, (sample) => sample.linearAccelerationSampledAtMs);
2075
+ const rapidWindow = rapidCoverageWindow ?? recentRapidWindow;
2076
+ const recentNormalWindow = recentMetrics(window, tuning.longitudinalWindowMs, "linearAccelerationSampledAtMs");
2077
+ const normalCoverageWindow = selectCoverageWindow(window, tuning.longitudinalMinimumSamples, tuning.longitudinalWindowMs, (sample) => sample.linearAccelerationSampledAtMs);
2078
+ const normalWindow = normalCoverageWindow ?? recentNormalWindow;
2081
2079
  let rapidCandidate = null;
2082
2080
  let rejectionReason = null;
2083
2081
  const rapidValues = rapidWindow.map((sample) => sample.longitudinal);
@@ -2099,7 +2097,7 @@ const detectLongitudinalEvidence = (window, tuning) => {
2099
2097
  verticalRms,
2100
2098
  hasReversal
2101
2099
  };
2102
- if (windowCovers(rapidWindow, tuning.rapidLongitudinalMinimumSamples, tuning.rapidLongitudinalWindowMs, "linearAccelerationSampledAtMs")) {
2100
+ if (rapidCoverageWindow !== null) {
2103
2101
  if (rapidPeak >= tuning.rapidLongitudinalPeakThreshold && Math.abs(rapidMean) >= tuning.rapidLongitudinalMeanThreshold) {
2104
2102
  if (verticalRms > longitudinalRms * tuning.verticalBumpRatio) rejectionReason = "vertical_bump";
2105
2103
  else if (hasReversal) rejectionReason = "longitudinal_reversal";
@@ -2121,8 +2119,9 @@ const detectLongitudinalEvidence = (window, tuning) => {
2121
2119
  peak: maximumAbsolute(values),
2122
2120
  coherence
2123
2121
  };
2124
- retainedNormal = Math.abs(mean) >= tuning.longitudinalExitThreshold;
2125
- if (windowCovers(normalWindow, tuning.longitudinalMinimumSamples, tuning.longitudinalWindowMs, "linearAccelerationSampledAtMs") && Math.abs(mean) >= tuning.longitudinalThreshold && coherence >= tuning.longitudinalSignCoherence) normalCandidate = direction > 0 ? "ACCELERATION" : "BRAKING";
2122
+ const retentionValues = recentNormalWindow.map((sample) => sample.longitudinal);
2123
+ retainedNormal = Math.abs(trimmedMean(retentionValues)) >= tuning.longitudinalExitThreshold;
2124
+ if (normalCoverageWindow !== null && Math.abs(mean) >= tuning.longitudinalThreshold && coherence >= tuning.longitudinalSignCoherence) normalCandidate = direction > 0 ? "ACCELERATION" : "BRAKING";
2126
2125
  }
2127
2126
  return {
2128
2127
  rapidCandidate,
@@ -2134,7 +2133,8 @@ const detectLongitudinalEvidence = (window, tuning) => {
2134
2133
  };
2135
2134
  };
2136
2135
  const detectTurnEvidence = (window, tuning) => {
2137
- const latest = window[window.length - 1];
2136
+ const turnWindow = recentMetrics(window, tuning.turnWindowMs, "gyroscopeSampledAtMs");
2137
+ const latest = turnWindow[turnWindow.length - 1];
2138
2138
  if (!latest) return {
2139
2139
  candidate: null,
2140
2140
  latestYaw: 0,
@@ -2142,20 +2142,22 @@ const detectTurnEvidence = (window, tuning) => {
2142
2142
  rejectionReason: null,
2143
2143
  statistics: null
2144
2144
  };
2145
- const strongWindow = recentMetrics(window, tuning.turnConfirmationMs, "gyroscopeSampledAtMs");
2145
+ const recentStrongWindow = recentMetrics(turnWindow, tuning.turnConfirmationMs, "gyroscopeSampledAtMs");
2146
+ const strongCoverageWindow = selectCoverageWindow(window, tuning.turnMinimumSamples, tuning.turnConfirmationMs, (sample) => sample.gyroscopeSampledAtMs);
2147
+ const strongWindow = strongCoverageWindow ?? recentStrongWindow;
2146
2148
  const strongMean = trimmedMean(strongWindow.map((sample) => sample.yaw));
2147
2149
  const strongDirection = Math.sign(strongMean);
2148
2150
  const strongCoherence = signCoherence(strongWindow.map((sample) => sample.yaw), strongDirection, tuning.turnExitYawThreshold);
2149
- const strong = windowCovers(strongWindow, tuning.turnMinimumSamples, tuning.turnConfirmationMs, "gyroscopeSampledAtMs") && Math.abs(strongMean) >= tuning.turnYawThreshold && strongCoherence >= tuning.turnSignCoherence;
2150
- const signedAngle = integrateYaw(window);
2151
+ const strong = strongCoverageWindow !== null && Math.abs(strongMean) >= tuning.turnYawThreshold && strongCoherence >= tuning.turnSignCoherence;
2152
+ const signedAngle = integrateYaw(turnWindow);
2151
2153
  const gentleDirection = Math.sign(signedAngle);
2152
- const gentleCoherence = signCoherence(window.map((sample) => sample.yaw), gentleDirection, tuning.turnExitYawThreshold);
2153
- const yawRms = rootMeanSquare(window.map((sample) => sample.yaw));
2154
- const rawGyroRms = rootMeanSquare(window.map((sample) => sample.rawGyroMagnitude));
2154
+ const gentleCoherence = signCoherence(turnWindow.map((sample) => sample.yaw), gentleDirection, tuning.turnExitYawThreshold);
2155
+ const yawRms = rootMeanSquare(turnWindow.map((sample) => sample.yaw));
2156
+ const rawGyroRms = rootMeanSquare(turnWindow.map((sample) => sample.rawGyroMagnitude));
2155
2157
  const yawDominance = rawGyroRms <= Number.EPSILON ? 0 : Math.min(1, yawRms / rawGyroRms);
2156
- const lateralRms = rootMeanSquare(window.map((sample) => sample.lateral));
2157
- const verticalRms = rootMeanSquare(window.map((sample) => sample.vertical));
2158
- const gentleShape = window.length >= tuning.turnMinimumSamples && Math.abs(signedAngle) >= tuning.turnIntegratedAngleThreshold && gentleCoherence >= tuning.turnSignCoherence;
2158
+ const lateralRms = rootMeanSquare(turnWindow.map((sample) => sample.lateral));
2159
+ const verticalRms = rootMeanSquare(turnWindow.map((sample) => sample.vertical));
2160
+ const gentleShape = turnWindow.length >= tuning.turnMinimumSamples && Math.abs(signedAngle) >= tuning.turnIntegratedAngleThreshold && gentleCoherence >= tuning.turnSignCoherence;
2159
2161
  const verticalDominant = verticalRms > Math.max(lateralRms, tuning.turnVerticalNoiseFloor) * tuning.turnVerticalBumpRatio;
2160
2162
  const yawDominant = yawDominance >= tuning.turnYawDominanceThreshold;
2161
2163
  const laterallySupported = lateralRms >= tuning.turnLateralEvidenceThreshold;
@@ -2167,8 +2169,8 @@ const detectTurnEvidence = (window, tuning) => {
2167
2169
  sampledAtMs: latest.gyroscopeSampledAtMs,
2168
2170
  rejectionReason,
2169
2171
  statistics: {
2170
- sampleCount: window.length,
2171
- durationMs: metricWindowDuration(window, "gyroscopeSampledAtMs"),
2172
+ sampleCount: turnWindow.length,
2173
+ durationMs: metricWindowDuration(turnWindow, "gyroscopeSampledAtMs"),
2172
2174
  strongMean,
2173
2175
  strongCoherence,
2174
2176
  signedAngle,
@@ -2192,10 +2194,29 @@ const recentMetrics = (values, windowMs, timestampKey) => {
2192
2194
  const latestAtMs = values[values.length - 1]?.[timestampKey] ?? 0;
2193
2195
  return values.filter((value) => value[timestampKey] >= latestAtMs - windowMs);
2194
2196
  };
2195
- const windowCovers = (values, minimumSamples, durationMs, timestampKey) => {
2196
- if (values.length < minimumSamples) return false;
2197
- const first = values[0];
2198
- return values[values.length - 1][timestampKey] - first[timestampKey] >= durationMs;
2197
+ const MINIMUM_TEMPORAL_SAMPLES = 3;
2198
+ const MAXIMUM_RECOGNITION_GAP_MS = 1e3;
2199
+ const minimumTemporalSamples = (minimumSamples) => Math.min(minimumSamples, MINIMUM_TEMPORAL_SAMPLES);
2200
+ const maximumRecognitionGapMs = (minimumSamples, durationMs) => {
2201
+ const nominalSampleIntervalMs = durationMs / Math.max(1, minimumSamples - 1);
2202
+ return Math.max(MAXIMUM_RECOGNITION_GAP_MS, nominalSampleIntervalMs * 2.5);
2203
+ };
2204
+ const recognitionRetentionMs = (durationMs, minimumSamples) => Math.max(durationMs, maximumRecognitionGapMs(minimumSamples, durationMs) * Math.max(1, minimumTemporalSamples(minimumSamples) - 1));
2205
+ const selectCoverageWindow = (values, minimumSamples, durationMs, timestampOf) => {
2206
+ const requiredSamples = minimumTemporalSamples(minimumSamples);
2207
+ if (values.length < requiredSamples) return null;
2208
+ const lastAtMs = timestampOf(values[values.length - 1]);
2209
+ const maximumGapMs = maximumRecognitionGapMs(minimumSamples, durationMs);
2210
+ for (let startIndex = values.length - requiredSamples; startIndex >= 0; startIndex -= 1) {
2211
+ const coveredValues = values.slice(startIndex);
2212
+ if (lastAtMs - timestampOf(coveredValues[0]) < durationMs) continue;
2213
+ if (coveredValues.slice(1).every((value, index) => timestampOf(value) - timestampOf(coveredValues[index]) < maximumGapMs)) return coveredValues;
2214
+ }
2215
+ return null;
2216
+ };
2217
+ const recentVectors = (values, windowMs) => {
2218
+ const latestAtMs = values[values.length - 1]?.sampledAtMs ?? 0;
2219
+ return values.filter((value) => value.sampledAtMs >= latestAtMs - windowMs);
2199
2220
  };
2200
2221
  const integrateYaw = (values) => {
2201
2222
  let angle = 0;
@@ -2276,7 +2297,7 @@ const systemClock = {
2276
2297
  const isBaseStatus = (status) => status === "STOPPED" || status === "STEADY_DRIVING";
2277
2298
  const noop = () => void 0;
2278
2299
  //#endregion
2279
- //#region src/sensor/driving_expression_player.ts
2300
+ //#region src/driving_expression/driving_expression_player.ts
2280
2301
  const DEBUG_MODULE = "driving_expression_player.ts";
2281
2302
  const MAX_RETAINED_IMAGES = 3;
2282
2303
  const POSITIVE_FINITE_DEBUG = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ziztechnology/dial-library",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "private": false,
5
5
  "description": "Tooony 表盘工具函数、方法",
6
6
  "files": [