@ray-js/robot-data-stream 0.0.17-beta.9 → 0.1.0-beta.0
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 +23 -1
- package/lib/api/index.d.ts +1 -0
- package/lib/api/p2pApi.d.ts +30 -7
- package/lib/api/p2pApi.js +59 -23
- package/lib/api/sweeperP2p.d.ts +18 -1
- package/lib/api/sweeperP2p.js +177 -25
- package/lib/index.d.ts +15 -9
- package/lib/index.js +60 -3
- package/lib/mqtt/createCommonOptions.d.ts +2 -20
- package/lib/mqtt/createCommonOptions.js +17 -5
- package/lib/mqtt/defineRobotCommand.d.ts +52 -0
- package/lib/mqtt/defineRobotCommand.js +71 -0
- package/lib/mqtt/hooks/useStructuredMessage.d.ts +5 -1
- package/lib/mqtt/hooks/useStructuredMessage.js +22 -10
- package/lib/mqtt/index.d.ts +7 -1
- package/lib/mqtt/index.js +11 -1
- package/lib/mqtt/promise.d.ts +21 -6
- package/lib/mqtt/promise.js +47 -12
- package/lib/mqtt/type/protocols/carpetProtocol.d.ts +2 -0
- package/lib/mqtt/type/protocols/index.d.ts +2 -0
- package/lib/mqtt/type/protocols/index.js +3 -1
- package/lib/mqtt/type/protocols/thresholdProtocol.d.ts +22 -0
- package/lib/mqtt/type/protocols/thresholdProtocol.js +1 -0
- package/lib/mqtt/type/requestType.d.ts +4 -0
- package/lib/mqtt/type/requestType.js +7 -0
- package/lib/mqtt/useCarpet.js +12 -4
- package/lib/mqtt/useCarpetClean.js +6 -2
- package/lib/mqtt/useCruise.js +3 -1
- package/lib/mqtt/useCustomCommand.d.ts +39 -0
- package/lib/mqtt/useCustomCommand.js +45 -0
- package/lib/mqtt/useDeviceModel.js +3 -1
- package/lib/mqtt/useFurnitureModel.js +3 -1
- package/lib/mqtt/useHistoryMap.js +9 -3
- package/lib/mqtt/usePartDivision.js +3 -1
- package/lib/mqtt/usePartMerge.js +3 -1
- package/lib/mqtt/usePassword.js +6 -2
- package/lib/mqtt/usePurifierSchedule.js +3 -1
- package/lib/mqtt/useQuiteHours.js +3 -1
- package/lib/mqtt/useResetMap.js +3 -1
- package/lib/mqtt/useRoomProperty.js +3 -1
- package/lib/mqtt/useSchedule.js +3 -1
- package/lib/mqtt/useSelectRoomClean.js +3 -1
- package/lib/mqtt/useSpotClean.js +3 -1
- package/lib/mqtt/useSpotPolygons.js +3 -1
- package/lib/mqtt/useThreshold.d.ts +10 -0
- package/lib/mqtt/useThreshold.js +90 -0
- package/lib/mqtt/useVirtualArea.js +3 -1
- package/lib/mqtt/useVirtualWall.js +3 -1
- package/lib/mqtt/useVoice.js +3 -1
- package/lib/mqtt/useWifiMap.js +3 -1
- package/lib/mqtt/useZoneClean.js +3 -1
- package/lib/utils/index.d.ts +7 -7
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -76,4 +76,26 @@ export default function Home() {
|
|
|
76
76
|
);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
```
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Custom Commands (extension point for customized firmware)
|
|
82
|
+
|
|
83
|
+
Commands that only exist on a customized firmware fork do NOT belong in this library's built-in hooks. Declare, send and listen to them in your own repo:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { useCustomCommand, defineRobotCommand } from '@ray-js/robot-data-stream';
|
|
87
|
+
|
|
88
|
+
// One-off send (any reqType; must be used within MqttProvider)
|
|
89
|
+
const { sendCustomCommand } = useCustomCommand();
|
|
90
|
+
await sendCustomCommand<Res>('peakValleyChargeQry');
|
|
91
|
+
|
|
92
|
+
// Declarative: one file per command, returns { useSend, useResponse } shaped like built-in hooks
|
|
93
|
+
export const peakValleyCharge = defineRobotCommand<Req, Res>({
|
|
94
|
+
reqType: 'peakValleyChargeSet',
|
|
95
|
+
responseType: 'peakValleyChargeQry',
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Key semantics: MQTT + LAN JSON socket channels only (no DP `command_trans`); reqType + taskId response matching (when firmware echoes taskId, concurrent same-reqType requests do not cross-wire; responses without taskId pass through, degrading to first-response matching in that case); `data`-envelope unwrapping and `success`/`errCode` normalization are handled by the library; built-in protocol payload/response types are exported from the package root for type reuse. No runtime validation, no loading/retry — same contract as built-in hooks.
|
|
100
|
+
|
|
101
|
+
Full reference: `docs/reference/custom-commands.md`.
|
package/lib/api/index.d.ts
CHANGED
package/lib/api/p2pApi.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export type P2pErrorCode = 'QUERY_ALBUM_TIMEOUT' | 'PACKET_LENGTH_MISMATCH' | 'DOWNLOAD_STREAM_FAILED' | 'FIRST_FRAME_TIMEOUT' | 'NO_ALBUM_FILES' | string;
|
|
2
|
+
export interface P2pError {
|
|
3
|
+
code: P2pErrorCode;
|
|
4
|
+
message: string;
|
|
5
|
+
originalError?: any;
|
|
6
|
+
}
|
|
1
7
|
/**
|
|
2
8
|
* P2P 工具类
|
|
3
9
|
*/
|
|
@@ -6,10 +12,12 @@ export default class P2pApi {
|
|
|
6
12
|
isConnecting: boolean;
|
|
7
13
|
offSessionStatusChange: () => void;
|
|
8
14
|
onLogger: (type: 'warn' | 'error' | 'info', v: string) => void;
|
|
15
|
+
onError?: (error: P2pError) => void;
|
|
9
16
|
initTimeStamp: number;
|
|
10
17
|
devId: string;
|
|
11
18
|
traceId: string;
|
|
12
19
|
constructor();
|
|
20
|
+
emitError: (error: P2pError) => void;
|
|
13
21
|
/**
|
|
14
22
|
* 设备断开之后的重连
|
|
15
23
|
*/
|
|
@@ -58,7 +66,9 @@ export default class P2pApi {
|
|
|
58
66
|
* @param callback
|
|
59
67
|
* @returns
|
|
60
68
|
*/
|
|
61
|
-
onP2pStreamPacketReceive: (callback:
|
|
69
|
+
onP2pStreamPacketReceive: (callback: {
|
|
70
|
+
(...args: any[]): void;
|
|
71
|
+
}) => () => void;
|
|
62
72
|
/**
|
|
63
73
|
* 开始下载文件
|
|
64
74
|
* files : {"files":["filesname1", "filesname2", "filesname3" ]}
|
|
@@ -71,24 +81,32 @@ export default class P2pApi {
|
|
|
71
81
|
* @param callback
|
|
72
82
|
* @returns
|
|
73
83
|
*/
|
|
74
|
-
onDownloadProgressUpdate: (callback:
|
|
84
|
+
onDownloadProgressUpdate: (callback: {
|
|
85
|
+
(...args: any[]): void;
|
|
86
|
+
}) => () => void;
|
|
75
87
|
/**
|
|
76
88
|
* 注册多文件下载进度监听
|
|
77
89
|
* @param callback
|
|
78
90
|
* @returns
|
|
79
91
|
*/
|
|
80
|
-
onDownloadTotalProgressUpdate: (callback:
|
|
92
|
+
onDownloadTotalProgressUpdate: (callback: {
|
|
93
|
+
(...args: any[]): void;
|
|
94
|
+
}) => () => void;
|
|
81
95
|
/**
|
|
82
96
|
* 注册单文件下载进度完成监听
|
|
83
97
|
* @param callback
|
|
84
98
|
* @returns
|
|
85
99
|
*/
|
|
86
|
-
onFileDownloadComplete: (callback:
|
|
100
|
+
onFileDownloadComplete: (callback: {
|
|
101
|
+
(...args: any[]): void;
|
|
102
|
+
}) => () => void;
|
|
87
103
|
/**
|
|
88
104
|
* 注册设备因为其他异常断开连接的事件
|
|
89
105
|
* @returns
|
|
90
106
|
*/
|
|
91
|
-
onSessionStatusChange: (callback:
|
|
107
|
+
onSessionStatusChange: (callback: {
|
|
108
|
+
(...args: any[]): void;
|
|
109
|
+
}) => () => void;
|
|
92
110
|
/**
|
|
93
111
|
* 取消进行下载
|
|
94
112
|
* @returns
|
|
@@ -96,10 +114,15 @@ export default class P2pApi {
|
|
|
96
114
|
cancelDownloadTask: () => Promise<unknown>;
|
|
97
115
|
/**
|
|
98
116
|
* 查询设备相册文件列表
|
|
99
|
-
* @param albumName
|
|
117
|
+
* @param albumName 相册名
|
|
118
|
+
* @param retries 失败重试次数,默认 2 次(共至多 3 次)
|
|
119
|
+
* @param delay 重试延迟,默认 1500ms
|
|
100
120
|
* @returns
|
|
101
121
|
*/
|
|
102
|
-
queryAlbumFileIndexs: (albumName: string) => Promise<
|
|
122
|
+
queryAlbumFileIndexs: (albumName: string, retries?: number, delay?: number) => Promise<{
|
|
123
|
+
count: number;
|
|
124
|
+
items: any[];
|
|
125
|
+
} | null>;
|
|
103
126
|
/**
|
|
104
127
|
* P2p SDK 销毁
|
|
105
128
|
* @returns
|
package/lib/api/p2pApi.js
CHANGED
|
@@ -15,6 +15,18 @@ export default class P2pApi {
|
|
|
15
15
|
this.isConnected = false;
|
|
16
16
|
this.isConnecting = false;
|
|
17
17
|
}
|
|
18
|
+
emitError = error => {
|
|
19
|
+
if (typeof this.onError === 'function') {
|
|
20
|
+
try {
|
|
21
|
+
this.onError(error);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
logger('error', {
|
|
24
|
+
msg: 'onError callback error ==>',
|
|
25
|
+
e
|
|
26
|
+
}, this.onLogger);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
18
30
|
|
|
19
31
|
/**
|
|
20
32
|
* 设备断开之后的重连
|
|
@@ -420,31 +432,55 @@ export default class P2pApi {
|
|
|
420
432
|
|
|
421
433
|
/**
|
|
422
434
|
* 查询设备相册文件列表
|
|
423
|
-
* @param albumName
|
|
435
|
+
* @param albumName 相册名
|
|
436
|
+
* @param retries 失败重试次数,默认 2 次(共至多 3 次)
|
|
437
|
+
* @param delay 重试延迟,默认 1500ms
|
|
424
438
|
* @returns
|
|
425
439
|
*/
|
|
426
|
-
queryAlbumFileIndexs =
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
440
|
+
queryAlbumFileIndexs = (() => {
|
|
441
|
+
var _this = this;
|
|
442
|
+
return function (albumName) {
|
|
443
|
+
let retries = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2;
|
|
444
|
+
let delay = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1500;
|
|
445
|
+
const doQuery = attempt => {
|
|
446
|
+
return new Promise(resolve => {
|
|
447
|
+
p2p.queryAlbumFileIndexs({
|
|
448
|
+
deviceId: _this.devId,
|
|
449
|
+
albumName,
|
|
450
|
+
success: params => {
|
|
451
|
+
logger('info', {
|
|
452
|
+
msg: `queryAlbumFileIndexs success (attempt ${attempt}) ==>`,
|
|
453
|
+
params
|
|
454
|
+
}, _this.onLogger);
|
|
455
|
+
resolve(params);
|
|
456
|
+
},
|
|
457
|
+
fail: params => {
|
|
458
|
+
logger('warn', {
|
|
459
|
+
msg: `queryAlbumFileIndexs failed (attempt ${attempt}) ==>`,
|
|
460
|
+
params
|
|
461
|
+
}, _this.onLogger);
|
|
462
|
+
if (attempt < retries && _this.isConnected) {
|
|
463
|
+
logger('info', {
|
|
464
|
+
msg: `queryAlbumFileIndexs retrying in ${delay}ms (attempt ${attempt + 1}/${retries})...`
|
|
465
|
+
}, _this.onLogger);
|
|
466
|
+
setTimeout(() => {
|
|
467
|
+
doQuery(attempt + 1).then(resolve);
|
|
468
|
+
}, delay);
|
|
469
|
+
} else {
|
|
470
|
+
_this.emitError({
|
|
471
|
+
code: 'QUERY_ALBUM_TIMEOUT',
|
|
472
|
+
message: `queryAlbumFileIndexs failed after ${attempt + 1} attempt(s)`,
|
|
473
|
+
originalError: params
|
|
474
|
+
});
|
|
475
|
+
resolve(null);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
return doQuery(0);
|
|
482
|
+
};
|
|
483
|
+
})();
|
|
448
484
|
|
|
449
485
|
/**
|
|
450
486
|
* P2p SDK 销毁
|
package/lib/api/sweeperP2p.d.ts
CHANGED
|
@@ -82,6 +82,9 @@ export declare class SweeperP2p extends P2pApi {
|
|
|
82
82
|
offTotalDownLoadProgressUpdate: () => void;
|
|
83
83
|
onDefineStructuredMode: (isStructured: boolean) => void;
|
|
84
84
|
enableCustomLog: boolean;
|
|
85
|
+
mismatchCount: number;
|
|
86
|
+
reDownloadCount: number;
|
|
87
|
+
isReDownloading: boolean;
|
|
85
88
|
constructor();
|
|
86
89
|
private setStreamFilePath;
|
|
87
90
|
private setDataFilePath;
|
|
@@ -128,12 +131,16 @@ export declare class SweeperP2p extends P2pApi {
|
|
|
128
131
|
deviceId: string;
|
|
129
132
|
status: number;
|
|
130
133
|
}) => void;
|
|
134
|
+
/**
|
|
135
|
+
* 重连设备并重新开启数据观察
|
|
136
|
+
*/
|
|
137
|
+
private reconnectAndResume;
|
|
131
138
|
/**
|
|
132
139
|
* 开始进行文件下载
|
|
133
140
|
* @param downloadType
|
|
134
141
|
* 0: 下载断开 1: 持续下载
|
|
135
142
|
*/
|
|
136
|
-
startObserverSweeperDataByP2P: (downloadType: number, devId: string, onReceiveMapData: (data: string) => void, onReceivePathData: (data: string) => void, onReceiveAIPicData?: (
|
|
143
|
+
startObserverSweeperDataByP2P: (downloadType: number, devId: string, onReceiveMapData: (data: string) => void, onReceivePathData: (data: string) => void, onReceiveAIPicData?: (data: string) => void, onReceiveAIPicHDData?: (data: string) => void, onReceiveWifiMapData?: (data: string) => void, onDefineStructuredMode?: (isStructured: boolean) => void) => Promise<void>;
|
|
137
144
|
/**
|
|
138
145
|
* 在下载过程中继续添加下载文件
|
|
139
146
|
* @param files
|
|
@@ -142,10 +149,20 @@ export declare class SweeperP2p extends P2pApi {
|
|
|
142
149
|
* @returns
|
|
143
150
|
*/
|
|
144
151
|
appendDownloadStreamDuringTask: (files: Array<string>, successCb?: () => void, failCb?: () => void) => Promise<boolean> | void;
|
|
152
|
+
/**
|
|
153
|
+
* 渐进式平滑自愈:重新拉取数据流
|
|
154
|
+
* 优先尝试轻量 cancelDownloadTask -> downloadStream,
|
|
155
|
+
* 若重试超过 2 次,升级为 disconnectDevice -> connectDevice 彻底重连
|
|
156
|
+
*/
|
|
157
|
+
reDownloadStream: (reason?: string) => Promise<void>;
|
|
145
158
|
/**
|
|
146
159
|
* 注册下载有关的监听
|
|
147
160
|
*/
|
|
148
161
|
registerP2pDownloadEvent: () => void;
|
|
162
|
+
/**
|
|
163
|
+
* 清空分包缓存与组包进度
|
|
164
|
+
*/
|
|
165
|
+
clearPacketCache: () => void;
|
|
149
166
|
/**
|
|
150
167
|
* 移除下载有关的监听
|
|
151
168
|
*/
|
package/lib/api/sweeperP2p.js
CHANGED
|
@@ -69,6 +69,9 @@ export class SweeperP2p extends P2pApi {
|
|
|
69
69
|
this.exitFiles = [];
|
|
70
70
|
this.firstPackageTime = Date.now();
|
|
71
71
|
this.enableCustomLog = false;
|
|
72
|
+
this.mismatchCount = 0;
|
|
73
|
+
this.reDownloadCount = 0;
|
|
74
|
+
this.isReDownloading = false;
|
|
72
75
|
this.packetTotalMap = new Map([['map.bin', -1], ['map_structured.bin', -1], ['cleanPath.bin', -1], ['ai.bin', -1], ['aiHD_XXXX_YYYY.bin', -1], ['map.bin.stream', -1], ['map_structured.bin.stream', -1], ['cleanPath.bin.stream', -1], ['ai.bin.stream', -1], ['aiHD_XXXX_YYYY.bin.stream', -1]]);
|
|
73
76
|
this.packetSerialNumberCacheMap = new Map([['map.bin', -1], ['map_structured.bin', -1], ['cleanPath.bin', -1], ['ai.bin', -1], ['aiHD_XXXX_YYYY.bin', -1], ['map.bin.stream', -1], ['map_structured.bin.stream', -1], ['cleanPath.bin.stream', -1], ['ai.bin.stream', -1], ['aiHD_XXXX_YYYY.bin.stream', -1]]);
|
|
74
77
|
this.fileLengthCacheMap = new Map([['map.bin', -1], ['map_structured.bin', -1], ['cleanPath.bin', -1], ['ai.bin', -1], ['aiHD_XXXX_YYYY.bin', -1], ['map.bin.stream', -1], ['map_structured.bin.stream', -1], ['cleanPath.bin.stream', -1], ['ai.bin.stream', -1], ['aiHD_XXXX_YYYY.bin.stream', -1]]);
|
|
@@ -230,7 +233,7 @@ export class SweeperP2p extends P2pApi {
|
|
|
230
233
|
this.reconnectP2p(() => {
|
|
231
234
|
logP2PDataIfEnabled(this.enableCustomLog, this.devId, 'session_reconnect_success', 'Reconnected successfully, restarting data observer');
|
|
232
235
|
// 重连之后重新开启文件下载, 这里的成功不需要注册断开事件
|
|
233
|
-
this.startObserverSweeperDataByP2P(this.downloadType, this.devId, this.onReceiveMapData, this.onReceivePathData, this.onReceiveAIPicData, this.onReceiveAIPicHDData, this.onReceiveWifiMapData);
|
|
236
|
+
this.startObserverSweeperDataByP2P(this.downloadType, this.devId, this.onReceiveMapData, this.onReceivePathData, this.onReceiveAIPicData, this.onReceiveAIPicHDData, this.onReceiveWifiMapData, this.onDefineStructuredMode);
|
|
234
237
|
}, '');
|
|
235
238
|
} else {
|
|
236
239
|
logger('warn', {
|
|
@@ -242,6 +245,31 @@ export class SweeperP2p extends P2pApi {
|
|
|
242
245
|
}
|
|
243
246
|
};
|
|
244
247
|
|
|
248
|
+
/**
|
|
249
|
+
* 重连设备并重新开启数据观察
|
|
250
|
+
*/
|
|
251
|
+
reconnectAndResume = (() => {
|
|
252
|
+
var _this = this;
|
|
253
|
+
return async function () {
|
|
254
|
+
let reason = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'reconnectAndResume';
|
|
255
|
+
logger('info', {
|
|
256
|
+
msg: `reconnectAndResume triggered, reason: ${reason}`
|
|
257
|
+
}, _this.onLogger);
|
|
258
|
+
logP2PDataIfEnabled(_this.enableCustomLog, _this.devId, 'reconnect_and_resume', `Reconnecting device and resuming stream, reason: ${reason}`);
|
|
259
|
+
_this.removeP2pDownloadEvent();
|
|
260
|
+
try {
|
|
261
|
+
await _this.disconnectDevice();
|
|
262
|
+
} catch (e) {
|
|
263
|
+
// ignore
|
|
264
|
+
}
|
|
265
|
+
_this.isConnected = false;
|
|
266
|
+
_this.isConnecting = false;
|
|
267
|
+
_this.connectDevice(() => {
|
|
268
|
+
_this.startObserverSweeperDataByP2P(_this.downloadType, _this.devId, _this.onReceiveMapData, _this.onReceivePathData, _this.onReceiveAIPicData, _this.onReceiveAIPicHDData, _this.onReceiveWifiMapData, _this.onDefineStructuredMode);
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
})();
|
|
272
|
+
|
|
245
273
|
/**
|
|
246
274
|
* 开始进行文件下载
|
|
247
275
|
* @param downloadType
|
|
@@ -285,6 +313,21 @@ export class SweeperP2p extends P2pApi {
|
|
|
285
313
|
this.file = await this.queryAlbumFileIndexs(this.albumName);
|
|
286
314
|
logP2PDataIfEnabled(this.enableCustomLog, devId, 'query_album_files', `Query album files completed, file count: ${((_this$file = this.file) === null || _this$file === void 0 || (_this$file = _this$file.items) === null || _this$file === void 0 ? void 0 : _this$file.length) || 0}`);
|
|
287
315
|
}
|
|
316
|
+
if (!this.file || !this.file.items || this.file.items.length === 0) {
|
|
317
|
+
logger('warn', {
|
|
318
|
+
msg: 'startObserverSweeperDataByP2P: queryAlbumFileIndexs failed or returned empty items'
|
|
319
|
+
}, this.onLogger);
|
|
320
|
+
logP2PDataIfEnabled(this.enableCustomLog, devId, 'query_album_files_failed', 'queryAlbumFileIndexs failed or returned empty items');
|
|
321
|
+
// queryAlbumFileIndexs 底层失败已自动透传 QUERY_ALBUM_TIMEOUT
|
|
322
|
+
// 若是相册成功返回但列表为空,则透传 NO_ALBUM_FILES
|
|
323
|
+
if (this.file && (!this.file.items || this.file.items.length === 0)) {
|
|
324
|
+
this.emitError({
|
|
325
|
+
code: 'NO_ALBUM_FILES',
|
|
326
|
+
message: 'queryAlbumFileIndexs returned empty items'
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
288
331
|
if (this.file && ((_this$file$items = this.file.items) === null || _this$file$items === void 0 ? void 0 : _this$file$items.length) > 0) {
|
|
289
332
|
if (this.downloadType === 0) {
|
|
290
333
|
const exitFiles = this.queryNeedFiles(this.file.items);
|
|
@@ -307,16 +350,11 @@ export class SweeperP2p extends P2pApi {
|
|
|
307
350
|
msg: `${downloadType} p2p downloadStream failed and try reConnect Device===>`
|
|
308
351
|
}, this.onLogger);
|
|
309
352
|
logP2PDataIfEnabled(this.enableCustomLog, devId, 'download_stream_fail', `Stream download failed, attempting reconnect, downloadType: ${downloadType}`);
|
|
310
|
-
this.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
this.isConnecting = false;
|
|
314
|
-
logP2PDataIfEnabled(this.enableCustomLog, devId, 'download_stream_reconnect', 'Reconnecting device after stream download failure');
|
|
315
|
-
this.connectDevice(() => {
|
|
316
|
-
// 重连之后重新开启文件下载, 这里的成功不需要注册断开事件
|
|
317
|
-
this.startObserverSweeperDataByP2P(this.downloadType, this.devId, this.onReceiveMapData, this.onReceivePathData, this.onReceiveAIPicData, this.onReceiveAIPicHDData, this.onReceiveWifiMapData);
|
|
318
|
-
});
|
|
353
|
+
this.emitError({
|
|
354
|
+
code: 'DOWNLOAD_STREAM_FAILED',
|
|
355
|
+
message: `downloadStream failed for downloadType ${downloadType}`
|
|
319
356
|
});
|
|
357
|
+
this.reconnectAndResume(`download_stream_fail_${downloadType}`);
|
|
320
358
|
});
|
|
321
359
|
} else if (await this.initFilePath(this.dataFilePath)) {
|
|
322
360
|
logP2PDataIfEnabled(this.enableCustomLog, devId, 'download_file_start', `Starting file download, files count: ${exitFiles.length}`);
|
|
@@ -350,16 +388,11 @@ export class SweeperP2p extends P2pApi {
|
|
|
350
388
|
msg: `${downloadType} p2p downloadStream failed and try reConnect Device===>`
|
|
351
389
|
}, this.onLogger);
|
|
352
390
|
logP2PDataIfEnabled(this.enableCustomLog, devId, 'download_stream_fail', `Stream download failed, attempting reconnect, downloadType: ${downloadType}`);
|
|
353
|
-
this.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
this.isConnecting = false;
|
|
357
|
-
logP2PDataIfEnabled(this.enableCustomLog, devId, 'download_stream_reconnect', 'Reconnecting device after stream download failure');
|
|
358
|
-
this.connectDevice(() => {
|
|
359
|
-
// 重连之后重新开启文件下载, 这里的成功不需要注册断开事件
|
|
360
|
-
this.startObserverSweeperDataByP2P(this.downloadType, this.devId, this.onReceiveMapData, this.onReceivePathData, this.onReceiveAIPicData, this.onReceiveAIPicHDData, this.onReceiveWifiMapData);
|
|
361
|
-
});
|
|
391
|
+
this.emitError({
|
|
392
|
+
code: 'DOWNLOAD_STREAM_FAILED',
|
|
393
|
+
message: `downloadStream failed for downloadType ${downloadType}`
|
|
362
394
|
});
|
|
395
|
+
this.reconnectAndResume(`download_stream_fail_${downloadType}`);
|
|
363
396
|
});
|
|
364
397
|
} else if (await this.initFilePath(this.streamFilePath)) {
|
|
365
398
|
// 每次要下载前都需要先检查文件目录是否存在 防止中间过程被删除掉文件目录
|
|
@@ -391,6 +424,88 @@ export class SweeperP2p extends P2pApi {
|
|
|
391
424
|
}, this.albumName, successCb, failCb);
|
|
392
425
|
};
|
|
393
426
|
|
|
427
|
+
/**
|
|
428
|
+
* 渐进式平滑自愈:重新拉取数据流
|
|
429
|
+
* 优先尝试轻量 cancelDownloadTask -> downloadStream,
|
|
430
|
+
* 若重试超过 2 次,升级为 disconnectDevice -> connectDevice 彻底重连
|
|
431
|
+
*/
|
|
432
|
+
reDownloadStream = (() => {
|
|
433
|
+
var _this2 = this;
|
|
434
|
+
return async function () {
|
|
435
|
+
let reason = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'unknown';
|
|
436
|
+
if (_this2.isReDownloading || !_this2.isConnected) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
_this2.isReDownloading = true;
|
|
440
|
+
logger('info', {
|
|
441
|
+
msg: `reDownloadStream triggered, reason: ${reason}, retryCount: ${_this2.reDownloadCount}`
|
|
442
|
+
}, _this2.onLogger);
|
|
443
|
+
logP2PDataIfEnabled(_this2.enableCustomLog, _this2.devId, 'redownload_stream_trigger', `reDownloadStream triggered, reason: ${reason}, retryCount: ${_this2.reDownloadCount}`);
|
|
444
|
+
if (_this2.reDownloadCount >= 2) {
|
|
445
|
+
logger('warn', {
|
|
446
|
+
msg: 'reDownloadStream exceeded max retries (2), escalating to full reconnect'
|
|
447
|
+
}, _this2.onLogger);
|
|
448
|
+
logP2PDataIfEnabled(_this2.enableCustomLog, _this2.devId, 'redownload_stream_escalate', 'Exceeded max retries, escalating to full reconnect');
|
|
449
|
+
_this2.reDownloadCount = 0;
|
|
450
|
+
_this2.mismatchCount = 0;
|
|
451
|
+
_this2.isReDownloading = false;
|
|
452
|
+
await _this2.reconnectAndResume('redownload_stream_escalate');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
_this2.reDownloadCount += 1;
|
|
456
|
+
_this2.clearPacketCache();
|
|
457
|
+
try {
|
|
458
|
+
await _this2.cancelDownloadTask();
|
|
459
|
+
} catch (e) {
|
|
460
|
+
// ignore
|
|
461
|
+
}
|
|
462
|
+
if (!_this2.isConnected) {
|
|
463
|
+
_this2.isReDownloading = false;
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (!_this2.exitFiles || _this2.exitFiles.length === 0) {
|
|
467
|
+
var _this2$file;
|
|
468
|
+
if (!_this2.file || !_this2.file.items || _this2.file.items.length === 0) {
|
|
469
|
+
// @ts-ignore
|
|
470
|
+
_this2.file = await _this2.queryAlbumFileIndexs(_this2.albumName);
|
|
471
|
+
}
|
|
472
|
+
if ((_this2$file = _this2.file) !== null && _this2$file !== void 0 && (_this2$file = _this2$file.items) !== null && _this2$file !== void 0 && _this2$file.length) {
|
|
473
|
+
_this2.exitFiles = _this2.queryNeedFiles(_this2.file.items);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (!_this2.exitFiles || _this2.exitFiles.length === 0) {
|
|
477
|
+
logger('warn', {
|
|
478
|
+
msg: 'reDownloadStream: no exitFiles found to download'
|
|
479
|
+
}, _this2.onLogger);
|
|
480
|
+
_this2.isReDownloading = false;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
await _this2.downloadStream({
|
|
485
|
+
files: _this2.exitFiles
|
|
486
|
+
}, _this2.albumName, () => {
|
|
487
|
+
logger('info', {
|
|
488
|
+
msg: `reDownloadStream downloadStream success (retry ${_this2.reDownloadCount})`
|
|
489
|
+
}, _this2.onLogger);
|
|
490
|
+
logP2PDataIfEnabled(_this2.enableCustomLog, _this2.devId, 'redownload_stream_success', `reDownloadStream success on retry ${_this2.reDownloadCount}`);
|
|
491
|
+
_this2.isReDownloading = false;
|
|
492
|
+
}, () => {
|
|
493
|
+
logger('warn', {
|
|
494
|
+
msg: `reDownloadStream downloadStream failed (retry ${_this2.reDownloadCount})`
|
|
495
|
+
}, _this2.onLogger);
|
|
496
|
+
logP2PDataIfEnabled(_this2.enableCustomLog, _this2.devId, 'redownload_stream_fail', `reDownloadStream failed on retry ${_this2.reDownloadCount}`);
|
|
497
|
+
_this2.isReDownloading = false;
|
|
498
|
+
});
|
|
499
|
+
} catch (e) {
|
|
500
|
+
logger('error', {
|
|
501
|
+
msg: 'reDownloadStream error ==>',
|
|
502
|
+
e
|
|
503
|
+
}, _this2.onLogger);
|
|
504
|
+
_this2.isReDownloading = false;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
})();
|
|
508
|
+
|
|
394
509
|
/**
|
|
395
510
|
* 注册下载有关的监听
|
|
396
511
|
*/
|
|
@@ -416,13 +531,9 @@ export class SweeperP2p extends P2pApi {
|
|
|
416
531
|
};
|
|
417
532
|
|
|
418
533
|
/**
|
|
419
|
-
*
|
|
534
|
+
* 清空分包缓存与组包进度
|
|
420
535
|
*/
|
|
421
|
-
|
|
422
|
-
logger('info', {
|
|
423
|
-
msg: 'removeP2pDownloadEvent ==>'
|
|
424
|
-
}, this.onLogger);
|
|
425
|
-
logP2PDataIfEnabled(this.enableCustomLog, this.devId, 'remove_download_event', 'Removing download events');
|
|
536
|
+
clearPacketCache = () => {
|
|
426
537
|
if (shouldDownloadStream) {
|
|
427
538
|
[...this.packetSerialNumberCacheMap.keys()].forEach(key => {
|
|
428
539
|
this.packetSerialNumberCacheMap.set(key, -1);
|
|
@@ -438,6 +549,17 @@ export class SweeperP2p extends P2pApi {
|
|
|
438
549
|
});
|
|
439
550
|
}
|
|
440
551
|
this.cacheData = {};
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* 移除下载有关的监听
|
|
556
|
+
*/
|
|
557
|
+
removeP2pDownloadEvent = () => {
|
|
558
|
+
logger('info', {
|
|
559
|
+
msg: 'removeP2pDownloadEvent ==>'
|
|
560
|
+
}, this.onLogger);
|
|
561
|
+
logP2PDataIfEnabled(this.enableCustomLog, this.devId, 'remove_download_event', 'Removing download events');
|
|
562
|
+
this.clearPacketCache();
|
|
441
563
|
this.offFileDownloadComplete && this.offFileDownloadComplete();
|
|
442
564
|
this.offP2pStreamPacketReceive && this.offP2pStreamPacketReceive();
|
|
443
565
|
|
|
@@ -544,8 +666,34 @@ export class SweeperP2p extends P2pApi {
|
|
|
544
666
|
msg: `p2pStreamPacketReceiveCallback: fileName : ${fileName} length mismatch, expected: ${cacheFileLength * 2}, received: ${hexValue.length}`
|
|
545
667
|
}, this.onLogger);
|
|
546
668
|
logP2PDataIfEnabled(this.enableCustomLog, this.devId, 'packet_length_mismatch', `Packet length mismatch, fileName: ${fileName}, expected: ${cacheFileLength * 2}, received: ${hexValue.length}`);
|
|
669
|
+
|
|
670
|
+
// 清理受损文件的组包缓存,防止污染后续数据
|
|
671
|
+
cachePacketMap.clear();
|
|
672
|
+
this.packetTotalMap.set(fileName, -1);
|
|
673
|
+
this.mismatchCount += 1;
|
|
674
|
+
this.emitError({
|
|
675
|
+
code: 'PACKET_LENGTH_MISMATCH',
|
|
676
|
+
message: `Packet length mismatch for ${fileName}: expected ${cacheFileLength * 2}, received ${hexValue.length}`,
|
|
677
|
+
originalError: {
|
|
678
|
+
fileName,
|
|
679
|
+
expected: cacheFileLength * 2,
|
|
680
|
+
received: hexValue.length,
|
|
681
|
+
mismatchCount: this.mismatchCount
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
if (this.mismatchCount >= 2) {
|
|
685
|
+
logger('warn', {
|
|
686
|
+
msg: `consecutive length mismatch reached ${this.mismatchCount}, triggering reDownloadStream`
|
|
687
|
+
}, this.onLogger);
|
|
688
|
+
this.mismatchCount = 0;
|
|
689
|
+
this.reDownloadStream('consecutive_length_mismatch');
|
|
690
|
+
}
|
|
547
691
|
return;
|
|
548
692
|
}
|
|
693
|
+
|
|
694
|
+
// 校验通过,重置 mismatchCount 和 reDownloadCount
|
|
695
|
+
this.mismatchCount = 0;
|
|
696
|
+
this.reDownloadCount = 0;
|
|
549
697
|
if (this.firstPackageTime > 0 && fileName.indexOf('map') !== -1) {
|
|
550
698
|
const packageTime = Date.now() - this.firstPackageTime;
|
|
551
699
|
logger('info', {
|
|
@@ -735,6 +883,10 @@ export class SweeperP2p extends P2pApi {
|
|
|
735
883
|
this.disconnectDevice();
|
|
736
884
|
}
|
|
737
885
|
this.removeP2pDownloadEvent();
|
|
886
|
+
this.mismatchCount = 0;
|
|
887
|
+
this.reDownloadCount = 0;
|
|
888
|
+
this.isReDownloading = false;
|
|
889
|
+
this.file = undefined;
|
|
738
890
|
// 没有必要cancelDownloadTask,只需要调用断连
|
|
739
891
|
// this.cancelDownloadTask();
|
|
740
892
|
|
package/lib/index.d.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
+
import { P2pError, P2pErrorCode } from './api';
|
|
1
2
|
export * from './mqtt';
|
|
2
3
|
export * from './mqtt/type/requestType';
|
|
4
|
+
export { pointsToString, pickWaterFields } from './utils';
|
|
3
5
|
type TOnLogger = (type: 'warn' | 'error' | 'info', data: string) => void;
|
|
4
|
-
|
|
5
|
-
logTag?: string
|
|
6
|
-
enableCustomLog?: boolean
|
|
7
|
-
onReceiveAIPicData?: (
|
|
8
|
-
onReceiveAIPicHDData?: (
|
|
9
|
-
onReceiveWifiMapData?: (
|
|
10
|
-
onLogger?: TOnLogger
|
|
11
|
-
onDefineStructuredMode?: (
|
|
12
|
-
|
|
6
|
+
export interface P2PDataStreamOptions {
|
|
7
|
+
logTag?: string;
|
|
8
|
+
enableCustomLog?: boolean;
|
|
9
|
+
onReceiveAIPicData?: (data: string) => void;
|
|
10
|
+
onReceiveAIPicHDData?: (data: string) => void;
|
|
11
|
+
onReceiveWifiMapData?: (data: string) => void;
|
|
12
|
+
onLogger?: TOnLogger;
|
|
13
|
+
onDefineStructuredMode?: (isStructured: boolean) => void;
|
|
14
|
+
onError?: (error: P2pError) => void;
|
|
15
|
+
firstFrameTimeout?: number;
|
|
16
|
+
}
|
|
17
|
+
declare const useP2PDataStream: (devId: string, onReceiveMapData: (data: string) => void, onReceivePathData: (data: string) => void, opt?: P2PDataStreamOptions) => {
|
|
13
18
|
appendDownloadStreamDuringTask: (files: Array<string>, successCb?: () => void, failedCb?: () => void) => Promise<boolean> | void;
|
|
14
19
|
};
|
|
15
20
|
declare const StreamDataNotificationCenter: import("mitt").Emitter<Record<import("mitt").EventType, unknown>>;
|
|
21
|
+
export type { P2PDataStreamOptions, P2pError, P2pErrorCode };
|
|
16
22
|
export { StreamDataNotificationCenter, useP2PDataStream };
|