@ziztechnology/dial-library 0.0.13 → 0.0.15

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
@@ -1,9 +1,10 @@
1
1
  import { Gunzip } from "fflate";
2
- const resolveLogIntervalMs = (intervalMs = void 0) => typeof intervalMs === "number" && Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : 500;
2
+ const resolveLogIntervalMs = (intervalMs = void 0) => typeof intervalMs === "number" && Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : 200;
3
3
  //#endregion
4
4
  //#region src/helper/debugger.ts
5
5
  const logIntervalMs = resolveLogIntervalMs();
6
6
  const lastLoggedAtByType = /* @__PURE__ */ new Map();
7
+ let hasLoggedSdkVersion = false;
7
8
  const createLogTypeKey = (moduleName, scope, phase) => JSON.stringify([
8
9
  moduleName,
9
10
  scope,
@@ -38,11 +39,76 @@ const dialSdkDebugWarn = (moduleName, scope, phase, data) => {
38
39
  if (!shouldLog(moduleName, scope, phase)) return;
39
40
  const message = `[DIAL_SDK_DEBUG - ${moduleName}] ${scope}: ${phase}`;
40
41
  try {
42
+ if (!hasLoggedSdkVersion) {
43
+ console.log(`[DIAL_SDK_DEBUG] SDK version: 0.0.15`);
44
+ hasLoggedSdkVersion = true;
45
+ }
41
46
  if (data === void 0) console.log(message);
42
47
  else console.log(message, stringifyDebugData(data));
43
48
  } catch {}
44
49
  };
45
50
  //#endregion
51
+ //#region src/sensor/unified_info.ts
52
+ const DEBUG_MODULE$5 = "unified_info.ts";
53
+ const unifiedSensorInfo = () => {
54
+ dialSdkDebugWarn(DEBUG_MODULE$5, "unifiedSensorInfo", "called");
55
+ try {
56
+ return window.unifiedSensorInfo().then((payload) => {
57
+ dialSdkDebugWarn(DEBUG_MODULE$5, "unifiedSensorInfo", "resolved", { payload });
58
+ return payload;
59
+ }, (error) => {
60
+ dialSdkDebugWarn(DEBUG_MODULE$5, "unifiedSensorInfo", "rejected", error);
61
+ throw error;
62
+ });
63
+ } catch (error) {
64
+ dialSdkDebugWarn(DEBUG_MODULE$5, "unifiedSensorInfo", "threw", error);
65
+ throw error;
66
+ }
67
+ };
68
+ //#endregion
69
+ //#region src/sensor/constants.ts
70
+ const BATTERY_HEALTH_LABELS = {
71
+ GOOD: "良好",
72
+ OVERHEAT: "过热",
73
+ DEAD: "失效",
74
+ OVER_VOLTAGE: "电压过高",
75
+ UNSPECIFIED_FAILURE: "异常",
76
+ COLD: "温度过低",
77
+ UNKNOWN: "未知"
78
+ };
79
+ //#endregion
80
+ //#region src/driving_expression/constants.ts
81
+ const CAR_RUNNING_LABELS = {
82
+ STOPPED: "静止",
83
+ STEADY_DRIVING: "匀速行驶",
84
+ SUDDEN_BRAKING: "急刹车",
85
+ BRAKING: "刹车",
86
+ LEFT_TURN: "左转",
87
+ RIGHT_TURN: "右转",
88
+ ACCELERATION: "加速",
89
+ RAPID_ACCELERATION: "急加速"
90
+ };
91
+ const CAR_RUNNING_STATUSES = [
92
+ "STOPPED",
93
+ "STEADY_DRIVING",
94
+ "ACCELERATION",
95
+ "RAPID_ACCELERATION",
96
+ "BRAKING",
97
+ "SUDDEN_BRAKING",
98
+ "LEFT_TURN",
99
+ "RIGHT_TURN"
100
+ ];
101
+ const CAR_RUNNING_STATUS_PRIORITY = {
102
+ STOPPED: 0,
103
+ STEADY_DRIVING: 0,
104
+ ACCELERATION: 1,
105
+ BRAKING: 2,
106
+ LEFT_TURN: 3,
107
+ RIGHT_TURN: 3,
108
+ RAPID_ACCELERATION: 4,
109
+ SUDDEN_BRAKING: 5
110
+ };
111
+ //#endregion
46
112
  //#region src/helper/sensor.ts
47
113
  const extractFreshVector = (metric, capturedAtMs, maxSampleAgeMs) => {
48
114
  if (!metric?.available || !Number.isFinite(capturedAtMs) || !Number.isFinite(metric.sampledAtMs)) return null;
@@ -82,8 +148,8 @@ const isRecord = (value, debug) => {
82
148
  return result;
83
149
  };
84
150
  //#endregion
85
- //#region src/sensor/check_status.ts
86
- const DEBUG_MODULE$5 = "check_status.ts";
151
+ //#region src/driving_expression/check_status.ts
152
+ const DEBUG_MODULE$4 = "check_status.ts";
87
153
  const DEVICE_RIGHT = {
88
154
  x: 1,
89
155
  y: 0,
@@ -95,7 +161,7 @@ const ZERO_VECTOR$1 = {
95
161
  z: 0
96
162
  };
97
163
  const DEFAULT_DRIVING_STATUS_TUNING = Object.freeze({
98
- version: "driving-status-calibrated-2026-07-17",
164
+ version: "driving-status-calibrated-2026-07-17-r2",
99
165
  maxSampleAgeMs: 1e3,
100
166
  gravityFilterTimeConstantMs: 1e3,
101
167
  biasFilterTimeConstantMs: 1e4,
@@ -108,7 +174,7 @@ const DEFAULT_DRIVING_STATUS_TUNING = Object.freeze({
108
174
  stoppedRawGyroThreshold: .065,
109
175
  drivingGyroThreshold: .04,
110
176
  drivingRawGyroThreshold: .08,
111
- stoppedConfirmationMs: 3e3,
177
+ stoppedConfirmationMs: 1500,
112
178
  drivingConfirmationMs: 600,
113
179
  longitudinalWindowMs: 800,
114
180
  longitudinalMinimumSamples: 5,
@@ -139,14 +205,14 @@ const DEFAULT_DRIVING_STATUS_TUNING = Object.freeze({
139
205
  turnClosureWindowMs: 1500
140
206
  });
141
207
  const classifyDrivingSnapshot = (info, tuningOverrides) => {
142
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingSnapshot", "called", {
208
+ dialSdkDebugWarn(DEBUG_MODULE$4, "classifyDrivingSnapshot", "called", {
143
209
  info,
144
210
  tuningOverrides
145
211
  });
146
212
  const tuning = resolveDrivingStatusTuning(tuningOverrides);
147
213
  const metrics = extractDrivingSnapshotMetrics(info, tuning);
148
214
  if (metrics === null) {
149
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingSnapshot", "returned", { result: null });
215
+ dialSdkDebugWarn(DEBUG_MODULE$4, "classifyDrivingSnapshot", "returned", { result: null });
150
216
  return null;
151
217
  }
152
218
  const { candidateStatus, rejectionReason } = classifyInstantaneousEvidence(metrics, tuning);
@@ -160,11 +226,11 @@ const classifyDrivingSnapshot = (info, tuningOverrides) => {
160
226
  maximumSampleAgeMs: Math.max(info.capturedAtMs - metrics.linearAccelerationSampledAtMs, info.capturedAtMs - metrics.gyroscopeSampledAtMs, info.capturedAtMs - metrics.gravitySampledAtMs)
161
227
  }
162
228
  };
163
- dialSdkDebugWarn(DEBUG_MODULE$5, "classifyDrivingSnapshot", "returned", { result });
229
+ dialSdkDebugWarn(DEBUG_MODULE$4, "classifyDrivingSnapshot", "returned", { result });
164
230
  return result;
165
231
  };
166
232
  const resolveDrivingStatusTuning = (overrides = {}) => {
167
- dialSdkDebugWarn(DEBUG_MODULE$5, "resolveDrivingStatusTuning", "called", { overrides });
233
+ dialSdkDebugWarn(DEBUG_MODULE$4, "resolveDrivingStatusTuning", "called", { overrides });
168
234
  const tuning = {
169
235
  ...DEFAULT_DRIVING_STATUS_TUNING,
170
236
  ...overrides
@@ -204,7 +270,7 @@ const resolveDrivingStatusTuning = (overrides = {}) => {
204
270
  "turnExitConfirmationMs",
205
271
  "turnClosureWindowMs"
206
272
  ]) requirePositiveFinite(field, tuning[field], {
207
- moduleName: DEBUG_MODULE$5,
273
+ moduleName: DEBUG_MODULE$4,
208
274
  scope: "assertPositiveFinite"
209
275
  });
210
276
  for (const field of [
@@ -229,17 +295,17 @@ const resolveDrivingStatusTuning = (overrides = {}) => {
229
295
  if (tuning.rapidLongitudinalMeanThreshold <= tuning.longitudinalThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "rapidLongitudinalMeanThreshold must be greater than longitudinalThreshold");
230
296
  if (tuning.rapidLongitudinalPeakThreshold < tuning.rapidLongitudinalMeanThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "rapidLongitudinalPeakThreshold must be greater than or equal to rapidLongitudinalMeanThreshold");
231
297
  if (tuning.turnYawThreshold <= tuning.turnExitYawThreshold) throwLoggedRangeError("resolveDrivingStatusTuning", "turnYawThreshold must be greater than turnExitYawThreshold");
232
- dialSdkDebugWarn(DEBUG_MODULE$5, "resolveDrivingStatusTuning", "returned", { tuning });
298
+ dialSdkDebugWarn(DEBUG_MODULE$4, "resolveDrivingStatusTuning", "returned", { tuning });
233
299
  return tuning;
234
300
  };
235
301
  const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUNING, correction = {}) => {
236
- dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "called", {
302
+ dialSdkDebugWarn(DEBUG_MODULE$4, "extractDrivingSnapshotMetrics", "called", {
237
303
  info,
238
304
  tuning,
239
305
  correction
240
306
  });
241
307
  if (!info || !Number.isFinite(info.capturedAtMs)) {
242
- dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned invalid capture time", { info });
308
+ dialSdkDebugWarn(DEBUG_MODULE$4, "extractDrivingSnapshotMetrics", "returned invalid capture time", { info });
243
309
  return null;
244
310
  }
245
311
  const linearAcceleration = extractFreshVector(info.linearAcceleration, info.capturedAtMs, tuning.maxSampleAgeMs);
@@ -250,7 +316,7 @@ const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUN
250
316
  sampledAtMs: gravityMetric.sampledAtMs
251
317
  } : gravityMetric);
252
318
  if (linearAcceleration === null || gyroscope === null || gravity === null) {
253
- dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned unavailable vectors", {
319
+ dialSdkDebugWarn(DEBUG_MODULE$4, "extractDrivingSnapshotMetrics", "returned unavailable vectors", {
254
320
  linearAcceleration,
255
321
  gyroscope,
256
322
  gravity
@@ -259,7 +325,7 @@ const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUN
259
325
  }
260
326
  const frame = buildVehicleFrame(gravity.value, tuning.vehicleRightMinimumNorm);
261
327
  if (frame === null) {
262
- dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned invalid vehicle frame", {
328
+ dialSdkDebugWarn(DEBUG_MODULE$4, "extractDrivingSnapshotMetrics", "returned invalid vehicle frame", {
263
329
  gravity: gravity.value,
264
330
  vehicleRightMinimumNorm: tuning.vehicleRightMinimumNorm
265
331
  });
@@ -282,28 +348,28 @@ const extractDrivingSnapshotMetrics = (info, tuning = DEFAULT_DRIVING_STATUS_TUN
282
348
  rawGyroscope: gyroscope.value,
283
349
  frame
284
350
  };
285
- dialSdkDebugWarn(DEBUG_MODULE$5, "extractDrivingSnapshotMetrics", "returned", { result });
351
+ dialSdkDebugWarn(DEBUG_MODULE$4, "extractDrivingSnapshotMetrics", "returned", { result });
286
352
  return result;
287
353
  };
288
354
  const buildVehicleFrame = (gravity, rightMinimumNorm = DEFAULT_DRIVING_STATUS_TUNING.vehicleRightMinimumNorm) => {
289
- dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "called", {
355
+ dialSdkDebugWarn(DEBUG_MODULE$4, "buildVehicleFrame", "called", {
290
356
  gravity,
291
357
  rightMinimumNorm
292
358
  });
293
359
  const gravityDirection = normalize(gravity);
294
360
  if (gravityDirection === null) {
295
- dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "returned invalid gravity", { result: null });
361
+ dialSdkDebugWarn(DEBUG_MODULE$4, "buildVehicleFrame", "returned invalid gravity", { result: null });
296
362
  return null;
297
363
  }
298
364
  const up = gravityDirection;
299
365
  const projectedRight = subtract(DEVICE_RIGHT, scale(up, dot(DEVICE_RIGHT, up)));
300
366
  if (magnitude(projectedRight) < rightMinimumNorm) {
301
- dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "returned degenerate right axis", { result: null });
367
+ dialSdkDebugWarn(DEBUG_MODULE$4, "buildVehicleFrame", "returned degenerate right axis", { result: null });
302
368
  return null;
303
369
  }
304
370
  const right = normalize(projectedRight);
305
371
  if (right === null) {
306
- dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "returned invalid right axis", { result: null });
372
+ dialSdkDebugWarn(DEBUG_MODULE$4, "buildVehicleFrame", "returned invalid right axis", { result: null });
307
373
  return null;
308
374
  }
309
375
  const forward = normalize(cross(right, up));
@@ -312,7 +378,7 @@ const buildVehicleFrame = (gravity, rightMinimumNorm = DEFAULT_DRIVING_STATUS_TU
312
378
  right,
313
379
  forward
314
380
  };
315
- dialSdkDebugWarn(DEBUG_MODULE$5, "buildVehicleFrame", "returned", { result });
381
+ dialSdkDebugWarn(DEBUG_MODULE$4, "buildVehicleFrame", "returned", { result });
316
382
  return result;
317
383
  };
318
384
  const classifyInstantaneousEvidence = (metrics, tuning) => {
@@ -381,69 +447,10 @@ const normalize = (value) => {
381
447
  };
382
448
  const throwLoggedRangeError = (scope, message) => {
383
449
  const error = new RangeError(message);
384
- dialSdkDebugWarn(DEBUG_MODULE$5, scope, "threw", error);
450
+ dialSdkDebugWarn(DEBUG_MODULE$4, scope, "threw", error);
385
451
  throw error;
386
452
  };
387
453
  //#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
454
  //#region src/helper/async.ts
448
455
  const createAbortError = (message, debug) => {
449
456
  dialSdkDebugWarn(debug.moduleName, debug.scope, "called");
@@ -475,7 +482,7 @@ const invokeSafely = (callback, args, onError) => {
475
482
  }
476
483
  };
477
484
  //#endregion
478
- //#region src/sensor/driving_expressions.ts
485
+ //#region src/driving_expression/driving_expressions.ts
479
486
  const DEBUG_MODULE$3 = "driving_expressions.ts";
480
487
  const IS_RECORD_DEBUG$1 = {
481
488
  moduleName: DEBUG_MODULE$3,
@@ -591,7 +598,7 @@ const tryParseDrivingExpressionsConfig = (raw, options) => {
591
598
  }
592
599
  };
593
600
  /**
594
- * Reads Runtime's typed injection first, then the legacy face-config value.
601
+ * Reads Runtime's typed driving-expressions injection.
595
602
  * It never installs official fallbacks implicitly.
596
603
  */
597
604
  const readDrivingExpressionsConfig = (raw, options) => {
@@ -1097,24 +1104,8 @@ const parseJsonObject = (raw) => {
1097
1104
  };
1098
1105
  const readRuntimeDrivingExpressionsValue = () => {
1099
1106
  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 });
1107
+ const result = globalThis.__TOOOONY_DRIVING_EXPRESSIONS__;
1108
+ dialSdkDebugWarn(DEBUG_MODULE$3, "readRuntimeDrivingExpressionsValue", "returned typed injection", { result });
1118
1109
  return result;
1119
1110
  };
1120
1111
  const parseMediaAssetId = (raw, status) => {
@@ -1249,7 +1240,7 @@ const requireNonNegativeFinite = (name, value) => {
1249
1240
  return value;
1250
1241
  };
1251
1242
  //#endregion
1252
- //#region src/sensor/page_lifecycle.ts
1243
+ //#region src/driving_expression/page_lifecycle.ts
1253
1244
  const DEBUG_MODULE$2 = "page_lifecycle.ts";
1254
1245
  const subscribers = /* @__PURE__ */ new Set();
1255
1246
  let installedWindow = null;
@@ -1387,7 +1378,7 @@ const safeInvoke = (callback) => {
1387
1378
  })) dialSdkDebugWarn(DEBUG_MODULE$2, "safeInvoke", "returned");
1388
1379
  };
1389
1380
  //#endregion
1390
- //#region src/sensor/driving_status_controller.ts
1381
+ //#region src/driving_expression/driving_status_controller.ts
1391
1382
  const DEBUG_MODULE$1 = "driving_status_controller.ts";
1392
1383
  const ZERO_VECTOR = {
1393
1384
  x: 0,
@@ -1624,6 +1615,7 @@ var DefaultDrivingStatusController = class {
1624
1615
  }, delayMs);
1625
1616
  }
1626
1617
  async poll(generation) {
1618
+ const pollStartedAtMs = this.clock.now();
1627
1619
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "called", { generation });
1628
1620
  if (!this.polling || generation !== this.generation || this.pendingRead !== null) {
1629
1621
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "ignored", {
@@ -1674,7 +1666,10 @@ var DefaultDrivingStatusController = class {
1674
1666
  this.processUnavailable("provider_error");
1675
1667
  } finally {
1676
1668
  if (this.pendingRead === pendingRead) this.pendingRead = null;
1677
- if (this.polling && generation === this.generation) this.schedulePoll(this.pollIntervalMs, generation);
1669
+ if (this.polling && generation === this.generation) {
1670
+ const elapsedMs = Math.max(0, this.clock.now() - pollStartedAtMs);
1671
+ this.schedulePoll(Math.max(0, this.pollIntervalMs - elapsedMs), generation);
1672
+ }
1678
1673
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.poll", "completed", {
1679
1674
  generation,
1680
1675
  polling: this.polling
@@ -1735,19 +1730,23 @@ var DefaultDrivingStatusController = class {
1735
1730
  frameAgeMs: metrics.capturedAtMs - gravity.sampledAtMs,
1736
1731
  tuningVersion: this.tuning.version
1737
1732
  });
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);
1733
+ const longitudinalRetentionMs = Math.max(recognitionRetentionMs(this.tuning.longitudinalWindowMs, this.tuning.longitudinalMinimumSamples), recognitionRetentionMs(this.tuning.rapidLongitudinalWindowMs, this.tuning.rapidLongitudinalMinimumSamples));
1734
+ const addedLongitudinal = pushUniqueMetric(this.longitudinalWindow, metrics, "linearAccelerationSampledAtMs", longitudinalRetentionMs);
1735
+ const yawRetentionMs = Math.max(this.tuning.turnWindowMs, this.tuning.turnClosureWindowMs, recognitionRetentionMs(this.tuning.turnConfirmationMs, this.tuning.turnMinimumSamples));
1736
+ const addedYaw = pushUniqueMetric(this.yawWindow, metrics, "gyroscopeSampledAtMs", yawRetentionMs);
1737
+ const addedMotion = pushUniqueVector(this.motionWindow, accelerometerSample, recognitionRetentionMs(this.tuning.motionWindowMs, this.tuning.motionMinimumSamples));
1741
1738
  if (!addedLongitudinal && !addedYaw && !addedMotion) {
1742
1739
  dialSdkDebugWarn(DEBUG_MODULE$1, "DefaultDrivingStatusController.processAvailable", "ignored duplicate sample");
1743
1740
  return;
1744
1741
  }
1745
- const motionMad = calculateCombinedAxisMad(this.motionWindow, this.tuning.motionMinimumSamples);
1742
+ const motionCoverageWindow = selectCoverageWindow(this.motionWindow, this.tuning.motionMinimumSamples, this.tuning.motionWindowMs, (sample) => sample.sampledAtMs);
1743
+ const motionStatisticsWindow = motionCoverageWindow ?? recentVectors(this.motionWindow, this.tuning.motionWindowMs);
1744
+ const motionMad = motionCoverageWindow === null ? null : calculateCombinedAxisMad(motionCoverageWindow, minimumTemporalSamples(this.tuning.motionMinimumSamples));
1746
1745
  const previousBaseStatus = this.baseStatus;
1747
1746
  const previousUrgentStatus = this.urgentStatus;
1748
1747
  const previousLongitudinalStatus = this.normalLongitudinalStatus;
1749
1748
  const previousTurnStatus = this.turnStatus;
1750
- const baseCondition = this.updateBaseDetector(motionMad, metrics, accelerometerSample.sampledAtMs, addedMotion);
1749
+ const baseCondition = this.updateBaseDetector(motionMad, metrics, accelerometerSample.sampledAtMs, addedMotion, motionCoverageWindow?.[0]?.sampledAtMs ?? accelerometerSample.sampledAtMs);
1751
1750
  const longitudinalEvidence = detectLongitudinalEvidence(this.longitudinalWindow, this.tuning);
1752
1751
  if (addedLongitudinal) this.updateLongitudinalDetector(longitudinalEvidence, metrics.linearAccelerationSampledAtMs);
1753
1752
  const turnEvidence = detectTurnEvidence(this.yawWindow, this.tuning);
@@ -1760,8 +1759,8 @@ var DefaultDrivingStatusController = class {
1760
1759
  if (this.stationaryConfirmed) this.updateBias(metrics);
1761
1760
  const statistics = {
1762
1761
  base: {
1763
- sampleCount: this.motionWindow.length,
1764
- durationMs: vectorWindowDuration(this.motionWindow),
1762
+ sampleCount: motionStatisticsWindow.length,
1763
+ durationMs: vectorWindowDuration(motionStatisticsWindow),
1765
1764
  motionMad,
1766
1765
  gyroMagnitude: metrics.gyroMagnitude,
1767
1766
  rawGyroMagnitude: metrics.rawGyroMagnitude
@@ -1807,7 +1806,7 @@ var DefaultDrivingStatusController = class {
1807
1806
  normalLongitudinalStatus: this.normalLongitudinalStatus
1808
1807
  });
1809
1808
  }
1810
- updateBaseDetector(motionMad, metrics, sampledAtMs, isNewMotionSample) {
1809
+ updateBaseDetector(motionMad, metrics, sampledAtMs, isNewMotionSample, conditionStartedAtMs) {
1811
1810
  if (motionMad === null || !isNewMotionSample) return "warming_up";
1812
1811
  const gyroForStop = this.biasCalibrated ? metrics.gyroMagnitude : metrics.rawGyroMagnitude;
1813
1812
  const stoppedGyroThreshold = this.biasCalibrated ? this.tuning.stoppedGyroThreshold : this.tuning.stoppedRawGyroThreshold;
@@ -1816,7 +1815,7 @@ var DefaultDrivingStatusController = class {
1816
1815
  const moving = motionMad >= this.tuning.drivingMotionThreshold || gyroForStop >= drivingGyroThreshold;
1817
1816
  if (stationary) {
1818
1817
  this.drivingConditionSinceMs = null;
1819
- if (this.stoppedConditionSinceMs === null) this.stoppedConditionSinceMs = this.motionWindow[0]?.sampledAtMs ?? sampledAtMs;
1818
+ if (this.stoppedConditionSinceMs === null) this.stoppedConditionSinceMs = conditionStartedAtMs;
1820
1819
  if (sampledAtMs - this.stoppedConditionSinceMs >= this.tuning.stoppedConfirmationMs) {
1821
1820
  this.baseStatus = "STOPPED";
1822
1821
  this.stationaryConfirmed = true;
@@ -1826,7 +1825,7 @@ var DefaultDrivingStatusController = class {
1826
1825
  this.stoppedConditionSinceMs = null;
1827
1826
  this.stationaryConfirmed = false;
1828
1827
  if (moving) {
1829
- if (this.drivingConditionSinceMs === null) this.drivingConditionSinceMs = this.motionWindow[0]?.sampledAtMs ?? sampledAtMs;
1828
+ if (this.drivingConditionSinceMs === null) this.drivingConditionSinceMs = sampledAtMs;
1830
1829
  if (sampledAtMs - this.drivingConditionSinceMs >= this.tuning.drivingConfirmationMs) this.baseStatus = "STEADY_DRIVING";
1831
1830
  return "moving";
1832
1831
  }
@@ -1942,7 +1941,6 @@ var DefaultDrivingStatusController = class {
1942
1941
  const now = this.clock.now();
1943
1942
  if (this.unavailableSinceMs === null) this.unavailableSinceMs = now;
1944
1943
  const unavailableForMs = Math.max(0, now - this.unavailableSinceMs);
1945
- this.clearRecognitionWindows();
1946
1944
  this.setCandidate(null, now);
1947
1945
  this.emitDiagnostic({
1948
1946
  type: "sample_unavailable",
@@ -1960,6 +1958,7 @@ var DefaultDrivingStatusController = class {
1960
1958
  return;
1961
1959
  }
1962
1960
  this.fallbackApplied = true;
1961
+ this.clearRecognitionWindows();
1963
1962
  this.baseStatus = "STOPPED";
1964
1963
  this.emitDiagnostic({
1965
1964
  type: "fallback_to_stopped",
@@ -2076,8 +2075,12 @@ var DefaultDrivingStatusController = class {
2076
2075
  }
2077
2076
  };
2078
2077
  const detectLongitudinalEvidence = (window, tuning) => {
2079
- const rapidWindow = recentMetrics(window, tuning.rapidLongitudinalWindowMs, "linearAccelerationSampledAtMs");
2080
- const normalWindow = recentMetrics(window, tuning.longitudinalWindowMs, "linearAccelerationSampledAtMs");
2078
+ const recentRapidWindow = recentMetrics(window, tuning.rapidLongitudinalWindowMs, "linearAccelerationSampledAtMs");
2079
+ const rapidCoverageWindow = selectCoverageWindow(window, tuning.rapidLongitudinalMinimumSamples, tuning.rapidLongitudinalWindowMs, (sample) => sample.linearAccelerationSampledAtMs);
2080
+ const rapidWindow = rapidCoverageWindow ?? recentRapidWindow;
2081
+ const recentNormalWindow = recentMetrics(window, tuning.longitudinalWindowMs, "linearAccelerationSampledAtMs");
2082
+ const normalCoverageWindow = selectCoverageWindow(window, tuning.longitudinalMinimumSamples, tuning.longitudinalWindowMs, (sample) => sample.linearAccelerationSampledAtMs);
2083
+ const normalWindow = normalCoverageWindow ?? recentNormalWindow;
2081
2084
  let rapidCandidate = null;
2082
2085
  let rejectionReason = null;
2083
2086
  const rapidValues = rapidWindow.map((sample) => sample.longitudinal);
@@ -2099,7 +2102,7 @@ const detectLongitudinalEvidence = (window, tuning) => {
2099
2102
  verticalRms,
2100
2103
  hasReversal
2101
2104
  };
2102
- if (windowCovers(rapidWindow, tuning.rapidLongitudinalMinimumSamples, tuning.rapidLongitudinalWindowMs, "linearAccelerationSampledAtMs")) {
2105
+ if (rapidCoverageWindow !== null) {
2103
2106
  if (rapidPeak >= tuning.rapidLongitudinalPeakThreshold && Math.abs(rapidMean) >= tuning.rapidLongitudinalMeanThreshold) {
2104
2107
  if (verticalRms > longitudinalRms * tuning.verticalBumpRatio) rejectionReason = "vertical_bump";
2105
2108
  else if (hasReversal) rejectionReason = "longitudinal_reversal";
@@ -2121,8 +2124,9 @@ const detectLongitudinalEvidence = (window, tuning) => {
2121
2124
  peak: maximumAbsolute(values),
2122
2125
  coherence
2123
2126
  };
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";
2127
+ const retentionValues = recentNormalWindow.map((sample) => sample.longitudinal);
2128
+ retainedNormal = Math.abs(trimmedMean(retentionValues)) >= tuning.longitudinalExitThreshold;
2129
+ if (normalCoverageWindow !== null && Math.abs(mean) >= tuning.longitudinalThreshold && coherence >= tuning.longitudinalSignCoherence) normalCandidate = direction > 0 ? "ACCELERATION" : "BRAKING";
2126
2130
  }
2127
2131
  return {
2128
2132
  rapidCandidate,
@@ -2134,7 +2138,8 @@ const detectLongitudinalEvidence = (window, tuning) => {
2134
2138
  };
2135
2139
  };
2136
2140
  const detectTurnEvidence = (window, tuning) => {
2137
- const latest = window[window.length - 1];
2141
+ const turnWindow = recentMetrics(window, tuning.turnWindowMs, "gyroscopeSampledAtMs");
2142
+ const latest = turnWindow[turnWindow.length - 1];
2138
2143
  if (!latest) return {
2139
2144
  candidate: null,
2140
2145
  latestYaw: 0,
@@ -2142,20 +2147,22 @@ const detectTurnEvidence = (window, tuning) => {
2142
2147
  rejectionReason: null,
2143
2148
  statistics: null
2144
2149
  };
2145
- const strongWindow = recentMetrics(window, tuning.turnConfirmationMs, "gyroscopeSampledAtMs");
2150
+ const recentStrongWindow = recentMetrics(turnWindow, tuning.turnConfirmationMs, "gyroscopeSampledAtMs");
2151
+ const strongCoverageWindow = selectCoverageWindow(window, tuning.turnMinimumSamples, tuning.turnConfirmationMs, (sample) => sample.gyroscopeSampledAtMs);
2152
+ const strongWindow = strongCoverageWindow ?? recentStrongWindow;
2146
2153
  const strongMean = trimmedMean(strongWindow.map((sample) => sample.yaw));
2147
2154
  const strongDirection = Math.sign(strongMean);
2148
2155
  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);
2156
+ const strong = strongCoverageWindow !== null && Math.abs(strongMean) >= tuning.turnYawThreshold && strongCoherence >= tuning.turnSignCoherence;
2157
+ const signedAngle = integrateYaw(turnWindow);
2151
2158
  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));
2159
+ const gentleCoherence = signCoherence(turnWindow.map((sample) => sample.yaw), gentleDirection, tuning.turnExitYawThreshold);
2160
+ const yawRms = rootMeanSquare(turnWindow.map((sample) => sample.yaw));
2161
+ const rawGyroRms = rootMeanSquare(turnWindow.map((sample) => sample.rawGyroMagnitude));
2155
2162
  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;
2163
+ const lateralRms = rootMeanSquare(turnWindow.map((sample) => sample.lateral));
2164
+ const verticalRms = rootMeanSquare(turnWindow.map((sample) => sample.vertical));
2165
+ const gentleShape = turnWindow.length >= tuning.turnMinimumSamples && Math.abs(signedAngle) >= tuning.turnIntegratedAngleThreshold && gentleCoherence >= tuning.turnSignCoherence;
2159
2166
  const verticalDominant = verticalRms > Math.max(lateralRms, tuning.turnVerticalNoiseFloor) * tuning.turnVerticalBumpRatio;
2160
2167
  const yawDominant = yawDominance >= tuning.turnYawDominanceThreshold;
2161
2168
  const laterallySupported = lateralRms >= tuning.turnLateralEvidenceThreshold;
@@ -2167,8 +2174,8 @@ const detectTurnEvidence = (window, tuning) => {
2167
2174
  sampledAtMs: latest.gyroscopeSampledAtMs,
2168
2175
  rejectionReason,
2169
2176
  statistics: {
2170
- sampleCount: window.length,
2171
- durationMs: metricWindowDuration(window, "gyroscopeSampledAtMs"),
2177
+ sampleCount: turnWindow.length,
2178
+ durationMs: metricWindowDuration(turnWindow, "gyroscopeSampledAtMs"),
2172
2179
  strongMean,
2173
2180
  strongCoherence,
2174
2181
  signedAngle,
@@ -2192,10 +2199,29 @@ const recentMetrics = (values, windowMs, timestampKey) => {
2192
2199
  const latestAtMs = values[values.length - 1]?.[timestampKey] ?? 0;
2193
2200
  return values.filter((value) => value[timestampKey] >= latestAtMs - windowMs);
2194
2201
  };
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;
2202
+ const MINIMUM_TEMPORAL_SAMPLES = 3;
2203
+ const MAXIMUM_RECOGNITION_GAP_MS = 1e3;
2204
+ const minimumTemporalSamples = (minimumSamples) => Math.min(minimumSamples, MINIMUM_TEMPORAL_SAMPLES);
2205
+ const maximumRecognitionGapMs = (minimumSamples, durationMs) => {
2206
+ const nominalSampleIntervalMs = durationMs / Math.max(1, minimumSamples - 1);
2207
+ return Math.max(MAXIMUM_RECOGNITION_GAP_MS, nominalSampleIntervalMs * 2.5);
2208
+ };
2209
+ const recognitionRetentionMs = (durationMs, minimumSamples) => Math.max(durationMs, maximumRecognitionGapMs(minimumSamples, durationMs) * Math.max(1, minimumTemporalSamples(minimumSamples) - 1));
2210
+ const selectCoverageWindow = (values, minimumSamples, durationMs, timestampOf) => {
2211
+ const requiredSamples = minimumTemporalSamples(minimumSamples);
2212
+ if (values.length < requiredSamples) return null;
2213
+ const lastAtMs = timestampOf(values[values.length - 1]);
2214
+ const maximumGapMs = maximumRecognitionGapMs(minimumSamples, durationMs);
2215
+ for (let startIndex = values.length - requiredSamples; startIndex >= 0; startIndex -= 1) {
2216
+ const coveredValues = values.slice(startIndex);
2217
+ if (lastAtMs - timestampOf(coveredValues[0]) < durationMs) continue;
2218
+ if (coveredValues.slice(1).every((value, index) => timestampOf(value) - timestampOf(coveredValues[index]) < maximumGapMs)) return coveredValues;
2219
+ }
2220
+ return null;
2221
+ };
2222
+ const recentVectors = (values, windowMs) => {
2223
+ const latestAtMs = values[values.length - 1]?.sampledAtMs ?? 0;
2224
+ return values.filter((value) => value.sampledAtMs >= latestAtMs - windowMs);
2199
2225
  };
2200
2226
  const integrateYaw = (values) => {
2201
2227
  let angle = 0;
@@ -2276,7 +2302,7 @@ const systemClock = {
2276
2302
  const isBaseStatus = (status) => status === "STOPPED" || status === "STEADY_DRIVING";
2277
2303
  const noop = () => void 0;
2278
2304
  //#endregion
2279
- //#region src/sensor/driving_expression_player.ts
2305
+ //#region src/driving_expression/driving_expression_player.ts
2280
2306
  const DEBUG_MODULE = "driving_expression_player.ts";
2281
2307
  const MAX_RETAINED_IMAGES = 3;
2282
2308
  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.15",
4
4
  "private": false,
5
5
  "description": "Tooony 表盘工具函数、方法",
6
6
  "files": [