@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.
Files changed (53) hide show
  1. package/README.md +23 -1
  2. package/lib/api/index.d.ts +1 -0
  3. package/lib/api/p2pApi.d.ts +30 -7
  4. package/lib/api/p2pApi.js +59 -23
  5. package/lib/api/sweeperP2p.d.ts +18 -1
  6. package/lib/api/sweeperP2p.js +177 -25
  7. package/lib/index.d.ts +15 -9
  8. package/lib/index.js +60 -3
  9. package/lib/mqtt/createCommonOptions.d.ts +2 -20
  10. package/lib/mqtt/createCommonOptions.js +17 -5
  11. package/lib/mqtt/defineRobotCommand.d.ts +52 -0
  12. package/lib/mqtt/defineRobotCommand.js +71 -0
  13. package/lib/mqtt/hooks/useStructuredMessage.d.ts +5 -1
  14. package/lib/mqtt/hooks/useStructuredMessage.js +22 -10
  15. package/lib/mqtt/index.d.ts +7 -1
  16. package/lib/mqtt/index.js +11 -1
  17. package/lib/mqtt/promise.d.ts +21 -6
  18. package/lib/mqtt/promise.js +47 -12
  19. package/lib/mqtt/type/protocols/carpetProtocol.d.ts +2 -0
  20. package/lib/mqtt/type/protocols/index.d.ts +2 -0
  21. package/lib/mqtt/type/protocols/index.js +3 -1
  22. package/lib/mqtt/type/protocols/thresholdProtocol.d.ts +22 -0
  23. package/lib/mqtt/type/protocols/thresholdProtocol.js +1 -0
  24. package/lib/mqtt/type/requestType.d.ts +4 -0
  25. package/lib/mqtt/type/requestType.js +7 -0
  26. package/lib/mqtt/useCarpet.js +12 -4
  27. package/lib/mqtt/useCarpetClean.js +6 -2
  28. package/lib/mqtt/useCruise.js +3 -1
  29. package/lib/mqtt/useCustomCommand.d.ts +39 -0
  30. package/lib/mqtt/useCustomCommand.js +45 -0
  31. package/lib/mqtt/useDeviceModel.js +3 -1
  32. package/lib/mqtt/useFurnitureModel.js +3 -1
  33. package/lib/mqtt/useHistoryMap.js +9 -3
  34. package/lib/mqtt/usePartDivision.js +3 -1
  35. package/lib/mqtt/usePartMerge.js +3 -1
  36. package/lib/mqtt/usePassword.js +6 -2
  37. package/lib/mqtt/usePurifierSchedule.js +3 -1
  38. package/lib/mqtt/useQuiteHours.js +3 -1
  39. package/lib/mqtt/useResetMap.js +3 -1
  40. package/lib/mqtt/useRoomProperty.js +3 -1
  41. package/lib/mqtt/useSchedule.js +3 -1
  42. package/lib/mqtt/useSelectRoomClean.js +3 -1
  43. package/lib/mqtt/useSpotClean.js +3 -1
  44. package/lib/mqtt/useSpotPolygons.js +3 -1
  45. package/lib/mqtt/useThreshold.d.ts +10 -0
  46. package/lib/mqtt/useThreshold.js +90 -0
  47. package/lib/mqtt/useVirtualArea.js +3 -1
  48. package/lib/mqtt/useVirtualWall.js +3 -1
  49. package/lib/mqtt/useVoice.js +3 -1
  50. package/lib/mqtt/useWifiMap.js +3 -1
  51. package/lib/mqtt/useZoneClean.js +3 -1
  52. package/lib/utils/index.d.ts +7 -7
  53. 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`.
@@ -1,2 +1,3 @@
1
1
  import SweeperP2pInstance from './sweeperP2p';
2
+ export type { P2pError, P2pErrorCode } from './p2pApi';
2
3
  export { SweeperP2pInstance };
@@ -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: (...args: any[]) => void) => () => void;
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: (...args: any[]) => void) => () => void;
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: (...args: any[]) => void) => () => void;
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: (...args: any[]) => void) => () => void;
100
+ onFileDownloadComplete: (callback: {
101
+ (...args: any[]): void;
102
+ }) => () => void;
87
103
  /**
88
104
  * 注册设备因为其他异常断开连接的事件
89
105
  * @returns
90
106
  */
91
- onSessionStatusChange: (callback: (...args: any[]) => void) => () => void;
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<unknown>;
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 = albumName => {
427
- return new Promise(resolve => {
428
- p2p.queryAlbumFileIndexs({
429
- deviceId: this.devId,
430
- albumName,
431
- success: params => {
432
- logger('info', {
433
- msg: 'queryAlbumFileIndexs ==>',
434
- params
435
- }, this.onLogger);
436
- resolve(params);
437
- },
438
- fail: params => {
439
- logger('warn', {
440
- msg: 'queryAlbumFileIndexs failed ==>',
441
- params
442
- }, this.onLogger);
443
- resolve(null);
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 销毁
@@ -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?: ((data: string) => void) | undefined, onReceiveAIPicHDData?: ((data: string) => void) | undefined, onReceiveWifiMapData?: ((data: string) => void) | undefined, onDefineStructuredMode?: ((isStructured: boolean) => void) | undefined) => Promise<void>;
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
  */
@@ -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.removeP2pDownloadEvent();
311
- this.disconnectDevice().then(() => {
312
- this.isConnected = false;
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.removeP2pDownloadEvent();
354
- this.disconnectDevice().then(() => {
355
- this.isConnected = false;
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
- removeP2pDownloadEvent = () => {
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
- declare const useP2PDataStream: (devId: string, onReceiveMapData: (data: string) => void, onReceivePathData: (data: string) => void, opt?: {
5
- logTag?: string | undefined;
6
- enableCustomLog?: boolean | undefined;
7
- onReceiveAIPicData?: ((data: string) => void) | undefined;
8
- onReceiveAIPicHDData?: ((data: string) => void) | undefined;
9
- onReceiveWifiMapData?: ((data: string) => void) | undefined;
10
- onLogger?: TOnLogger | undefined;
11
- onDefineStructuredMode?: ((isStructured: boolean) => void) | undefined;
12
- } | undefined) => {
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 };