@ziztechnology/dial-library 0.0.2 → 0.0.3
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 +346 -98
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +85 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,115 +1,196 @@
|
|
|
1
|
-
# Toooony
|
|
1
|
+
# Toooony 表盘 SDK
|
|
2
|
+
|
|
3
|
+
`@ziztechnology/dial-library` 为 Toooony 表盘提供统一的设备信息和行车状态 API。你可以用它读取传感器,也可以直接订阅经过平滑、确认和回退处理后的行车状态。
|
|
2
4
|
|
|
3
5
|
> [!NOTE]
|
|
4
|
-
> 最低支持 Runtime
|
|
6
|
+
> SDK 最低支持 Toooony Runtime `v1.5.20`。行车状态传感器 Bridge 仅向审核通过的 `PACKAGED_H5` 开放。第三方表盘应使用本 SDK,不要直接依赖 Runtime 注入到 `window` 上的内部方法。
|
|
5
7
|
|
|
6
8
|
## 安装
|
|
7
9
|
|
|
10
|
+
使用 npm、pnpm 或 yarn 安装:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @ziztechnology/dial-library
|
|
14
|
+
```
|
|
15
|
+
|
|
8
16
|
```bash
|
|
9
|
-
|
|
17
|
+
pnpm add @ziztechnology/dial-library
|
|
10
18
|
```
|
|
11
19
|
|
|
12
|
-
|
|
20
|
+
```bash
|
|
21
|
+
yarn add @ziztechnology/dial-library
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
SDK 是 ESM 包,并自带 TypeScript 类型。
|
|
25
|
+
|
|
26
|
+
## 快速开始
|
|
27
|
+
|
|
28
|
+
下面的例子会监听行车状态,并把当前状态显示在页面上:
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<p id="driving-status">正在读取行车状态…</p>
|
|
32
|
+
```
|
|
13
33
|
|
|
14
34
|
```ts
|
|
15
|
-
import {
|
|
16
|
-
OFFICIAL_DRIVING_EXPRESSION_FALLBACKS,
|
|
17
|
-
createDrivingExpressionPlayer,
|
|
18
|
-
createDrivingStatusController,
|
|
19
|
-
readDrivingExpressionsConfig,
|
|
20
|
-
} from '@ziztechnology/dial-library';
|
|
35
|
+
import { CAR_RUNNING_LABELS, createDrivingStatusController } from '@ziztechnology/dial-library';
|
|
21
36
|
|
|
22
|
-
const
|
|
23
|
-
if (!
|
|
37
|
+
const statusElement = document.querySelector('#driving-status');
|
|
38
|
+
if (!statusElement) throw new Error('找不到 #driving-status');
|
|
24
39
|
|
|
25
|
-
const player = createDrivingExpressionPlayer(document.querySelector('#expression')!, {
|
|
26
|
-
config,
|
|
27
|
-
// 官方母版必须显式传入;第三方不传时不会自动获得官方 Emoji。
|
|
28
|
-
fallbacks: OFFICIAL_DRIVING_EXPRESSION_FALLBACKS,
|
|
29
|
-
});
|
|
30
40
|
const controller = createDrivingStatusController();
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
// 控制器默认从 STOPPED 开始。subscribe 只在稳定状态改变后触发。
|
|
43
|
+
statusElement.textContent = CAR_RUNNING_LABELS[controller.getSnapshot().status];
|
|
44
|
+
|
|
33
45
|
const unsubscribe = controller.subscribe((event) => {
|
|
34
|
-
|
|
46
|
+
statusElement.textContent = CAR_RUNNING_LABELS[event.status];
|
|
35
47
|
});
|
|
36
|
-
controller.start();
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
const pause = () => {
|
|
40
|
-
controller.stop();
|
|
41
|
-
player.pause();
|
|
42
|
-
};
|
|
43
|
-
const resume = () => {
|
|
44
|
-
player.resume();
|
|
45
|
-
controller.start();
|
|
46
|
-
};
|
|
49
|
+
controller.start();
|
|
47
50
|
|
|
48
|
-
//
|
|
51
|
+
// 页面不再使用控制器时释放资源。
|
|
49
52
|
const destroy = () => {
|
|
50
53
|
unsubscribe();
|
|
51
54
|
controller.destroy();
|
|
52
|
-
player.destroy();
|
|
53
55
|
};
|
|
54
56
|
```
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
`start()` 启动传感器轮询;`destroy()` 停止轮询、取消未完成的读取并移除监听器。`start()`、`stop()` 和 `destroy()` 都可以重复调用。
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
控制器默认会跟随 Runtime 和页面生命周期自动暂停、恢复,因此大多数表盘不需要额外监听 `visibilitychange`。
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
## 常量
|
|
61
63
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
### 行车状态
|
|
65
|
+
|
|
66
|
+
`CarRunningStatus` 一共有八种取值。状态字符串是稳定协议,可以用于判断逻辑或作为素材配置的键:
|
|
67
|
+
|
|
68
|
+
| 常量值 | `CAR_RUNNING_LABELS` 中文名称 | 默认优先级 |
|
|
69
|
+
| -------------------- | ----------------------------- | ---------: |
|
|
70
|
+
| `STOPPED` | 静止 | 0 |
|
|
71
|
+
| `STEADY_DRIVING` | 匀速行驶 | 0 |
|
|
72
|
+
| `ACCELERATION` | 加速 | 1 |
|
|
73
|
+
| `BRAKING` | 刹车 | 2 |
|
|
74
|
+
| `LEFT_TURN` | 左转 | 3 |
|
|
75
|
+
| `RIGHT_TURN` | 右转 | 3 |
|
|
76
|
+
| `RAPID_ACCELERATION` | 急加速 | 4 |
|
|
77
|
+
| `SUDDEN_BRAKING` | 急刹车 | 5 |
|
|
78
|
+
|
|
79
|
+
相关导出:
|
|
80
|
+
|
|
81
|
+
- `CAR_RUNNING_STATUSES`:按协议顺序保存全部八种状态,适合遍历。
|
|
82
|
+
- `CAR_RUNNING_LABELS`:状态到中文名称的映射。
|
|
83
|
+
- `CAR_RUNNING_STATUS_PRIORITY`:状态优先级。高优先级动作可以抢占正在展示的低优先级状态。
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { CAR_RUNNING_LABELS, CAR_RUNNING_STATUSES, type CarRunningStatus } from '@ziztechnology/dial-library';
|
|
87
|
+
|
|
88
|
+
const renderStatus = (status: CarRunningStatus) => {
|
|
89
|
+
console.log(status, CAR_RUNNING_LABELS[status]);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
CAR_RUNNING_STATUSES.forEach(renderStatus);
|
|
71
93
|
```
|
|
72
94
|
|
|
73
|
-
|
|
95
|
+
### 电池状态
|
|
96
|
+
|
|
97
|
+
统一传感器信息中的电池字段使用以下字符串联合类型:
|
|
74
98
|
|
|
75
|
-
|
|
99
|
+
- `BatteryStatus`:`CHARGING`、`DISCHARGING`、`FULL`、`NOT_CHARGING`、`UNKNOWN`。
|
|
100
|
+
- `BatteryPlugged`:`AC`、`USB`、`WIRELESS`、`DOCK`、`NONE`。
|
|
101
|
+
- `BatteryHealth`:`GOOD`、`OVERHEAT`、`DEAD`、`OVER_VOLTAGE`、`UNSPECIFIED_FAILURE`、`COLD`、`UNKNOWN`。
|
|
102
|
+
- `BATTERY_HEALTH_LABELS`:`BatteryHealth` 到中文名称的映射。
|
|
76
103
|
|
|
77
|
-
|
|
104
|
+
## 统一的传感器
|
|
105
|
+
|
|
106
|
+
`unifiedSensorInfo()` 一次返回设备当前的统一快照。快照包含陀螺仪、加速度、电池、温度、Wi-Fi、方向和磁场等信息:
|
|
78
107
|
|
|
79
108
|
```ts
|
|
80
|
-
import {
|
|
109
|
+
import { unifiedSensorInfo } from '@ziztechnology/dial-library';
|
|
110
|
+
|
|
111
|
+
const info = await unifiedSensorInfo();
|
|
81
112
|
|
|
82
|
-
|
|
113
|
+
if (info.temperature.available) {
|
|
114
|
+
console.log(`电池温度:${info.temperature.value}${info.temperature.unit}`);
|
|
115
|
+
} else {
|
|
116
|
+
console.log(`温度不可用:${info.temperature.unavailableReason}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (info.linearAcceleration.available) {
|
|
120
|
+
const { x, y, z } = info.linearAcceleration.value;
|
|
121
|
+
console.log('线性加速度:', { x, y, z }, info.linearAcceleration.unit);
|
|
122
|
+
}
|
|
83
123
|
```
|
|
84
124
|
|
|
85
|
-
|
|
125
|
+
每个指标都使用 `available` 作为区分字段:
|
|
86
126
|
|
|
87
127
|
```ts
|
|
88
|
-
type
|
|
89
|
-
| {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
type SensorMetric<T> =
|
|
129
|
+
| {
|
|
130
|
+
available: true;
|
|
131
|
+
value: T;
|
|
132
|
+
unit: string | null;
|
|
133
|
+
source: string;
|
|
134
|
+
sampledAtMs: number;
|
|
135
|
+
unavailableReason: null;
|
|
136
|
+
}
|
|
137
|
+
| {
|
|
138
|
+
available: false;
|
|
139
|
+
value: null;
|
|
140
|
+
unit: string | null;
|
|
141
|
+
source: string;
|
|
142
|
+
sampledAtMs: number | null;
|
|
143
|
+
unavailableReason: UnifiedSensorUnavailableReason | null;
|
|
144
|
+
};
|
|
94
145
|
```
|
|
95
146
|
|
|
96
|
-
|
|
147
|
+
先判断 `available`,TypeScript 才会把 `value` 收窄到真实类型。不要用 `value === null` 猜测设备是否支持某项指标。
|
|
148
|
+
|
|
149
|
+
常见的 `unavailableReason` 有:
|
|
150
|
+
|
|
151
|
+
| 原因 | 含义 |
|
|
152
|
+
| ---------------------- | -------------------------- |
|
|
153
|
+
| `SENSOR_MISSING` | 设备没有对应传感器 |
|
|
154
|
+
| `NO_SAMPLE` | 传感器存在,但暂时没有样本 |
|
|
155
|
+
| `PLATFORM_UNAVAILABLE` | 当前平台不提供该能力 |
|
|
156
|
+
| `PERMISSION_DENIED` | 没有读取权限 |
|
|
157
|
+
| `NOT_CONNECTED` | 对应设备或网络未连接 |
|
|
97
158
|
|
|
98
|
-
`
|
|
159
|
+
`capturedAtMs` 是快照时间,具体指标的 `sampledAtMs` 是采样时间。行车状态识别默认拒绝未来样本和超过 1 秒的旧样本。
|
|
99
160
|
|
|
100
|
-
|
|
161
|
+
### 可用字段
|
|
101
162
|
|
|
102
|
-
|
|
163
|
+
| 字段 | 值 | 单位 |
|
|
164
|
+
| -------------------- | -------------------------------------- | -------- |
|
|
165
|
+
| `gyroscope` | `{ x, y, z }` | `rad/s` |
|
|
166
|
+
| `accelerometer` | `{ x, y, z }` | `m/s²` |
|
|
167
|
+
| `linearAcceleration` | `{ x, y, z }` | `m/s²` |
|
|
168
|
+
| `gravity` | `{ x, y, z }` | `m/s²` |
|
|
169
|
+
| `magneticField` | `{ x, y, z }` | `µT` |
|
|
170
|
+
| `orientation` | `{ azimuth, pitch, roll }` | `degree` |
|
|
171
|
+
| `motionIntensity` | 数值;旧 Runtime 中可能不存在 | `m/s²` |
|
|
172
|
+
| `temperature` | 数值 | `°C` |
|
|
173
|
+
| `wifiSsid` | 字符串 | 无 |
|
|
174
|
+
| `batteryCapacity` | `{ levelPercent, chargeCounterUah }` | 无 |
|
|
175
|
+
| `battery` | 电池是否存在、充电状态、健康度和电压等 | 无 |
|
|
176
|
+
|
|
177
|
+
如果只需要根据单次快照做无状态判断,可以使用纯函数 `classifyDrivingSnapshot()`:
|
|
103
178
|
|
|
104
179
|
```ts
|
|
105
180
|
import { classifyDrivingSnapshot, unifiedSensorInfo } from '@ziztechnology/dial-library';
|
|
106
181
|
|
|
107
182
|
const status = classifyDrivingSnapshot(await unifiedSensorInfo());
|
|
183
|
+
|
|
184
|
+
if (status === null) {
|
|
185
|
+
console.log('传感器数据不可用、非法或已经过期');
|
|
186
|
+
} else {
|
|
187
|
+
console.log('当前瞬时状态:', status);
|
|
188
|
+
}
|
|
108
189
|
```
|
|
109
190
|
|
|
110
|
-
`
|
|
191
|
+
`checkRunningStatus()` 是同一函数的兼容别名。单次分类不会去抖,也不会保存上一次状态;它使用 Runtime 的 `motionIntensity >= 0.4 m/s²` 判断瞬时行驶。面向 UI 时,通常应该使用下一节带 accelerometer 滑窗的状态控制器。
|
|
111
192
|
|
|
112
|
-
|
|
193
|
+
设备的固定安装方向为:
|
|
113
194
|
|
|
114
195
|
```text
|
|
115
196
|
+X = 车身右侧
|
|
@@ -117,68 +198,235 @@ const status = classifyDrivingSnapshot(await unifiedSensorInfo());
|
|
|
117
198
|
-Z = 车辆前进方向
|
|
118
199
|
```
|
|
119
200
|
|
|
120
|
-
|
|
201
|
+
因此纵向加速度取 `-linearAcceleration.z`,横向加速度取 `linearAcceleration.x`,偏航角速度取 `gyroscope.y`。
|
|
202
|
+
|
|
203
|
+
## 行车状态基本使用
|
|
121
204
|
|
|
122
|
-
|
|
205
|
+
### 订阅稳定状态
|
|
123
206
|
|
|
124
|
-
|
|
207
|
+
`createDrivingStatusController()` 负责持续读取传感器,并处理瞬时噪声。推荐让页面只响应它发出的 `status_changed`:
|
|
125
208
|
|
|
126
209
|
```ts
|
|
210
|
+
import { CAR_RUNNING_LABELS, createDrivingStatusController } from '@ziztechnology/dial-library';
|
|
211
|
+
|
|
127
212
|
const controller = createDrivingStatusController({
|
|
213
|
+
initialStatus: 'STOPPED',
|
|
128
214
|
pollIntervalMs: 200,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const unsubscribe = controller.subscribe((event) => {
|
|
218
|
+
console.log(`${CAR_RUNNING_LABELS[event.previousStatus]} -> ${CAR_RUNNING_LABELS[event.status]}`, event.reason);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
controller.start();
|
|
222
|
+
|
|
223
|
+
// 临时停止;之后可以再次 start()。
|
|
224
|
+
controller.stop();
|
|
225
|
+
|
|
226
|
+
// 永久释放;destroy() 后不能重新启动。
|
|
227
|
+
unsubscribe();
|
|
228
|
+
controller.destroy();
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
控制器默认每 200ms 读取一次,并保证上一次读取结束后才开始下一次。它会对普通动作平滑、要求候选状态连续出现、设置状态最短展示时间,并允许急刹车等高优先级状态抢占。候选状态不会触发 UI,只有提交后的稳定状态才会通知订阅者。
|
|
232
|
+
|
|
233
|
+
静止和匀速使用 accelerometer 的 1 秒三轴波动窗口判断。窗口按 `sampledAtMs` 去重,至少积累 5 个有效样本;总体标准差大于 `0.4 m/s²` 时进入 `STEADY_DRIVING`,小于 `0.25 m/s²` 时进入 `STOPPED`,中间区间保持原基础状态。滑窗预热不影响加速、刹车或转弯识别。accelerometer 不可用或过期时,整次行车快照进入不可用流程。
|
|
234
|
+
|
|
235
|
+
### 读取当前状态
|
|
236
|
+
|
|
237
|
+
订阅只通知后续变化。初次渲染或调试时,可以读取快照:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const snapshot = controller.getSnapshot();
|
|
241
|
+
|
|
242
|
+
console.log(snapshot.status); // 已提交状态
|
|
243
|
+
console.log(snapshot.candidate); // 正在确认的候选状态,可能为 null
|
|
244
|
+
console.log(snapshot.candidateConfirmationCount);
|
|
245
|
+
console.log(snapshot.lifecycle); // stopped、running 或 destroyed
|
|
246
|
+
console.log(snapshot.tuningVersion); // driving-status-v1
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 调整识别参数
|
|
250
|
+
|
|
251
|
+
默认参数集中在 `DEFAULT_DRIVING_STATUS_TUNING`。只覆盖需要调整的字段即可:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
const controller = createDrivingStatusController({
|
|
255
|
+
tuning: {
|
|
256
|
+
turnYawThreshold: 0.6,
|
|
257
|
+
motionWindowMs: 1_200,
|
|
258
|
+
motionMinimumSamples: 6,
|
|
259
|
+
drivingMotionThreshold: 0.45,
|
|
260
|
+
stoppedMotionThreshold: 0.28,
|
|
261
|
+
confirmationSamples: {
|
|
262
|
+
LEFT_TURN: 3,
|
|
263
|
+
RIGHT_TURN: 3,
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`drivingMotionThreshold` 必须大于 `stoppedMotionThreshold`,形成进入行驶和退出行驶的迟滞区间。`motionMinimumSamples` 必须是正整数,其余窗口和阈值必须是正有限数值。
|
|
270
|
+
|
|
271
|
+
固定安装的惯性传感器无法严格区分“完全静止”和“理想匀速直线运动”。默认算法使用三轴加速度波动和陀螺仪扰动近似判断,修改阈值后应在真机上路测。
|
|
272
|
+
|
|
273
|
+
### 驱动行车表情
|
|
274
|
+
|
|
275
|
+
Runtime 可以为八种状态注入图片、视频、Live Photo、TGS 或 Emoji 配置。`readDrivingExpressionsConfig()` 会读取并校验这份配置,播放器负责切换素材:
|
|
276
|
+
|
|
277
|
+
```html
|
|
278
|
+
<div id="expression"></div>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import {
|
|
283
|
+
createDrivingExpressionPlayer,
|
|
284
|
+
createDrivingStatusController,
|
|
285
|
+
readDrivingExpressionsConfig,
|
|
286
|
+
} from '@ziztechnology/dial-library';
|
|
287
|
+
|
|
288
|
+
const container = document.querySelector<HTMLElement>('#expression');
|
|
289
|
+
if (!container) throw new Error('找不到 #expression');
|
|
290
|
+
|
|
291
|
+
const config = readDrivingExpressionsConfig();
|
|
292
|
+
if (!config) throw new Error('Runtime 未提供有效的行车表情配置');
|
|
293
|
+
|
|
294
|
+
const player = createDrivingExpressionPlayer(container, { config });
|
|
295
|
+
const controller = createDrivingStatusController();
|
|
296
|
+
|
|
297
|
+
await player.show(controller.getSnapshot().status);
|
|
298
|
+
|
|
299
|
+
const unsubscribe = controller.subscribe((event) => {
|
|
300
|
+
void player.show(event.status);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
controller.start();
|
|
304
|
+
|
|
305
|
+
const destroy = () => {
|
|
306
|
+
unsubscribe();
|
|
307
|
+
controller.destroy();
|
|
308
|
+
player.destroy();
|
|
309
|
+
};
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
播放器同一时间只保留一个活动动态素材。它会复用视频元素,并在状态离开时停止播放;页面暂停或隐藏时,播放器与控制器都会自动暂停。
|
|
313
|
+
|
|
314
|
+
## 行车状态的回退
|
|
315
|
+
|
|
316
|
+
行车状态有两类彼此独立的回退:传感器不可用时的**状态回退**,以及表情素材失败时的**媒体回退**。
|
|
317
|
+
|
|
318
|
+
### 传感器不可用时回退到静止
|
|
319
|
+
|
|
320
|
+
控制器遇到一次读取失败、非法快照或过期样本时,会暂时保持最后一个已提交状态,避免页面闪烁。连续不可用达到 3 秒后,控制器才提交 `STOPPED`:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
const controller = createDrivingStatusController({
|
|
129
324
|
unavailableFallbackMs: 3_000,
|
|
130
|
-
initialStatus: 'STOPPED',
|
|
131
|
-
tuning: { turnYawThreshold: 0.32 },
|
|
132
325
|
onDiagnostic(event) {
|
|
133
|
-
|
|
326
|
+
if (event.type === 'fallback_to_stopped') {
|
|
327
|
+
console.warn(`传感器已连续不可用 ${event.unavailableForMs}ms`);
|
|
328
|
+
}
|
|
134
329
|
},
|
|
135
330
|
});
|
|
331
|
+
|
|
332
|
+
controller.subscribe((event) => {
|
|
333
|
+
if (event.reason === 'unavailable_fallback') {
|
|
334
|
+
console.log('已回退到 STOPPED');
|
|
335
|
+
}
|
|
336
|
+
});
|
|
136
337
|
```
|
|
137
338
|
|
|
138
|
-
|
|
339
|
+
传感器恢复后,控制器会清除不可用状态并重新确认候选,不会立即把单个样本提交给 UI。`unavailableFallbackMs` 必须是大于 0 的有限数值。
|
|
340
|
+
|
|
341
|
+
### 素材失败时使用显式回退
|
|
139
342
|
|
|
140
|
-
|
|
141
|
-
- 普通动作短窗口平滑,急动作检查未平滑峰值。
|
|
142
|
-
- 急刹车 > 急加速 > 转弯 > 刹车 > 加速 > 静止/匀速的抢占优先级。
|
|
143
|
-
- 急动作 1 样本、转弯/普通动作 2 样本、匀速 3 样本、静止 5 样本的连续确认。
|
|
144
|
-
- 动作退出阈值为入口阈值的 `0.75`,并对静止/匀速使用反向滞回区间。
|
|
145
|
-
- 每个已提交状态的最短展示时间;更高优先级候选可抢占,锁期间继续采样。
|
|
146
|
-
- 单次/短暂不可用保持最后状态;连续不可用达到 3 秒强制提交 `STOPPED`;恢复后重新确认候选。
|
|
343
|
+
SDK 提供 `OFFICIAL_DRIVING_EXPRESSION_FALLBACKS`,对应的默认 Emoji 为:
|
|
147
344
|
|
|
148
|
-
|
|
345
|
+
| 状态 | 回退内容 |
|
|
346
|
+
| -------------------- | -------- |
|
|
347
|
+
| `STOPPED` | 😐 |
|
|
348
|
+
| `STEADY_DRIVING` | 😊 |
|
|
349
|
+
| `ACCELERATION` | 😄 |
|
|
350
|
+
| `RAPID_ACCELERATION` | 😲 |
|
|
351
|
+
| `BRAKING` | 😣 |
|
|
352
|
+
| `SUDDEN_BRAKING` | 😱 |
|
|
353
|
+
| `LEFT_TURN` | 🤨 |
|
|
354
|
+
| `RIGHT_TURN` | 🤨 |
|
|
149
355
|
|
|
150
|
-
|
|
356
|
+
官方母版必须显式把它传给播放器:
|
|
151
357
|
|
|
152
358
|
```ts
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
359
|
+
import { OFFICIAL_DRIVING_EXPRESSION_FALLBACKS, createDrivingExpressionPlayer } from '@ziztechnology/dial-library';
|
|
360
|
+
|
|
361
|
+
const player = createDrivingExpressionPlayer(container, {
|
|
362
|
+
config,
|
|
363
|
+
fallbacks: OFFICIAL_DRIVING_EXPRESSION_FALLBACKS,
|
|
364
|
+
onDiagnostic(event) {
|
|
365
|
+
if (event.type === 'media_fallback_applied') {
|
|
366
|
+
console.warn(`${event.status} 已使用 ${event.kind} 回退素材`);
|
|
367
|
+
}
|
|
368
|
+
},
|
|
156
369
|
});
|
|
157
370
|
```
|
|
158
371
|
|
|
159
|
-
|
|
372
|
+
回退是**选择加入**的:第三方不传 `fallbacks`,SDK 就不会自动使用官方 Emoji。素材缺失、URL 非法、网络失败、超时或解码失败时,播放器会进入错误状态并发送 `media_load_failed` 诊断事件。如果你对你开发的表盘的样式一致性比较高,你可以编写自己的回退。
|
|
160
373
|
|
|
161
|
-
|
|
374
|
+
也可以只为部分状态提供自己的回退:
|
|
162
375
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
376
|
+
```ts
|
|
377
|
+
const player = createDrivingExpressionPlayer(container, {
|
|
378
|
+
config,
|
|
379
|
+
fallbacks: {
|
|
380
|
+
STOPPED: { kind: 'emoji', text: '😐' },
|
|
381
|
+
STEADY_DRIVING: { kind: 'emoji', text: '😊' },
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
```
|
|
171
385
|
|
|
172
|
-
|
|
386
|
+
播放器默认等待素材 5 秒,失败后按 15 秒退避重试。可以通过 `loadTimeoutMs` 和 `retryBackoffMs` 调整。Live Photo 的 motion 加载失败时也会应用该状态的显式回退,而不会一直停留在 cover。
|
|
173
387
|
|
|
174
|
-
|
|
388
|
+
### 配置本身无效时
|
|
175
389
|
|
|
176
|
-
|
|
390
|
+
`readDrivingExpressionsConfig()` 优先读取 Runtime 注入的 `__TOOOONY_DRIVING_EXPRESSIONS__`,并兼容旧的 `__TOOOONY_FACE_CONFIG__` 路径。配置不存在或无效时返回 `null`,不会替你补齐默认值:
|
|
177
391
|
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
392
|
+
```ts
|
|
393
|
+
const config = readDrivingExpressionsConfig();
|
|
394
|
+
|
|
395
|
+
if (!config) {
|
|
396
|
+
// 由表盘决定:显示静态占位、隐藏组件或终止初始化。
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
schema v1 要求 `states` 恰好包含全部八种状态。支持的单个状态素材为:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
type StructuredDrivingExpressionMedia =
|
|
404
|
+
| { kind: 'emoji'; text: string }
|
|
405
|
+
| { kind: 'image'; mediaAssetId?: number; url: string; mimeType?: string }
|
|
406
|
+
| {
|
|
407
|
+
kind: 'video';
|
|
408
|
+
mediaAssetId?: number;
|
|
409
|
+
url: string;
|
|
410
|
+
coverUrl?: string;
|
|
411
|
+
mimeType?: string;
|
|
412
|
+
}
|
|
413
|
+
| {
|
|
414
|
+
kind: 'live_photo';
|
|
415
|
+
mediaAssetId?: number;
|
|
416
|
+
coverUrl: string;
|
|
417
|
+
motionUrl: string;
|
|
418
|
+
}
|
|
419
|
+
| { kind: 'tgs'; mediaAssetId?: number; url: string; coverUrl?: string };
|
|
182
420
|
```
|
|
183
421
|
|
|
184
|
-
|
|
422
|
+
媒体 URL 必须是长期有效的公网 HTTPS 地址。解析器会拒绝 `http:`、`data:`、`blob:`、localhost、内网或保留地址,以及带常见临时签名参数的 URL。服务端仍应根据 `mediaAssetId` 和用户身份重建可信 URL;前端校验不能替代服务端所有权和域名校验。
|
|
423
|
+
|
|
424
|
+
旧版表盘曾使用裸图片 URL。只有迁移旧配置时,才应显式开启兼容:
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
import { parseDrivingExpressionsConfig } from '@ziztechnology/dial-library';
|
|
428
|
+
|
|
429
|
+
const config = parseDrivingExpressionsConfig(rawConfig, {
|
|
430
|
+
allowLegacyImageUrls: true,
|
|
431
|
+
});
|
|
432
|
+
```
|
package/dist/index.d.mts
CHANGED
|
@@ -73,6 +73,9 @@ interface DrivingStatusTuning {
|
|
|
73
73
|
longitudinalThreshold: number;
|
|
74
74
|
turnYawThreshold: number;
|
|
75
75
|
turnLateralThreshold: number;
|
|
76
|
+
motionWindowMs: number;
|
|
77
|
+
motionMinimumSamples: number;
|
|
78
|
+
drivingMotionThreshold: number;
|
|
76
79
|
stoppedMotionThreshold: number;
|
|
77
80
|
stoppedGyroThreshold: number;
|
|
78
81
|
hysteresisRatio: number;
|
package/dist/index.mjs
CHANGED
|
@@ -5,10 +5,13 @@ const DEFAULT_DRIVING_STATUS_TUNING = Object.freeze({
|
|
|
5
5
|
maxSampleAgeMs: 1e3,
|
|
6
6
|
rapidLongitudinalThreshold: 3,
|
|
7
7
|
longitudinalThreshold: 1,
|
|
8
|
-
turnYawThreshold: .
|
|
9
|
-
turnLateralThreshold:
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
turnYawThreshold: .5,
|
|
9
|
+
turnLateralThreshold: 2.5,
|
|
10
|
+
motionWindowMs: 1e3,
|
|
11
|
+
motionMinimumSamples: 5,
|
|
12
|
+
drivingMotionThreshold: .4,
|
|
13
|
+
stoppedMotionThreshold: .25,
|
|
14
|
+
stoppedGyroThreshold: .1,
|
|
12
15
|
hysteresisRatio: .75,
|
|
13
16
|
smoothingWindowSize: 3,
|
|
14
17
|
confirmationSamples: Object.freeze({
|
|
@@ -56,8 +59,12 @@ const resolveDrivingStatusTuning = (overrides = {}) => {
|
|
|
56
59
|
assertPositiveFinite("longitudinalThreshold", tuning.longitudinalThreshold);
|
|
57
60
|
assertPositiveFinite("turnYawThreshold", tuning.turnYawThreshold);
|
|
58
61
|
assertPositiveFinite("turnLateralThreshold", tuning.turnLateralThreshold);
|
|
62
|
+
assertPositiveFinite("motionWindowMs", tuning.motionWindowMs);
|
|
63
|
+
if (!Number.isInteger(tuning.motionMinimumSamples) || tuning.motionMinimumSamples < 1) throw new RangeError("motionMinimumSamples must be a positive integer");
|
|
64
|
+
assertPositiveFinite("drivingMotionThreshold", tuning.drivingMotionThreshold);
|
|
59
65
|
assertPositiveFinite("stoppedMotionThreshold", tuning.stoppedMotionThreshold);
|
|
60
66
|
assertPositiveFinite("stoppedGyroThreshold", tuning.stoppedGyroThreshold);
|
|
67
|
+
if (tuning.drivingMotionThreshold <= tuning.stoppedMotionThreshold) throw new RangeError("drivingMotionThreshold must be greater than stoppedMotionThreshold");
|
|
61
68
|
if (!Number.isInteger(tuning.smoothingWindowSize) || tuning.smoothingWindowSize < 1) throw new RangeError("smoothingWindowSize must be a positive integer");
|
|
62
69
|
if (tuning.rapidLongitudinalThreshold < tuning.longitudinalThreshold) throw new RangeError("rapidLongitudinalThreshold must be greater than or equal to longitudinalThreshold");
|
|
63
70
|
if (!Number.isFinite(tuning.hysteresisRatio) || tuning.hysteresisRatio <= 0 || tuning.hysteresisRatio >= 1) throw new RangeError("hysteresisRatio must be greater than 0 and less than 1");
|
|
@@ -92,7 +99,7 @@ const classifyDrivingMetrics = (metrics, tuning = DEFAULT_DRIVING_STATUS_TUNING)
|
|
|
92
99
|
if (Math.abs(lateral) >= tuning.turnLateralThreshold) return lateral < 0 ? "LEFT_TURN" : "RIGHT_TURN";
|
|
93
100
|
if (longitudinal <= -tuning.longitudinalThreshold) return "BRAKING";
|
|
94
101
|
if (longitudinal >= tuning.longitudinalThreshold) return "ACCELERATION";
|
|
95
|
-
if (motionIntensity
|
|
102
|
+
if (motionIntensity < tuning.drivingMotionThreshold && gyroMagnitude <= tuning.stoppedGyroThreshold) return "STOPPED";
|
|
96
103
|
return "STEADY_DRIVING";
|
|
97
104
|
};
|
|
98
105
|
const availableFreshVector = (metric, capturedAtMs, maxSampleAgeMs) => {
|
|
@@ -628,6 +635,7 @@ var DefaultDrivingStatusController = class {
|
|
|
628
635
|
constructor(options) {
|
|
629
636
|
this.listeners = /* @__PURE__ */ new Set();
|
|
630
637
|
this.metricWindow = [];
|
|
638
|
+
this.motionWindow = [];
|
|
631
639
|
this.candidate = null;
|
|
632
640
|
this.candidateConfirmationCount = 0;
|
|
633
641
|
this.lastAvailableAtMs = null;
|
|
@@ -662,6 +670,7 @@ var DefaultDrivingStatusController = class {
|
|
|
662
670
|
const initialStatus = options.initialStatus ?? "STOPPED";
|
|
663
671
|
if (!CAR_RUNNING_STATUSES.includes(initialStatus)) throw new TypeError(`Unsupported initialStatus: ${initialStatus}`);
|
|
664
672
|
this.status = initialStatus;
|
|
673
|
+
this.baseStatus = isBaseStatus(initialStatus) ? initialStatus : "STOPPED";
|
|
665
674
|
this.statusCommittedAtMs = this.clock.now();
|
|
666
675
|
}
|
|
667
676
|
start() {
|
|
@@ -688,6 +697,7 @@ var DefaultDrivingStatusController = class {
|
|
|
688
697
|
this.destroyed = true;
|
|
689
698
|
this.listeners.clear();
|
|
690
699
|
this.metricWindow.length = 0;
|
|
700
|
+
this.motionWindow.length = 0;
|
|
691
701
|
}
|
|
692
702
|
getSnapshot() {
|
|
693
703
|
return {
|
|
@@ -743,8 +753,9 @@ var DefaultDrivingStatusController = class {
|
|
|
743
753
|
const payload = await this.sensorProvider(pendingRead.signal);
|
|
744
754
|
if (!this.polling || generation !== this.generation || pendingRead.signal.aborted) return;
|
|
745
755
|
const metrics = extractDrivingSnapshotMetrics(payload, this.tuning);
|
|
746
|
-
|
|
747
|
-
|
|
756
|
+
const accelerometerSample = extractFreshAccelerometerSample(payload, this.tuning.maxSampleAgeMs);
|
|
757
|
+
if (metrics === null || accelerometerSample === null) this.processUnavailable("invalid_or_stale_snapshot");
|
|
758
|
+
else this.processAvailable(metrics, accelerometerSample);
|
|
748
759
|
} catch (error) {
|
|
749
760
|
if (!this.polling || generation !== this.generation || pendingRead.signal.aborted || isAbortError$1(error)) return;
|
|
750
761
|
this.processUnavailable("provider_error");
|
|
@@ -753,7 +764,7 @@ var DefaultDrivingStatusController = class {
|
|
|
753
764
|
if (this.polling && generation === this.generation) this.schedulePoll(this.pollIntervalMs, generation);
|
|
754
765
|
}
|
|
755
766
|
}
|
|
756
|
-
processAvailable(rawMetrics) {
|
|
767
|
+
processAvailable(rawMetrics, accelerometerSample) {
|
|
757
768
|
const now = this.clock.now();
|
|
758
769
|
this.lastAvailableAtMs = now;
|
|
759
770
|
this.unavailableSinceMs = null;
|
|
@@ -767,14 +778,26 @@ var DefaultDrivingStatusController = class {
|
|
|
767
778
|
this.metricWindow.push(rawMetrics);
|
|
768
779
|
while (this.metricWindow.length > Math.floor(this.tuning.smoothingWindowSize)) this.metricWindow.shift();
|
|
769
780
|
const smoothedMetrics = averageMetrics(this.metricWindow);
|
|
770
|
-
const
|
|
781
|
+
const motionIntensity = this.recordMotionSample(accelerometerSample, rawMetrics.capturedAtMs);
|
|
782
|
+
const nextCandidate = classifyControllerCandidate(rawMetrics, smoothedMetrics, motionIntensity, this.status, this.baseStatus, this.tuning);
|
|
771
783
|
this.processCandidate(nextCandidate, now);
|
|
772
784
|
}
|
|
785
|
+
recordMotionSample(sample, capturedAtMs) {
|
|
786
|
+
if (!this.motionWindow.some((existing) => existing.sampledAtMs === sample.sampledAtMs)) {
|
|
787
|
+
this.motionWindow.push(sample);
|
|
788
|
+
this.motionWindow.sort((left, right) => left.sampledAtMs - right.sampledAtMs);
|
|
789
|
+
}
|
|
790
|
+
const cutoffMs = capturedAtMs - this.tuning.motionWindowMs;
|
|
791
|
+
while (this.motionWindow[0] && this.motionWindow[0].sampledAtMs <= cutoffMs) this.motionWindow.shift();
|
|
792
|
+
if (this.motionWindow.length < this.tuning.motionMinimumSamples) return null;
|
|
793
|
+
return combinedAxisStandardDeviation(this.motionWindow);
|
|
794
|
+
}
|
|
773
795
|
processUnavailable(category) {
|
|
774
796
|
const now = this.clock.now();
|
|
775
797
|
if (this.unavailableSinceMs === null) this.unavailableSinceMs = now;
|
|
776
798
|
const unavailableForMs = Math.max(0, now - this.unavailableSinceMs);
|
|
777
799
|
this.metricWindow.length = 0;
|
|
800
|
+
this.motionWindow.length = 0;
|
|
778
801
|
this.setCandidate(null, now);
|
|
779
802
|
this.emitDiagnostic({
|
|
780
803
|
type: "sample_unavailable",
|
|
@@ -828,6 +851,7 @@ var DefaultDrivingStatusController = class {
|
|
|
828
851
|
if (nextStatus === this.status) return;
|
|
829
852
|
const previousStatus = this.status;
|
|
830
853
|
this.status = nextStatus;
|
|
854
|
+
if (isBaseStatus(nextStatus)) this.baseStatus = nextStatus;
|
|
831
855
|
this.statusCommittedAtMs = now;
|
|
832
856
|
this.setCandidate(null, now);
|
|
833
857
|
this.heldCandidate = null;
|
|
@@ -867,6 +891,7 @@ var DefaultDrivingStatusController = class {
|
|
|
867
891
|
}
|
|
868
892
|
resetTransientRecognition() {
|
|
869
893
|
this.metricWindow.length = 0;
|
|
894
|
+
this.motionWindow.length = 0;
|
|
870
895
|
this.candidate = null;
|
|
871
896
|
this.candidateConfirmationCount = 0;
|
|
872
897
|
this.unavailableSinceMs = null;
|
|
@@ -885,26 +910,28 @@ var DefaultDrivingStatusController = class {
|
|
|
885
910
|
this.unsubscribePageLifecycle = null;
|
|
886
911
|
}
|
|
887
912
|
};
|
|
888
|
-
const classifyControllerCandidate = (raw, smoothed, current, tuning) => {
|
|
889
|
-
const entry = classifyEntryCandidate(raw, smoothed,
|
|
890
|
-
if (!retainsCurrentStatus(raw, smoothed, current, tuning)) return entry;
|
|
913
|
+
const classifyControllerCandidate = (raw, smoothed, motionIntensity, current, baseStatus, tuning) => {
|
|
914
|
+
const entry = classifyEntryCandidate(raw, smoothed, motionIntensity, baseStatus, tuning);
|
|
915
|
+
if (!retainsCurrentStatus(raw, smoothed, motionIntensity, current, tuning)) return entry;
|
|
891
916
|
if (entry === current) return current;
|
|
892
917
|
if (CAR_RUNNING_STATUS_PRIORITY[entry] > CAR_RUNNING_STATUS_PRIORITY[current]) return entry;
|
|
893
918
|
if (isOppositeTurn(entry, current)) return entry;
|
|
894
919
|
if ((entry === "STOPPED" || entry === "STEADY_DRIVING") && (current === "STOPPED" || current === "STEADY_DRIVING")) return entry;
|
|
895
920
|
return current;
|
|
896
921
|
};
|
|
897
|
-
const classifyEntryCandidate = (raw, smoothed,
|
|
922
|
+
const classifyEntryCandidate = (raw, smoothed, motionIntensity, baseStatus, tuning) => {
|
|
898
923
|
if (raw.longitudinal <= -tuning.rapidLongitudinalThreshold) return "SUDDEN_BRAKING";
|
|
899
924
|
if (raw.longitudinal >= tuning.rapidLongitudinalThreshold) return "RAPID_ACCELERATION";
|
|
900
925
|
if (Math.abs(smoothed.yaw) >= tuning.turnYawThreshold) return smoothed.yaw > 0 ? "LEFT_TURN" : "RIGHT_TURN";
|
|
901
926
|
if (Math.abs(smoothed.lateral) >= tuning.turnLateralThreshold) return smoothed.lateral < 0 ? "LEFT_TURN" : "RIGHT_TURN";
|
|
902
927
|
if (smoothed.longitudinal <= -tuning.longitudinalThreshold) return "BRAKING";
|
|
903
928
|
if (smoothed.longitudinal >= tuning.longitudinalThreshold) return "ACCELERATION";
|
|
904
|
-
|
|
905
|
-
|
|
929
|
+
if (motionIntensity === null) return baseStatus;
|
|
930
|
+
if (motionIntensity > tuning.drivingMotionThreshold || smoothed.gyroMagnitude > tuning.stoppedGyroThreshold) return "STEADY_DRIVING";
|
|
931
|
+
if (motionIntensity < tuning.stoppedMotionThreshold) return "STOPPED";
|
|
932
|
+
return baseStatus;
|
|
906
933
|
};
|
|
907
|
-
const retainsCurrentStatus = (raw, smoothed, current, tuning) => {
|
|
934
|
+
const retainsCurrentStatus = (raw, smoothed, motionIntensity, current, tuning) => {
|
|
908
935
|
const ratio = tuning.hysteresisRatio;
|
|
909
936
|
switch (current) {
|
|
910
937
|
case "SUDDEN_BRAKING": return raw.longitudinal <= -tuning.rapidLongitudinalThreshold * ratio;
|
|
@@ -913,8 +940,8 @@ const retainsCurrentStatus = (raw, smoothed, current, tuning) => {
|
|
|
913
940
|
case "RIGHT_TURN": return smoothed.yaw <= -tuning.turnYawThreshold * ratio || smoothed.lateral >= tuning.turnLateralThreshold * ratio;
|
|
914
941
|
case "BRAKING": return smoothed.longitudinal <= -tuning.longitudinalThreshold * ratio;
|
|
915
942
|
case "ACCELERATION": return smoothed.longitudinal >= tuning.longitudinalThreshold * ratio;
|
|
916
|
-
case "STOPPED": return
|
|
917
|
-
case "STEADY_DRIVING": return
|
|
943
|
+
case "STOPPED": return (motionIntensity === null || motionIntensity <= tuning.drivingMotionThreshold) && smoothed.gyroMagnitude <= tuning.stoppedGyroThreshold;
|
|
944
|
+
case "STEADY_DRIVING": return motionIntensity === null || motionIntensity >= tuning.stoppedMotionThreshold || smoothed.gyroMagnitude > tuning.stoppedGyroThreshold;
|
|
918
945
|
}
|
|
919
946
|
};
|
|
920
947
|
const averageMetrics = (metrics) => {
|
|
@@ -941,6 +968,45 @@ const averageMetrics = (metrics) => {
|
|
|
941
968
|
result.gyroMagnitude /= count;
|
|
942
969
|
return result;
|
|
943
970
|
};
|
|
971
|
+
const extractFreshAccelerometerSample = (info, maxSampleAgeMs) => {
|
|
972
|
+
const metric = info?.accelerometer;
|
|
973
|
+
if (!metric?.available || !Number.isFinite(info.capturedAtMs) || !Number.isFinite(metric.sampledAtMs)) return null;
|
|
974
|
+
const ageMs = info.capturedAtMs - metric.sampledAtMs;
|
|
975
|
+
if (ageMs < 0 || ageMs > maxSampleAgeMs) return null;
|
|
976
|
+
const { x, y, z } = metric.value;
|
|
977
|
+
if (![
|
|
978
|
+
x,
|
|
979
|
+
y,
|
|
980
|
+
z
|
|
981
|
+
].every(Number.isFinite)) return null;
|
|
982
|
+
return {
|
|
983
|
+
sampledAtMs: metric.sampledAtMs,
|
|
984
|
+
value: metric.value
|
|
985
|
+
};
|
|
986
|
+
};
|
|
987
|
+
const combinedAxisStandardDeviation = (samples) => {
|
|
988
|
+
const count = samples.length;
|
|
989
|
+
let meanX = 0;
|
|
990
|
+
let meanY = 0;
|
|
991
|
+
let meanZ = 0;
|
|
992
|
+
for (const sample of samples) {
|
|
993
|
+
meanX += sample.value.x;
|
|
994
|
+
meanY += sample.value.y;
|
|
995
|
+
meanZ += sample.value.z;
|
|
996
|
+
}
|
|
997
|
+
meanX /= count;
|
|
998
|
+
meanY /= count;
|
|
999
|
+
meanZ /= count;
|
|
1000
|
+
let varianceX = 0;
|
|
1001
|
+
let varianceY = 0;
|
|
1002
|
+
let varianceZ = 0;
|
|
1003
|
+
for (const sample of samples) {
|
|
1004
|
+
varianceX += (sample.value.x - meanX) ** 2;
|
|
1005
|
+
varianceY += (sample.value.y - meanY) ** 2;
|
|
1006
|
+
varianceZ += (sample.value.z - meanZ) ** 2;
|
|
1007
|
+
}
|
|
1008
|
+
return Math.sqrt((varianceX + varianceY + varianceZ) / count);
|
|
1009
|
+
};
|
|
944
1010
|
const defaultSensorProvider = () => {
|
|
945
1011
|
const runtimeGlobal = globalThis;
|
|
946
1012
|
if (typeof runtimeGlobal.unifiedSensorInfo !== "function") return Promise.reject(/* @__PURE__ */ new Error("window.unifiedSensorInfo is unavailable"));
|
|
@@ -952,6 +1018,7 @@ const systemClock = {
|
|
|
952
1018
|
clearTimeout: (handle) => globalThis.clearTimeout(handle)
|
|
953
1019
|
};
|
|
954
1020
|
const isOppositeTurn = (candidate, current) => candidate === "LEFT_TURN" && current === "RIGHT_TURN" || candidate === "RIGHT_TURN" && current === "LEFT_TURN";
|
|
1021
|
+
const isBaseStatus = (status) => status === "STOPPED" || status === "STEADY_DRIVING";
|
|
955
1022
|
const isAbortError$1 = (error) => error instanceof Error && error.name === "AbortError";
|
|
956
1023
|
const requirePositiveFinite = (name, value) => {
|
|
957
1024
|
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${name} must be a positive finite number`);
|