@ziztechnology/dial-library 0.0.2 → 0.0.4
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 +348 -98
- package/dist/index.d.mts +4 -9
- package/dist/index.mjs +87 -20
- package/package.json +9 -9
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,237 @@ 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
|
+
TGS 使用 SDK 内置的 SVG light 播放器,并且只在首次显示 TGS 素材时动态加载。表盘项目不需要安装、导入或配置 `lottie-web`,也不需要为构建工具添加 Lottie alias。
|
|
315
|
+
|
|
316
|
+
## 行车状态的回退
|
|
317
|
+
|
|
318
|
+
行车状态有两类彼此独立的回退:传感器不可用时的**状态回退**,以及表情素材失败时的**媒体回退**。
|
|
319
|
+
|
|
320
|
+
### 传感器不可用时回退到静止
|
|
321
|
+
|
|
322
|
+
控制器遇到一次读取失败、非法快照或过期样本时,会暂时保持最后一个已提交状态,避免页面闪烁。连续不可用达到 3 秒后,控制器才提交 `STOPPED`:
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
const controller = createDrivingStatusController({
|
|
129
326
|
unavailableFallbackMs: 3_000,
|
|
130
|
-
initialStatus: 'STOPPED',
|
|
131
|
-
tuning: { turnYawThreshold: 0.32 },
|
|
132
327
|
onDiagnostic(event) {
|
|
133
|
-
|
|
328
|
+
if (event.type === 'fallback_to_stopped') {
|
|
329
|
+
console.warn(`传感器已连续不可用 ${event.unavailableForMs}ms`);
|
|
330
|
+
}
|
|
134
331
|
},
|
|
135
332
|
});
|
|
333
|
+
|
|
334
|
+
controller.subscribe((event) => {
|
|
335
|
+
if (event.reason === 'unavailable_fallback') {
|
|
336
|
+
console.log('已回退到 STOPPED');
|
|
337
|
+
}
|
|
338
|
+
});
|
|
136
339
|
```
|
|
137
340
|
|
|
138
|
-
|
|
341
|
+
传感器恢复后,控制器会清除不可用状态并重新确认候选,不会立即把单个样本提交给 UI。`unavailableFallbackMs` 必须是大于 0 的有限数值。
|
|
342
|
+
|
|
343
|
+
### 素材失败时使用显式回退
|
|
139
344
|
|
|
140
|
-
|
|
141
|
-
- 普通动作短窗口平滑,急动作检查未平滑峰值。
|
|
142
|
-
- 急刹车 > 急加速 > 转弯 > 刹车 > 加速 > 静止/匀速的抢占优先级。
|
|
143
|
-
- 急动作 1 样本、转弯/普通动作 2 样本、匀速 3 样本、静止 5 样本的连续确认。
|
|
144
|
-
- 动作退出阈值为入口阈值的 `0.75`,并对静止/匀速使用反向滞回区间。
|
|
145
|
-
- 每个已提交状态的最短展示时间;更高优先级候选可抢占,锁期间继续采样。
|
|
146
|
-
- 单次/短暂不可用保持最后状态;连续不可用达到 3 秒强制提交 `STOPPED`;恢复后重新确认候选。
|
|
345
|
+
SDK 提供 `OFFICIAL_DRIVING_EXPRESSION_FALLBACKS`,对应的默认 Emoji 为:
|
|
147
346
|
|
|
148
|
-
|
|
347
|
+
| 状态 | 回退内容 |
|
|
348
|
+
| -------------------- | -------- |
|
|
349
|
+
| `STOPPED` | 😐 |
|
|
350
|
+
| `STEADY_DRIVING` | 😊 |
|
|
351
|
+
| `ACCELERATION` | 😄 |
|
|
352
|
+
| `RAPID_ACCELERATION` | 😲 |
|
|
353
|
+
| `BRAKING` | 😣 |
|
|
354
|
+
| `SUDDEN_BRAKING` | 😱 |
|
|
355
|
+
| `LEFT_TURN` | 🤨 |
|
|
356
|
+
| `RIGHT_TURN` | 🤨 |
|
|
149
357
|
|
|
150
|
-
|
|
358
|
+
官方母版必须显式把它传给播放器:
|
|
151
359
|
|
|
152
360
|
```ts
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
361
|
+
import { OFFICIAL_DRIVING_EXPRESSION_FALLBACKS, createDrivingExpressionPlayer } from '@ziztechnology/dial-library';
|
|
362
|
+
|
|
363
|
+
const player = createDrivingExpressionPlayer(container, {
|
|
364
|
+
config,
|
|
365
|
+
fallbacks: OFFICIAL_DRIVING_EXPRESSION_FALLBACKS,
|
|
366
|
+
onDiagnostic(event) {
|
|
367
|
+
if (event.type === 'media_fallback_applied') {
|
|
368
|
+
console.warn(`${event.status} 已使用 ${event.kind} 回退素材`);
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
回退是**选择加入**的:第三方不传 `fallbacks`,SDK 就不会自动使用官方 Emoji。素材缺失、URL 非法、网络失败、超时或解码失败时,播放器会进入错误状态并发送 `media_load_failed` 诊断事件。如果你对你开发的表盘的样式一致性比较高,你可以编写自己的回退。
|
|
375
|
+
|
|
376
|
+
也可以只为部分状态提供自己的回退:
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
const player = createDrivingExpressionPlayer(container, {
|
|
380
|
+
config,
|
|
381
|
+
fallbacks: {
|
|
382
|
+
STOPPED: { kind: 'emoji', text: '😐' },
|
|
383
|
+
STEADY_DRIVING: { kind: 'emoji', text: '😊' },
|
|
384
|
+
},
|
|
156
385
|
});
|
|
157
386
|
```
|
|
158
387
|
|
|
159
|
-
|
|
388
|
+
播放器默认等待素材 5 秒,失败后按 15 秒退避重试。可以通过 `loadTimeoutMs` 和 `retryBackoffMs` 调整。Live Photo 的 motion 加载失败时也会应用该状态的显式回退,而不会一直停留在 cover。
|
|
160
389
|
|
|
161
|
-
|
|
390
|
+
### 配置本身无效时
|
|
162
391
|
|
|
163
|
-
|
|
164
|
-
- 图片/GIF 使用原生 `<img>`,保留 GIF 动画。
|
|
165
|
-
- 视频复用一个 `<video muted loop playsinline>`;离开状态时停止并清空,重新进入从 `0` 开始。
|
|
166
|
-
- Live Photo 先显示 cover,motion 就绪后使用静音循环视频;motion 失败时应用该状态 fallback,不会永久停在 cover。
|
|
167
|
-
- TGS 以无凭据 CORS fetch 读取,限制压缩/解压大小、图层数、帧率和帧数;使用 `fflate` 解压并通过 `lottie-web` 循环渲染。
|
|
168
|
-
- TGS 仅接受约定的 TGS、gzip 或 JSON Content-Type;HTML 和无关 MIME 会立即回退。
|
|
169
|
-
- 默认加载超时 5 秒。失败后应用显式 fallback,并按退避时间重试;状态离开、pause 或 destroy 会取消网络、预加载和动画。
|
|
170
|
-
- 轻量预加载顺序为当前状态、`STOPPED`、其余状态;只缓存图片/cover 提示,不同时解码八个视频。
|
|
392
|
+
`readDrivingExpressionsConfig()` 优先读取 Runtime 注入的 `__TOOOONY_DRIVING_EXPRESSIONS__`,并兼容旧的 `__TOOOONY_FACE_CONFIG__` 路径。配置不存在或无效时返回 `null`,不会替你补齐默认值:
|
|
171
393
|
|
|
172
|
-
|
|
394
|
+
```ts
|
|
395
|
+
const config = readDrivingExpressionsConfig();
|
|
173
396
|
|
|
174
|
-
|
|
397
|
+
if (!config) {
|
|
398
|
+
// 由表盘决定:显示静态占位、隐藏组件或终止初始化。
|
|
399
|
+
}
|
|
400
|
+
```
|
|
175
401
|
|
|
176
|
-
|
|
402
|
+
schema v1 要求 `states` 恰好包含全部八种状态。支持的单个状态素材为:
|
|
177
403
|
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
404
|
+
```ts
|
|
405
|
+
type StructuredDrivingExpressionMedia =
|
|
406
|
+
| { kind: 'emoji'; text: string }
|
|
407
|
+
| { kind: 'image'; mediaAssetId?: number; url: string; mimeType?: string }
|
|
408
|
+
| {
|
|
409
|
+
kind: 'video';
|
|
410
|
+
mediaAssetId?: number;
|
|
411
|
+
url: string;
|
|
412
|
+
coverUrl?: string;
|
|
413
|
+
mimeType?: string;
|
|
414
|
+
}
|
|
415
|
+
| {
|
|
416
|
+
kind: 'live_photo';
|
|
417
|
+
mediaAssetId?: number;
|
|
418
|
+
coverUrl: string;
|
|
419
|
+
motionUrl: string;
|
|
420
|
+
}
|
|
421
|
+
| { kind: 'tgs'; mediaAssetId?: number; url: string; coverUrl?: string };
|
|
182
422
|
```
|
|
183
423
|
|
|
184
|
-
|
|
424
|
+
媒体 URL 必须是长期有效的公网 HTTPS 地址。解析器会拒绝 `http:`、`data:`、`blob:`、localhost、内网或保留地址,以及带常见临时签名参数的 URL。服务端仍应根据 `mediaAssetId` 和用户身份重建可信 URL;前端校验不能替代服务端所有权和域名校验。
|
|
425
|
+
|
|
426
|
+
旧版表盘曾使用裸图片 URL。只有迁移旧配置时,才应显式开启兼容:
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
import { parseDrivingExpressionsConfig } from '@ziztechnology/dial-library';
|
|
430
|
+
|
|
431
|
+
const config = parseDrivingExpressionsConfig(rawConfig, {
|
|
432
|
+
allowLegacyImageUrls: true,
|
|
433
|
+
});
|
|
434
|
+
```
|
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;
|
|
@@ -279,20 +282,12 @@ interface DrivingExpressionPlayerSnapshot {
|
|
|
279
282
|
mediaKind: StructuredDrivingExpressionMedia['kind'] | null;
|
|
280
283
|
errorCategory: DrivingExpressionPlayerErrorCategory | null;
|
|
281
284
|
}
|
|
282
|
-
interface DrivingTgsAnimationHandle {
|
|
283
|
-
destroy(): void;
|
|
284
|
-
pause?(): void;
|
|
285
|
-
play?(): void;
|
|
286
|
-
restart?(): void;
|
|
287
|
-
}
|
|
288
|
-
type DrivingTgsRenderer = (container: HTMLElement, animationData: Record<string, unknown>, signal: AbortSignal) => DrivingTgsAnimationHandle | Promise<DrivingTgsAnimationHandle>;
|
|
289
285
|
interface DrivingExpressionPlayerOptions {
|
|
290
286
|
config: DrivingExpressionsConfigInput;
|
|
291
287
|
fallbacks?: Partial<Readonly<Record<CarRunningStatus, StructuredDrivingExpressionMedia>>>;
|
|
292
288
|
loadTimeoutMs?: number;
|
|
293
289
|
retryBackoffMs?: number;
|
|
294
290
|
fit?: 'contain' | 'cover';
|
|
295
|
-
tgsRenderer?: DrivingTgsRenderer;
|
|
296
291
|
fetch?: typeof globalThis.fetch;
|
|
297
292
|
onDiagnostic?: (event: DrivingExpressionPlayerDiagnosticEvent) => void;
|
|
298
293
|
maxTgsCompressedBytes?: number;
|
|
@@ -310,4 +305,4 @@ interface DrivingExpressionPlayer {
|
|
|
310
305
|
}
|
|
311
306
|
declare const createDrivingExpressionPlayer: (container: HTMLElement, options: DrivingExpressionPlayerOptions) => DrivingExpressionPlayer;
|
|
312
307
|
//#endregion
|
|
313
|
-
export { AvailableSensorMetric, BATTERY_HEALTH_LABELS, BatteryCapacityValue, BatteryHealth, BatteryPlugged, BatteryStateValue, BatteryStatus, CAR_RUNNING_LABELS, CAR_RUNNING_STATUSES, CAR_RUNNING_STATUS_PRIORITY, CarRunningStatus, CompleteDrivingExpressionsConfig, DEFAULT_DRIVING_STATUS_TUNING, DrivingControllerSnapshot, DrivingExpressionMedia, DrivingExpressionPlayer, DrivingExpressionPlayerDiagnosticEvent, DrivingExpressionPlayerErrorCategory, DrivingExpressionPlayerOptions, DrivingExpressionPlayerSnapshot, DrivingExpressionResolutionError, DrivingExpressionsConfig, DrivingExpressionsConfigError, DrivingExpressionsConfigErrorCode, DrivingExpressionsConfigInput, DrivingSensorProvider, DrivingSnapshotMetrics, DrivingStatusChangeReason, DrivingStatusClock, DrivingStatusController, DrivingStatusControllerOptions, DrivingStatusDiagnosticEvent, DrivingStatusEvent, DrivingStatusTuning, DrivingStatusTuningOverrides,
|
|
308
|
+
export { AvailableSensorMetric, BATTERY_HEALTH_LABELS, BatteryCapacityValue, BatteryHealth, BatteryPlugged, BatteryStateValue, BatteryStatus, CAR_RUNNING_LABELS, CAR_RUNNING_STATUSES, CAR_RUNNING_STATUS_PRIORITY, CarRunningStatus, CompleteDrivingExpressionsConfig, DEFAULT_DRIVING_STATUS_TUNING, DrivingControllerSnapshot, DrivingExpressionMedia, DrivingExpressionPlayer, DrivingExpressionPlayerDiagnosticEvent, DrivingExpressionPlayerErrorCategory, DrivingExpressionPlayerOptions, DrivingExpressionPlayerSnapshot, DrivingExpressionResolutionError, DrivingExpressionsConfig, DrivingExpressionsConfigError, DrivingExpressionsConfigErrorCode, DrivingExpressionsConfigInput, DrivingSensorProvider, DrivingSnapshotMetrics, DrivingStatusChangeReason, DrivingStatusClock, DrivingStatusController, DrivingStatusControllerOptions, DrivingStatusDiagnosticEvent, DrivingStatusEvent, DrivingStatusTuning, DrivingStatusTuningOverrides, EmojiDrivingExpressionMedia, ImageDrivingExpressionMedia, LivePhotoDrivingExpressionMedia, MediaLibraryRef, OFFICIAL_DRIVING_EXPRESSIONS_CONFIG, OFFICIAL_DRIVING_EXPRESSION_FALLBACKS, OrientationValue, ParseDrivingExpressionsOptions, StructuredDrivingExpressionMedia, TgsDrivingExpressionMedia, UnavailableSensorMetric, UnifiedSensorInfoPayload, UnifiedSensorMetric, UnifiedSensorUnavailableReason, Vector3Value, VideoDrivingExpressionMedia, checkRunningStatus, classifyDrivingMetrics, classifyDrivingSnapshot, createDrivingExpressionPlayer, createDrivingStatusController, extractDrivingSnapshotMetrics, isPublicDrivingMediaUrl, parseDrivingExpressionsConfig, readDrivingExpressionsConfig, resolveDrivingExpression, resolveDrivingExpressionStrict, resolveDrivingStatusTuning, tryParseDrivingExpressionsConfig, unifiedSensorInfo };
|
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`);
|
|
@@ -1004,7 +1071,7 @@ var DefaultDrivingExpressionPlayer = class {
|
|
|
1004
1071
|
this.loadTimeoutMs = positiveFinite("loadTimeoutMs", options.loadTimeoutMs ?? 5e3);
|
|
1005
1072
|
this.retryBackoffMs = positiveFinite("retryBackoffMs", options.retryBackoffMs ?? 15e3);
|
|
1006
1073
|
this.fit = options.fit ?? "contain";
|
|
1007
|
-
this.tgsRenderer =
|
|
1074
|
+
this.tgsRenderer = defaultTgsRenderer;
|
|
1008
1075
|
this.fetchImplementation = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
1009
1076
|
this.onDiagnostic = options.onDiagnostic;
|
|
1010
1077
|
this.maxTgsCompressedBytes = positiveFinite("maxTgsCompressedBytes", options.maxTgsCompressedBytes ?? 2 * 1024 * 1024);
|
|
@@ -1537,7 +1604,7 @@ const withTimeout = (promise, timeoutMs, signal) => new Promise((resolve, reject
|
|
|
1537
1604
|
});
|
|
1538
1605
|
const defaultTgsRenderer = async (container, animationData, signal) => {
|
|
1539
1606
|
if (signal.aborted) throw createAbortError();
|
|
1540
|
-
const module = await import("lottie-web");
|
|
1607
|
+
const module = await import("lottie-web/build/player/lottie_light.js");
|
|
1541
1608
|
if (signal.aborted) throw createAbortError();
|
|
1542
1609
|
const animation = module.default.loadAnimation({
|
|
1543
1610
|
animationData,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziztechnology/dial-library",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Tooony 表盘工具函数、方法",
|
|
6
6
|
"files": [
|
|
@@ -11,13 +11,6 @@
|
|
|
11
11
|
".": "./dist/index.mjs",
|
|
12
12
|
"./package.json": "./package.json"
|
|
13
13
|
},
|
|
14
|
-
"scripts": {
|
|
15
|
-
"build": "tsdown",
|
|
16
|
-
"dev": "tsdown --watch",
|
|
17
|
-
"fmt": "oxfmt",
|
|
18
|
-
"fmt:check": "oxfmt --check",
|
|
19
|
-
"test": "pnpm run build && node --test test/*.test.mjs"
|
|
20
|
-
},
|
|
21
14
|
"dependencies": {
|
|
22
15
|
"fflate": "^0.8.2",
|
|
23
16
|
"lottie-web": "^5.13.0"
|
|
@@ -26,5 +19,12 @@
|
|
|
26
19
|
"oxfmt": "^0.58.0",
|
|
27
20
|
"tsdown": "^0.22.0",
|
|
28
21
|
"typescript": "^6.0.3"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsdown",
|
|
25
|
+
"dev": "tsdown --watch",
|
|
26
|
+
"fmt": "oxfmt",
|
|
27
|
+
"fmt:check": "oxfmt --check",
|
|
28
|
+
"test": "pnpm run build && node --test test/*.test.mjs"
|
|
29
29
|
}
|
|
30
|
-
}
|
|
30
|
+
}
|