@ray-js/robot-data-stream 0.0.1-beta-1

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 ADDED
@@ -0,0 +1,69 @@
1
+ English | [简体中文](./README-zh_CN.md)
2
+
3
+ # @ray-js/robot-data-stream
4
+
5
+ [![latest](https://img.shields.io/npm/v/@ray-js/robot-data-stream/latest.svg)](https://www.npmjs.com/package/@ray-js/robot-data-stream) [![download](https://img.shields.io/npm/dt/@ray-js/robot-data-stream.svg)](https://www.npmjs.com/package/@ray-js/robot-data-stream)
6
+
7
+ > Robot P2P data stream hooks
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ $ npm install @ray-js/robot-data-stream
13
+ # or
14
+ $ yarn add @ray-js/robot-data-stream
15
+ ```
16
+
17
+ ## Develop
18
+
19
+ ```sh
20
+ # watch compile component code
21
+ yarn watch
22
+ # watch compile demo
23
+ yarn start:tuya
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```tsx
29
+
30
+ import React, { useEffect } from 'react';
31
+ import { View, Text } from '@ray-js/ray';
32
+ import { useP2PDataStream, StreamDataNotificationCenter } from '@ray-js/robot-data-stream';
33
+ import styles from './index.module.less';
34
+
35
+ const DemoBlock = ({ devId }) => {
36
+
37
+ const onReceiveMapData = (data: string) => {
38
+ StreamDataNotificationCenter.emit('receiveMapData', data)
39
+ }
40
+
41
+ const onReceivePathData = (data:string) => {
42
+ StreamDataNotificationCenter.emit('receivePathData', data)
43
+ }
44
+
45
+
46
+ useEffect(() => {
47
+
48
+ useP2PDataStream(devId, onReceiveMapData, onReceivePathData);
49
+
50
+ }, []);
51
+
52
+ return (
53
+ <View className={styles.demoBlock}>
54
+ <View className={styles.demoBlockTitle}>
55
+ <Text className={styles.demoBlockTitleText}>{devId}</Text>
56
+ </View>
57
+ </View>
58
+ )
59
+ };
60
+
61
+ export default function Home() {
62
+ return (
63
+ <View className={styles.view}>
64
+ <DemoBlock devId="" />
65
+ </View>
66
+ );
67
+ }
68
+ ```
69
+
@@ -0,0 +1,2 @@
1
+ import SweeperP2pInstance from "./sweeperP2p";
2
+ export { SweeperP2pInstance };
@@ -0,0 +1,2 @@
1
+ import SweeperP2pInstance from "./sweeperP2p";
2
+ export { SweeperP2pInstance };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * P2P 工具类
3
+ */
4
+ export default class P2pApi {
5
+ isConnected: boolean;
6
+ offSessionStatusChange: () => void;
7
+ devId: string;
8
+ constructor();
9
+ /**
10
+ * 设备断开之后的重连
11
+ */
12
+ reconnectP2p: (successCb?: () => void) => void;
13
+ /**
14
+ * 初始化P2P SDK
15
+ * @param id 用户Id
16
+ * @returns
17
+ */
18
+ initP2pSdk: (devId: string) => Promise<boolean>;
19
+ /**
20
+ * 连接设备
21
+ * @returns
22
+ */
23
+ connectDevice: (successCb?: () => void, failCb?: () => void, completeCb?: () => void) => Promise<boolean>;
24
+ /**
25
+ * 断开P2P设备连接
26
+ * @returns
27
+ */
28
+ disconnectDevice: () => Promise<boolean>;
29
+ /**
30
+ * 开始P2p流传输
31
+ * @param files
32
+ * @param albumName
33
+ * @param filePath
34
+ * @param successCb
35
+ * @param failedCb
36
+ * @returns
37
+ */
38
+ downloadStream: (files: {
39
+ files: Array<string>;
40
+ }, albumName: string, successCb?: () => void, failedCb?: () => void) => Promise<unknown> | undefined;
41
+ /**
42
+ * 监听p2p传输数据流
43
+ * @param callback
44
+ * @returns
45
+ */
46
+ onP2pStreamPacketReceive: (callback: (...args: any[]) => void) => () => void;
47
+ /**
48
+ * 开始下载文件
49
+ * files : {"files":["filesname1", "filesname2", "filesname3" ]}
50
+ */
51
+ downloadFile: (files: {
52
+ files: Array<string>;
53
+ }, albumName: string, filePath: string, successCb?: () => void, failedCb?: () => void) => Promise<unknown> | null;
54
+ /**
55
+ * 注册下载监听事件
56
+ * @param callback
57
+ * @returns
58
+ */
59
+ onDownloadProgressUpdate: (callback: (...args: any[]) => void) => () => void;
60
+ /**
61
+ * 注册多文件下载进度监听
62
+ * @param callback
63
+ * @returns
64
+ */
65
+ onDownloadTotalProgressUpdate: (callback: (...args: any[]) => void) => () => void;
66
+ /**
67
+ * 注册单文件下载进度完成监听
68
+ * @param callback
69
+ * @returns
70
+ */
71
+ onFileDownloadComplete: (callback: (...args: any[]) => void) => () => void;
72
+ /**
73
+ * 注册设备因为其他异常断开连接的事件
74
+ * @returns
75
+ */
76
+ onSessionStatusChange: (callback: (...args: any[]) => void) => () => void;
77
+ /**
78
+ * 取消进行下载
79
+ * @returns
80
+ */
81
+ cancelDownloadTask: () => Promise<unknown>;
82
+ /**
83
+ * 查询设备相册文件列表
84
+ * @param albumName
85
+ * @returns
86
+ */
87
+ queryAlbumFileIndexs: (albumName: string) => Promise<unknown>;
88
+ /**
89
+ * P2p SDK 销毁
90
+ * @returns
91
+ */
92
+ deInitP2PSDK: () => Promise<boolean>;
93
+ }
@@ -0,0 +1,342 @@
1
+ /* eslint-disable prefer-promise-reject-errors */
2
+ import { p2p } from '@ray-js/ray';
3
+ import moment from 'moment';
4
+ import { Logger } from '@/utils';
5
+ /**
6
+ * P2P 工具类
7
+ */
8
+ export default class P2pApi {
9
+ // P2p连接状态
10
+
11
+ constructor() {
12
+ this.isConnected = false;
13
+ }
14
+
15
+ /**
16
+ * 设备断开之后的重连
17
+ */
18
+ reconnectP2p = successCb => {
19
+ /**
20
+ * 监听到断开事件后
21
+ * 连接成功后设置flag为true
22
+ * 并清除定时器
23
+ */
24
+ this.isConnected = false;
25
+ if (!this.isConnected) {
26
+ Logger.info('start p2p reconnect ==>', moment().format('YYYY-MM-DD HH:mm:ss'));
27
+ this.connectDevice(() => {
28
+ Logger.info('p2p reconnect success ==>', moment().format('YYYY-MM-DD HH:mm:ss'));
29
+ this.isConnected = true;
30
+ if (this.isConnected) {
31
+ typeof successCb === 'function' && successCb();
32
+ }
33
+ }, () => {}, () => {
34
+ Logger.log('reconnect complete ==>', String(this.isConnected));
35
+ if (!this.isConnected) {
36
+ Logger.warn('p2p reconnect failed ==>', moment().format('YYYY-MM-DD HH:mm:ss'));
37
+ this.reconnectP2p(successCb);
38
+ }
39
+ });
40
+ }
41
+ };
42
+
43
+ /**
44
+ * 初始化P2P SDK
45
+ * @param id 用户Id
46
+ * @returns
47
+ */
48
+ initP2pSdk = async devId => {
49
+ return new Promise((resolve, reject) => {
50
+ try {
51
+ this.devId = devId;
52
+ p2p.P2PSDKInit({
53
+ success: () => {
54
+ Logger.info('P2PSDKInit success');
55
+ resolve(true);
56
+ },
57
+ fail: params => {
58
+ Logger.warn('P2PSDKInit fail', params);
59
+ resolve(false);
60
+ }
61
+ });
62
+ } catch (e) {
63
+ reject(false);
64
+ }
65
+ });
66
+ };
67
+
68
+ /**
69
+ * 连接设备
70
+ * @returns
71
+ */
72
+ connectDevice = (successCb, failCb, completeCb) => {
73
+ return new Promise((resolve, reject) => {
74
+ try {
75
+ p2p.connectDevice({
76
+ deviceId: this.devId,
77
+ timeout: 5000,
78
+ mode: 0,
79
+ success: () => {
80
+ Logger.info('p2p connectDevice success');
81
+ this.isConnected = true;
82
+ typeof successCb === 'function' && successCb();
83
+ resolve(true);
84
+ },
85
+ fail: params => {
86
+ Logger.warn('p2p connectDevice failed ==>', params);
87
+ typeof failCb === 'function' && failCb();
88
+ resolve(false);
89
+ },
90
+ complete: () => {
91
+ typeof completeCb === 'function' && completeCb();
92
+ }
93
+ });
94
+ } catch (e) {
95
+ Logger.error('p2p connectDevice occur an error ==>', e);
96
+ reject(false);
97
+ }
98
+ });
99
+ };
100
+
101
+ /**
102
+ * 断开P2P设备连接
103
+ * @returns
104
+ */
105
+ disconnectDevice = () => {
106
+ return new Promise((resolve, reject) => {
107
+ try {
108
+ p2p.disconnectDevice({
109
+ deviceId: this.devId,
110
+ success: () => {
111
+ this.isConnected = false;
112
+ Logger.info('p2p disconnectDevice success');
113
+ resolve(true);
114
+ },
115
+ fail: () => {
116
+ Logger.warn('p2p disconnectDevice failed');
117
+ resolve(false);
118
+ }
119
+ });
120
+ } catch (e) {
121
+ Logger.warn('p2p disconnectDevice occur an error ==>', e);
122
+ reject(false);
123
+ }
124
+ });
125
+ };
126
+
127
+ /**
128
+ * 开始P2p流传输
129
+ * @param files
130
+ * @param albumName
131
+ * @param filePath
132
+ * @param successCb
133
+ * @param failedCb
134
+ * @returns
135
+ */
136
+ downloadStream = (files, albumName, successCb, failedCb) => {
137
+ try {
138
+ if (this.isConnected) {
139
+ return new Promise(resolve => {
140
+ p2p.downloadStream({
141
+ deviceId: this.devId,
142
+ albumName: albumName,
143
+ jsonfiles: JSON.stringify(files),
144
+ success: () => {
145
+ Logger.info('p2p downloadStream success');
146
+ typeof successCb === 'function' && successCb();
147
+ resolve(true);
148
+ },
149
+ fail: params => {
150
+ Logger.warn('p2p downloadStream failed ==>', params);
151
+ setTimeout(() => {
152
+ typeof failedCb === 'function' && failedCb();
153
+ }, 500);
154
+ resolve(false);
155
+ },
156
+ complete: () => {}
157
+ });
158
+ });
159
+ }
160
+ } catch (e) {
161
+ Logger.warn('p2p downloadStream occur an error ==>', e);
162
+ }
163
+ };
164
+
165
+ /**
166
+ * 监听p2p传输数据流
167
+ * @param callback
168
+ * @returns
169
+ */
170
+ onP2pStreamPacketReceive = callback => {
171
+ p2p.onStreamPacketReceive(callback);
172
+ return () => {
173
+ // 反注册监听
174
+ p2p.offStreamPacketReceive(callback);
175
+ };
176
+ };
177
+
178
+ /**
179
+ * 开始下载文件
180
+ * files : {"files":["filesname1", "filesname2", "filesname3" ]}
181
+ */
182
+ downloadFile = (files, albumName, filePath, successCb, failedCb) => {
183
+ try {
184
+ if (this.isConnected) {
185
+ return new Promise(resolve => {
186
+ p2p.downloadFile({
187
+ deviceId: this.devId,
188
+ albumName: albumName,
189
+ filePath: filePath,
190
+ jsonfiles: JSON.stringify(files),
191
+ success: () => {
192
+ Logger.info('p2p downloadFile success');
193
+ typeof successCb === 'function' && successCb();
194
+ resolve(true);
195
+ },
196
+ fail: params => {
197
+ Logger.warn('p2p downloadFile failed ==>', params);
198
+ setTimeout(() => {
199
+ typeof failedCb === 'function' && failedCb();
200
+ }, 500);
201
+ resolve(false);
202
+ },
203
+ complete: () => {}
204
+ });
205
+ });
206
+ }
207
+ } catch (e) {
208
+ Logger.error('p2p downloadFile occur an error ==>', e);
209
+ }
210
+ return null;
211
+ };
212
+
213
+ /**
214
+ * 注册下载监听事件
215
+ * @param callback
216
+ * @returns
217
+ */
218
+ onDownloadProgressUpdate = callback => {
219
+ p2p.onDownloadProgressUpdate(callback);
220
+ return () => {
221
+ // 反注册监听
222
+ p2p.offDownloadProgressUpdate(callback);
223
+ };
224
+ };
225
+
226
+ /**
227
+ * 注册多文件下载进度监听
228
+ * @param callback
229
+ * @returns
230
+ */
231
+ onDownloadTotalProgressUpdate = callback => {
232
+ p2p.onDownloadTotalProgressUpdate(callback);
233
+ return () => {
234
+ // 反注册监听
235
+ p2p.offDownloadTotalProgressUpdate(callback);
236
+ };
237
+ };
238
+
239
+ /**
240
+ * 注册单文件下载进度完成监听
241
+ * @param callback
242
+ * @returns
243
+ */
244
+ onFileDownloadComplete = callback => {
245
+ p2p.onFileDownloadComplete(callback);
246
+ return () => {
247
+ // 反注册监听
248
+ p2p.offFileDownloadComplete(callback);
249
+ };
250
+ };
251
+
252
+ /**
253
+ * 注册设备因为其他异常断开连接的事件
254
+ * @returns
255
+ */
256
+ onSessionStatusChange = callback => {
257
+ p2p.onSessionStatusChange(callback);
258
+ this.offSessionStatusChange = () => {
259
+ // 反注册监听
260
+ p2p.offSessionStatusChange(callback);
261
+ };
262
+ return this.offSessionStatusChange;
263
+ };
264
+
265
+ /**
266
+ * 取消进行下载
267
+ * @returns
268
+ */
269
+ cancelDownloadTask = () => {
270
+ return new Promise((resolve, reject) => {
271
+ try {
272
+ p2p.cancelDownloadTask({
273
+ deviceId: this.devId,
274
+ success: () => {
275
+ resolve(true);
276
+ },
277
+ fail: () => {
278
+ reject(false);
279
+ }
280
+ });
281
+ } catch (e) {
282
+ Logger.info('cancelDownloadTask occur an error', e);
283
+ reject(false);
284
+ }
285
+ });
286
+ };
287
+
288
+ /**
289
+ * 查询设备相册文件列表
290
+ * @param albumName
291
+ * @returns
292
+ */
293
+ queryAlbumFileIndexs = albumName => {
294
+ return new Promise(resolve => {
295
+ p2p.queryAlbumFileIndexs({
296
+ deviceId: this.devId,
297
+ albumName,
298
+ success: params => {
299
+ Logger.info('queryAlbumFileIndexs ==>', params);
300
+ resolve(params);
301
+ },
302
+ fail: params => {
303
+ Logger.warn('queryAlbumFileIndexs failed ==>', params);
304
+ resolve(null);
305
+ }
306
+ });
307
+ });
308
+ };
309
+
310
+ /**
311
+ * P2p SDK 销毁
312
+ * @returns
313
+ */
314
+ deInitP2PSDK = async () => {
315
+ // 先销毁断开监听
316
+ typeof this.offSessionStatusChange === 'function' && this.offSessionStatusChange();
317
+ // 销毁初始化时,先进行断连操作
318
+ if (this.isConnected) {
319
+ await this.disconnectDevice();
320
+ }
321
+ return new Promise((resolve, reject) => {
322
+ try {
323
+ p2p.deInitSDK({
324
+ success: () => {
325
+ Logger.info('deInitP2pSDK success');
326
+ resolve(true);
327
+ },
328
+ fail: () => {
329
+ Logger.info('deInitP2pSDK failed');
330
+ resolve(false);
331
+ },
332
+ complete: () => {
333
+ resolve(true);
334
+ }
335
+ });
336
+ } catch (e) {
337
+ Logger.error('deInitP2pSDK occur an error ==>', e);
338
+ reject(false);
339
+ }
340
+ });
341
+ };
342
+ }
@@ -0,0 +1,157 @@
1
+ import P2pApi from './p2pApi';
2
+ /**
3
+ * 基于P2p工具类的扫地机扩展实现
4
+ */
5
+ interface FileInfo {
6
+ channel: number;
7
+ createTime: number;
8
+ dir: number;
9
+ duration: number;
10
+ filename: string;
11
+ idx: number;
12
+ type: number;
13
+ }
14
+ declare const FILE_NAME_MAP: {
15
+ readonly 'map.bin': {
16
+ readonly type: 0;
17
+ };
18
+ readonly 'cleanPath.bin': {
19
+ readonly type: 1;
20
+ };
21
+ readonly 'map.bin.stream': {
22
+ readonly type: 0;
23
+ };
24
+ readonly 'cleanPath.bin.stream': {
25
+ readonly type: 1;
26
+ };
27
+ };
28
+ export declare class SweeperP2p extends P2pApi {
29
+ file: {
30
+ items: Array<FileInfo>;
31
+ count: number;
32
+ } | undefined;
33
+ albumName: string;
34
+ streamFilePath: string;
35
+ dataFilePath: string;
36
+ mapBinData: string;
37
+ navPathBinData: string;
38
+ cleanPathBinData: string;
39
+ mapBinStream: string;
40
+ navPathBinStream: string;
41
+ cleanPathBinStream: string;
42
+ downloadType: number;
43
+ readingMap: boolean;
44
+ readingClean: boolean;
45
+ readingNav: boolean;
46
+ cacheDir: string;
47
+ fileIndex: number;
48
+ cacheData: any;
49
+ packetDataCacheMap: Map<keyof typeof FILE_NAME_MAP, Map<number, string>>;
50
+ packetSerialNumberCacheMap: Map<keyof typeof FILE_NAME_MAP, number>;
51
+ packetTotalMap: Map<keyof typeof FILE_NAME_MAP, number>;
52
+ onReceiveMapData: (data: string) => void;
53
+ onReceivePathData: (data: string) => void;
54
+ offFileDownloadComplete: () => void;
55
+ offP2pStreamPacketReceive: () => void;
56
+ offDownLoadProgressUpdate: () => void;
57
+ offTotalDownLoadProgressUpdate: () => void;
58
+ constructor();
59
+ private setStreamFilePath;
60
+ private setDataFilePath;
61
+ /**
62
+ * 获取流文件存储地址
63
+ * @param fileName
64
+ * @returns
65
+ */
66
+ private getStreamFilePath;
67
+ /**
68
+ * 获取完整文件存储地址
69
+ * @param fileName
70
+ * @returns
71
+ */
72
+ private getDataFilePath;
73
+ /**
74
+ * 创建文件路径文件夹
75
+ * @param filePath
76
+ * @returns
77
+ */
78
+ private createFilePath;
79
+ /**
80
+ * 检查当前文件目录是否存在
81
+ * @param filePath
82
+ * @returns
83
+ */
84
+ private checkIfDirIsExist;
85
+ /**
86
+ * 初始化文件目录
87
+ * @param filePath
88
+ * @returns
89
+ */
90
+ private initFilePath;
91
+ /**
92
+ * 根据文件名返回对应的fileType
93
+ * @param filename
94
+ */
95
+ private getFileType;
96
+ /**
97
+ * 设备连接状态发生改变
98
+ * @param data
99
+ */
100
+ sessionStatusCallback: (data: {
101
+ deviceId: string;
102
+ status: number;
103
+ }) => void;
104
+ /**
105
+ * 开始进行文件下载
106
+ * @param downloadType
107
+ * 0: 下载断开 1: 持续下载
108
+ */
109
+ startObserverSweeperDataByP2P: (downloadType: number, devId: string, onReceiveMapData: (data: string) => void, onReceivePathData: (data: string) => void) => Promise<void>;
110
+ /**
111
+ * 注册下载有关的监听
112
+ */
113
+ private registerP2pDownloadEvent;
114
+ /**
115
+ * 移除下载有关的监听
116
+ */
117
+ private removeP2pDownloadEvent;
118
+ /**
119
+ * 单文件下载完成回调
120
+ * @param downloadComplete
121
+ */
122
+ private fileDownloadCompleteCallback;
123
+ /**
124
+ * p2p数据流回调
125
+ * @param downloadComplete
126
+ */
127
+ private p2pStreamPacketReceiveCallback;
128
+ /**
129
+ * 单文件下载完成回调
130
+ * @param data
131
+ */
132
+ private fileDownloadProgressCallback;
133
+ /**
134
+ * 多文件下载进度监听
135
+ * @param data
136
+ */
137
+ private fileTotalDownloadProgressCallback;
138
+ /**
139
+ * 从指定的文件路径获取文件
140
+ * @param fileName
141
+ * @param filePath
142
+ */
143
+ private readFileFromPath;
144
+ private setReading;
145
+ private resetReading;
146
+ /**
147
+ * 查询返回的文件中是否包含了所需文件
148
+ * @param fileList
149
+ */
150
+ private queryNeedFiles;
151
+ /**
152
+ * 停止文件下载
153
+ */
154
+ stopObserverSweeperDataByP2P: () => Promise<unknown>;
155
+ }
156
+ declare const sweeperP2pInstance: SweeperP2p;
157
+ export default sweeperP2pInstance;
@@ -0,0 +1,479 @@
1
+ /* eslint-disable no-shadow */
2
+ import P2pApi from './p2pApi';
3
+ import Base64 from 'base64-js';
4
+ import { padStart } from 'lodash-es';
5
+ import { Logger } from '@/utils';
6
+ /**
7
+ * 基于P2p工具类的扫地机扩展实现
8
+ */
9
+
10
+ const FILE_NAME_MAP = {
11
+ 'map.bin': {
12
+ type: 0
13
+ },
14
+ 'cleanPath.bin': {
15
+ type: 1
16
+ },
17
+ 'map.bin.stream': {
18
+ type: 0
19
+ },
20
+ 'cleanPath.bin.stream': {
21
+ type: 1
22
+ }
23
+ };
24
+
25
+ // 走p2p流传输(新) or 读取bin文件(旧)
26
+ const shouldDownloadStream = Boolean(ty.p2p.downloadStream && ty.p2p.onStreamPacketReceive);
27
+ export class SweeperP2p extends P2pApi {
28
+ constructor() {
29
+ super();
30
+ this.file = undefined;
31
+ this.downloadType = 1;
32
+ this.fileIndex = -1; // -1 表示所有文件下载完成
33
+ this.albumName = 'ipc_sweeper_robot';
34
+ this.mapBinData = 'map.bin';
35
+ this.navPathBinData = 'navPath.bin';
36
+ this.cleanPathBinData = 'cleanPath.bin';
37
+ this.mapBinStream = 'map.bin.stream';
38
+ this.navPathBinStream = 'navPath.bin.stream';
39
+ this.cleanPathBinStream = 'cleanPath.bin.stream';
40
+ this.cacheDir = ty.env.USER_DATA_PATH;
41
+ this.readingMap = false;
42
+ this.readingClean = false;
43
+ this.readingNav = false;
44
+ this.cacheData = {};
45
+ this.packetTotalMap = new Map([['map.bin', -1], ['cleanPath.bin', -1], ['map.bin.stream', -1], ['cleanPath.bin.stream', -1]]);
46
+ this.packetSerialNumberCacheMap = new Map([['map.bin', -1], ['cleanPath.bin', -1], ['map.bin.stream', -1], ['cleanPath.bin.stream', -1]]);
47
+ this.packetDataCacheMap = new Map([['map.bin', new Map()], ['cleanPath.bin', new Map()], ['map.bin.stream', new Map()], ['cleanPath.bin.stream', new Map()]]);
48
+ }
49
+ setStreamFilePath = () => {
50
+ // this.streamFilePath = this.cacheDir + `/${this.albumName}/${devId}/stream`;
51
+ if (/usr/.test(this.cacheDir)) {
52
+ this.streamFilePath = this.cacheDir;
53
+ } else {
54
+ this.streamFilePath = this.cacheDir + 'usr';
55
+ }
56
+ // 检查存储文件目录是否存在
57
+ // this.initFilePath(this.streamFilePath);
58
+ };
59
+ setDataFilePath = () => {
60
+ // this.dataFilePath = this.cacheDir + `/${this.albumName}/${devId}/data`;
61
+ if (/usr/.test(this.cacheDir)) {
62
+ this.dataFilePath = this.cacheDir;
63
+ } else {
64
+ this.streamFilePath = this.cacheDir + 'usr';
65
+ }
66
+ // 检查存储文件目录是否存在
67
+ // this.initFilePath(this.dataFilePath);
68
+ };
69
+
70
+ /**
71
+ * 获取流文件存储地址
72
+ * @param fileName
73
+ * @returns
74
+ */
75
+ getStreamFilePath = fileName => {
76
+ return this.streamFilePath + '/' + fileName;
77
+ };
78
+
79
+ /**
80
+ * 获取完整文件存储地址
81
+ * @param fileName
82
+ * @returns
83
+ */
84
+ getDataFilePath = fileName => {
85
+ return this.dataFilePath + '/' + fileName;
86
+ };
87
+
88
+ /**
89
+ * 创建文件路径文件夹
90
+ * @param filePath
91
+ * @returns
92
+ */
93
+ createFilePath = filePath => {
94
+ try {
95
+ ty.getFileSystemManager().mkdirSync({
96
+ dirPath: filePath,
97
+ recursive: true
98
+ });
99
+ console.log('mkdirSync success: filePath ==>', filePath);
100
+ return true;
101
+ } catch (e) {
102
+ console.log('mkdirSync error ==>', e);
103
+ return false;
104
+ }
105
+ };
106
+
107
+ /**
108
+ * 检查当前文件目录是否存在
109
+ * @param filePath
110
+ * @returns
111
+ */
112
+ checkIfDirIsExist = filePath => {
113
+ return new Promise(resolve => {
114
+ ty.getFileSystemManager().access({
115
+ path: filePath,
116
+ success(params) {
117
+ console.info('file access success ==>', params);
118
+ resolve(true);
119
+ },
120
+ fail(params) {
121
+ console.info('file access fail ==>', params);
122
+ resolve(false);
123
+ }
124
+ });
125
+ });
126
+ };
127
+
128
+ /**
129
+ * 初始化文件目录
130
+ * @param filePath
131
+ * @returns
132
+ */
133
+ initFilePath = async filePath => {
134
+ const isDir = await this.checkIfDirIsExist(filePath);
135
+ if (!isDir) {
136
+ const result = this.createFilePath(filePath);
137
+ return result;
138
+ }
139
+ return isDir;
140
+ };
141
+
142
+ /**
143
+ * 根据文件名返回对应的fileType
144
+ * @param filename
145
+ */
146
+ getFileType = filename => {
147
+ if (filename.indexOf('map') !== -1) {
148
+ return 0;
149
+ }
150
+ if (filename.indexOf('cleanPath') !== -1) {
151
+ return 1;
152
+ }
153
+ if (filename.indexOf('navPath') !== -1) {
154
+ return 3;
155
+ }
156
+ return 2;
157
+ };
158
+
159
+ /**
160
+ * 设备连接状态发生改变
161
+ * @param data
162
+ */
163
+ sessionStatusCallback = data => {
164
+ Logger.info('sessionStatusCallback ==>', data);
165
+ if (data) {
166
+ const {
167
+ status
168
+ } = data;
169
+ if (status < 0) {
170
+ Logger.info('receive disconnect notice ==>', status);
171
+ this.reconnectP2p(() => {
172
+ // 重连之后重新开启文件下载, 这里的成功不需要注册断开事件
173
+ this.startObserverSweeperDataByP2P(this.downloadType, this.devId, this.onReceiveMapData, this.onReceivePathData);
174
+ });
175
+ }
176
+ }
177
+ };
178
+
179
+ /**
180
+ * 开始进行文件下载
181
+ * @param downloadType
182
+ * 0: 下载断开 1: 持续下载
183
+ */
184
+ startObserverSweeperDataByP2P = async (downloadType, devId, onReceiveMapData, onReceivePathData) => {
185
+ var _this$file$items;
186
+ if (![0, 1].some(item => item === downloadType)) {
187
+ Logger.warn('download type must be 0 or 1');
188
+ return;
189
+ }
190
+ this.onReceiveMapData = onReceiveMapData;
191
+ this.onReceivePathData = onReceivePathData;
192
+ Logger.info('startObserverSweeperDataByP2P ==>');
193
+ this.downloadType = downloadType;
194
+ this.setDataFilePath(devId);
195
+ this.setStreamFilePath(devId);
196
+ // 先移除监听,再重新注册
197
+ this.removeP2pDownloadEvent();
198
+ this.registerP2pDownloadEvent();
199
+ if (!this.file) {
200
+ //@ts-ignore
201
+ this.file = await this.queryAlbumFileIndexs(this.albumName);
202
+ }
203
+ if (this.file && ((_this$file$items = this.file.items) === null || _this$file$items === void 0 ? void 0 : _this$file$items.length) > 0) {
204
+ if (this.downloadType === 0) {
205
+ const exitFiles = this.queryNeedFiles(this.file.items);
206
+ if (exitFiles.length > 0) {
207
+ if (shouldDownloadStream) {
208
+ // 开启p2p流传输
209
+ this.downloadStream({
210
+ files: exitFiles
211
+ }, this.albumName);
212
+ } else if (await this.initFilePath(this.dataFilePath)) {
213
+ this.downloadFile({
214
+ files: exitFiles
215
+ }, this.albumName, this.dataFilePath);
216
+ }
217
+ }
218
+ } else if (this.downloadType === 1) {
219
+ const exitFiles = this.queryNeedFiles(this.file.items);
220
+ if (exitFiles.length > 0) {
221
+ if (shouldDownloadStream) {
222
+ // 开启p2p流传输
223
+ this.downloadStream({
224
+ files: exitFiles
225
+ }, this.albumName);
226
+ } else if (await this.initFilePath(this.streamFilePath)) {
227
+ // 每次要下载前都需要先检查文件目录是否存在 防止中间过程被删除掉文件目录
228
+ this.downloadFile({
229
+ files: exitFiles
230
+ }, this.albumName, this.streamFilePath);
231
+ }
232
+ }
233
+ }
234
+ }
235
+ };
236
+
237
+ /**
238
+ * 注册下载有关的监听
239
+ */
240
+ registerP2pDownloadEvent = () => {
241
+ if (shouldDownloadStream) {
242
+ // p2p数据流监听
243
+ this.offP2pStreamPacketReceive = this.onP2pStreamPacketReceive(this.p2pStreamPacketReceiveCallback);
244
+ } else {
245
+ // 注册下载完成的监听
246
+ this.offFileDownloadComplete = this.onFileDownloadComplete(this.fileDownloadCompleteCallback);
247
+ }
248
+
249
+ // this.offDownLoadProgressUpdate = this.onDownloadProgressUpdate(
250
+ // this.fileDownloadProgressCallback
251
+ // );
252
+ // this.offTotalDownLoadProgressUpdate = this.onDownloadTotalProgressUpdate(
253
+ // this.fileTotalDownloadProgressCallback
254
+ // );
255
+ };
256
+
257
+ /**
258
+ * 移除下载有关的监听
259
+ */
260
+ removeP2pDownloadEvent = () => {
261
+ this.offFileDownloadComplete && this.offFileDownloadComplete();
262
+ this.offP2pStreamPacketReceive && this.offP2pStreamPacketReceive();
263
+ if (shouldDownloadStream) {
264
+ [...this.packetSerialNumberCacheMap.keys()].forEach(key => {
265
+ this.packetSerialNumberCacheMap.set(key, -1);
266
+ });
267
+ [...this.packetTotalMap.keys()].forEach(key => {
268
+ this.packetTotalMap.set(key, -1);
269
+ });
270
+ [...this.packetDataCacheMap.keys()].forEach(key => {
271
+ this.packetDataCacheMap.set(key, new Map());
272
+ });
273
+ } else {
274
+ this.cacheData = {};
275
+ }
276
+
277
+ // this.offDownLoadProgressUpdate && this.offDownLoadProgressUpdate();
278
+ // this.offTotalDownLoadProgressUpdate && this.offTotalDownLoadProgressUpdate();
279
+ };
280
+
281
+ /**
282
+ * 单文件下载完成回调
283
+ * @param downloadComplete
284
+ */
285
+ fileDownloadCompleteCallback = data => {
286
+ if (data) {
287
+ const {
288
+ fileName,
289
+ index
290
+ } = data;
291
+ if (fileName) {
292
+ this.fileIndex = index;
293
+ const path = this.downloadType === 0 ? this.getDataFilePath(fileName) : this.getStreamFilePath(fileName);
294
+ this.readFileFromPath(fileName, path);
295
+ }
296
+ }
297
+ };
298
+
299
+ /**
300
+ * p2p数据流回调
301
+ * @param downloadComplete
302
+ */
303
+ p2pStreamPacketReceiveCallback = data => {
304
+ const {
305
+ fileName,
306
+ packetData,
307
+ fileSerialNumber,
308
+ packetIndex,
309
+ packetType
310
+ } = data;
311
+ const cachePacketMap = this.packetDataCacheMap.get(fileName);
312
+ const cacheSerialNumber = this.packetSerialNumberCacheMap.get(fileName);
313
+
314
+ // 说明收到了过时包的数据
315
+ if (fileSerialNumber < cacheSerialNumber) return;
316
+
317
+ // 说明收到了新的整包数据
318
+ if (fileSerialNumber > cacheSerialNumber) {
319
+ this.packetSerialNumberCacheMap.set(fileName, fileSerialNumber);
320
+ this.packetTotalMap.set(fileName, -1);
321
+ cachePacketMap.clear();
322
+ }
323
+ if (packetType === 2 || packetType === 3) {
324
+ // 收到了末尾包, packetIndex + 1代表这组包的总数
325
+ this.packetTotalMap.set(fileName, packetIndex + 1);
326
+ }
327
+ const packetTotal = this.packetTotalMap.get(fileName);
328
+ cachePacketMap.set(packetIndex, packetData);
329
+ if (packetTotal > -1) {
330
+ // 说明收到过末尾包了,只需要验证总包数是否吻合
331
+ const packetDatas = [...cachePacketMap.entries()];
332
+ if (packetDatas.length === packetTotal) {
333
+ packetDatas.sort((a, b) => a[0] - b[0]);
334
+
335
+ // 排序 - 组装 - 发出
336
+ const hexValue = packetDatas.map(_ref => {
337
+ let [index, data] = _ref;
338
+ return data;
339
+ }).join('');
340
+ const {
341
+ type
342
+ } = FILE_NAME_MAP[fileName];
343
+ if (this.cacheData[type] !== hexValue) {
344
+ if (type === 0) {
345
+ this.onReceiveMapData(hexValue);
346
+ }
347
+ if (type === 1) {
348
+ this.onReceivePathData(hexValue);
349
+ }
350
+ this.cacheData[type] = hexValue;
351
+ }
352
+ }
353
+ }
354
+ };
355
+
356
+ /**
357
+ * 单文件下载完成回调
358
+ * @param data
359
+ */
360
+ fileDownloadProgressCallback = () => {
361
+ // console.log('fileDownloadProgressCallback', data);
362
+ };
363
+
364
+ /**
365
+ * 多文件下载进度监听
366
+ * @param data
367
+ */
368
+ fileTotalDownloadProgressCallback = () => {
369
+ // console.log('fileTotalDownloadProgressCallback', data );
370
+ };
371
+
372
+ /**
373
+ * 从指定的文件路径获取文件
374
+ * @param fileName
375
+ * @param filePath
376
+ */
377
+ readFileFromPath = (fileName, filePath) => {
378
+ if (!this.setReading(fileName)) return;
379
+ try {
380
+ ty.getFileSystemManager().readFile({
381
+ filePath,
382
+ encoding: 'base64',
383
+ position: 0,
384
+ success: params => {
385
+ this.resetReading(fileName);
386
+ const bytes = Base64.toByteArray(params.data);
387
+ const hexValue = _(bytes).map(d => padStart(d.toString(16), 2, '0')).value().join('');
388
+ const type = this.getFileType(fileName);
389
+ if (this.cacheData[type] !== hexValue) {
390
+ if (hexValue.length === 0) {
391
+ Logger.warn('receive empty data');
392
+ return;
393
+ }
394
+ if (type === 0) {
395
+ this.onReceiveMapData(hexValue);
396
+ }
397
+ if (type === 1) {
398
+ this.onReceivePathData(hexValue);
399
+ }
400
+ this.cacheData[type] = hexValue;
401
+ }
402
+ },
403
+ fail: e => {
404
+ Logger.warn('readFileFromPath failed ==>', e);
405
+ this.resetReading(fileName);
406
+ }
407
+ });
408
+ } catch (e) {
409
+ this.resetReading(fileName);
410
+ Logger.error('readFileFromPath ==>', e);
411
+ }
412
+ };
413
+ setReading = fileName => {
414
+ if (fileName.indexOf('map') !== -1) {
415
+ if (this.readingMap === true) return false;
416
+ this.readingMap = true;
417
+ return true;
418
+ }
419
+ if (fileName.indexOf('cleanPath') !== -1) {
420
+ if (this.readingClean === true) return false;
421
+ this.readingClean = true;
422
+ return true;
423
+ }
424
+ if (fileName.indexOf('navPath') !== -1) {
425
+ if (this.readingNav === true) return false;
426
+ this.readingNav = true;
427
+ return true;
428
+ }
429
+ return true;
430
+ };
431
+ resetReading = fileName => {
432
+ if (fileName.indexOf('map') !== -1) {
433
+ this.readingMap = false;
434
+ } else if (fileName.indexOf('cleanPath') !== -1) {
435
+ this.readingClean = false;
436
+ } else if (fileName.indexOf('navPath') !== -1) {
437
+ this.readingNav = false;
438
+ }
439
+ };
440
+
441
+ /**
442
+ * 查询返回的文件中是否包含了所需文件
443
+ * @param fileList
444
+ */
445
+ queryNeedFiles = fileList => {
446
+ const exitFiles = [];
447
+ const streamPattern = /bin.stream$/;
448
+ const dataPattern = /bin$/;
449
+ fileList && fileList.forEach(item => {
450
+ if (this.downloadType === 0) {
451
+ if (dataPattern.test(item.filename)) {
452
+ exitFiles.push(item.filename);
453
+ }
454
+ } else if (this.downloadType === 1) {
455
+ if (streamPattern.test(item.filename)) {
456
+ exitFiles.push(item.filename);
457
+ }
458
+ }
459
+ });
460
+ return exitFiles;
461
+ };
462
+
463
+ /**
464
+ * 停止文件下载
465
+ */
466
+ stopObserverSweeperDataByP2P = async () => {
467
+ try {
468
+ this.removeP2pDownloadEvent();
469
+ const isCancel = await this.cancelDownloadTask();
470
+ Logger.info('cancelDownloadTask ==>', isCancel);
471
+ return isCancel;
472
+ } catch (e) {
473
+ Logger.error('stopObserverSweeperDataByP2P occur error ==>', e);
474
+ return false;
475
+ }
476
+ };
477
+ }
478
+ const sweeperP2pInstance = new SweeperP2p();
479
+ export default sweeperP2pInstance;
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ declare const useP2PDataStream: (devId: string, onReceiveMapData: (data: string) => void, onReceivePathData: (data: string) => void) => void;
2
+ declare const StreamDataNotificationCenter: import("mitt").Emitter<Record<import("mitt").EventType, unknown>>;
3
+ export { useP2PDataStream, StreamDataNotificationCenter };
package/lib/index.js ADDED
@@ -0,0 +1,93 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import mitt from 'mitt';
3
+ import { SweeperP2pInstance } from '@/api';
4
+ import { Logger } from './utils';
5
+ const useP2PDataStream = (devId, onReceiveMapData, onReceivePathData) => {
6
+ const isInit = useRef(null);
7
+ const offSessionStatusChange = useRef(null);
8
+ const isAppOnBackground = useRef(false);
9
+ const timer = useRef(null);
10
+ useEffect(() => {
11
+ if (devId.startsWith('vdevo')) {
12
+ Logger.warn('virtual device cannot use p2p');
13
+ return;
14
+ }
15
+ isInitP2p();
16
+ onEnterBackground();
17
+ onEnterForeground();
18
+ return () => {
19
+ unmount();
20
+ timer && clearInterval(timer);
21
+ };
22
+ }, []);
23
+
24
+ /**
25
+ * p2p连接
26
+ */
27
+ const isInitP2p = async () => {
28
+ Logger.info('hooks has been started initP2p');
29
+ isInit.current = await SweeperP2pInstance.initP2pSdk(devId);
30
+ if (isInit.current) {
31
+ SweeperP2pInstance.connectDevice(() => {
32
+ SweeperP2pInstance.startObserverSweeperDataByP2P(1, devId, onReceiveMapData, onReceivePathData);
33
+ offSessionStatusChange.current = SweeperP2pInstance.onSessionStatusChange(SweeperP2pInstance.sessionStatusCallback);
34
+ }, () => {
35
+ SweeperP2pInstance.reconnectP2p(() => {
36
+ SweeperP2pInstance.startObserverSweeperDataByP2P(1, devId, onReceiveMapData, onReceivePathData);
37
+ // 这里失败重连需要注册断开重连的事件
38
+ offSessionStatusChange.current = SweeperP2pInstance.onSessionStatusChange(SweeperP2pInstance.sessionStatusCallback);
39
+ });
40
+ });
41
+ }
42
+ };
43
+
44
+ /**
45
+ * 进入后台时断开P2P连接
46
+ */
47
+ const onEnterBackground = () => {
48
+ ty.onAppHide(() => {
49
+ Logger.info('hooks onAppHide');
50
+ isAppOnBackground.current = true;
51
+ if (isInit.current) {
52
+ // 判断进入后台之后,维持定时器,如果进入后台超过2分钟, 则断开P2P
53
+ timer.current = setTimeout(() => {
54
+ Logger.info('background timer has been exe');
55
+ if (isAppOnBackground.current) {
56
+ unmount();
57
+ }
58
+ clearTimeout(timer.current);
59
+ timer.current = null;
60
+ }, 2 * 60 * 1000);
61
+ }
62
+ });
63
+ };
64
+
65
+ /**
66
+ * p2p断开
67
+ */
68
+ const unmount = async () => {
69
+ Logger.info('hooks has been started unmount');
70
+ isInit.current = false;
71
+ if (offSessionStatusChange.current) {
72
+ offSessionStatusChange.current();
73
+ offSessionStatusChange.current = null;
74
+ }
75
+ await SweeperP2pInstance.stopObserverSweeperDataByP2P();
76
+ await SweeperP2pInstance.deInitP2PSDK();
77
+ };
78
+
79
+ /**
80
+ * 进入前台时开启P2P连接
81
+ */
82
+ const onEnterForeground = () => {
83
+ ty.onAppShow(() => {
84
+ Logger.info('hooks onAppShow');
85
+ isAppOnBackground.current = false;
86
+ if (!isInit.current) {
87
+ isInitP2p();
88
+ }
89
+ });
90
+ };
91
+ };
92
+ const StreamDataNotificationCenter = mitt();
93
+ export { useP2PDataStream, StreamDataNotificationCenter };
@@ -0,0 +1,2 @@
1
+ import Logger from './logger';
2
+ export { Logger };
@@ -0,0 +1,2 @@
1
+ import Logger from './logger';
2
+ export { Logger };
@@ -0,0 +1,15 @@
1
+ declare class Logger {
2
+ options: {
3
+ performance: string;
4
+ error: string;
5
+ info: string;
6
+ warn: string;
7
+ };
8
+ log(color: string, title: string, ...args: any): void;
9
+ performance(title: string, ...args: any): void;
10
+ warn(title: string, ...args: any): void;
11
+ error(title: string, ...args: any): void;
12
+ info(title: string, ...args: any): void;
13
+ }
14
+ declare const logger: Logger;
15
+ export default logger;
@@ -0,0 +1,44 @@
1
+ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2
+ class Logger {
3
+ options = {
4
+ performance: '#00cca3',
5
+ error: '#f81c1c',
6
+ info: '#5091f3',
7
+ warn: '#ffaa00'
8
+ };
9
+ log(color, title) {
10
+ for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
11
+ args[_key - 2] = arguments[_key];
12
+ }
13
+ console.log(`%c 【Robot Data Stream Log】: ${title}`, `background: ${color}; color: #FFFFFF; font-size: 20px`, ...args);
14
+ }
15
+ performance(title) {
16
+ for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
17
+ args[_key2 - 1] = arguments[_key2];
18
+ }
19
+ this.log(this.options.performance, title, ...args);
20
+ }
21
+ warn(title) {
22
+ for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
23
+ args[_key3 - 1] = arguments[_key3];
24
+ }
25
+ this.log(this.options.warn, title, ...args);
26
+ }
27
+ error(title) {
28
+ if (title === 'system error') {
29
+ debugger;
30
+ }
31
+ for (var _len4 = arguments.length, args = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
32
+ args[_key4 - 1] = arguments[_key4];
33
+ }
34
+ this.log(this.options.error, title, ...args);
35
+ }
36
+ info(title) {
37
+ for (var _len5 = arguments.length, args = new Array(_len5 > 1 ? _len5 - 1 : 0), _key5 = 1; _key5 < _len5; _key5++) {
38
+ args[_key5 - 1] = arguments[_key5];
39
+ }
40
+ this.log(this.options.info, title, ...args);
41
+ }
42
+ }
43
+ const logger = new Logger();
44
+ export default logger;
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@ray-js/robot-data-stream",
3
+ "version": "0.0.1-beta-1",
4
+ "description": "扫地机P2P数据流标准化组件",
5
+ "main": "lib/index",
6
+ "files": [
7
+ "lib"
8
+ ],
9
+ "license": "MIT",
10
+ "types": "lib/index.d.ts",
11
+ "maintainers": [
12
+ "tuya_npm",
13
+ {
14
+ "name": "tuyafe",
15
+ "email": "tuyafe@tuya.com"
16
+ }
17
+ ],
18
+ "scripts": {
19
+ "lint": "eslint src --ext .js,.jsx,.ts,.tsx --fix",
20
+ "build": "patch-package && ray build --type=component",
21
+ "watch": "ray start --type=component --output ./example/src/lib",
22
+ "build:tuya": "ray build ./example",
23
+ "build:wechat": "ray build ./example --target=wechat",
24
+ "build:web": "ray build ./example --target=web",
25
+ "build:native": "ray build ./example --target=native",
26
+ "start:native": "ray start ./example -t native --verbose",
27
+ "start:tuya": "ray start ./example",
28
+ "start:wechat": "ray start ./example -t wechat --verbose",
29
+ "start:web": "ray start ./example -t web",
30
+ "prepublishOnly": "yarn build",
31
+ "release-it": "standard-version"
32
+ },
33
+ "peerDependencies": {
34
+ "@ray-js/ray": "^1.5.0"
35
+ },
36
+ "dependencies": {
37
+ "clsx": "^1.2.1",
38
+ "lodash-es": "^4.17.21",
39
+ "moment": "^2.30.1",
40
+ "mitt": "^3.0.1"
41
+ },
42
+ "devDependencies": {
43
+ "@commitlint/cli": "^7.2.1",
44
+ "@commitlint/config-conventional": "^9.0.1",
45
+ "@ray-js/cli": "^1.5.20",
46
+ "@ray-js/ray": "^1.5.0",
47
+ "core-js": "^3.19.1",
48
+ "eslint-config-tuya-panel": "^0.4.2",
49
+ "husky": "^1.2.0",
50
+ "lint-staged": "^10.2.11",
51
+ "patch-package": "^8.0.0",
52
+ "standard-version": "9.3.2"
53
+ },
54
+ "resolutions": {
55
+ "@ray-js/builder-mp": "1.4.15"
56
+ },
57
+ "husky": {
58
+ "hooks": {
59
+ "commit-msg": "commitlint -E HUSKY_GIT_PARAMS --config commitlint.config.js",
60
+ "pre-commit": "lint-staged"
61
+ }
62
+ },
63
+ "lint-staged": {
64
+ "*.{ts,tsx,js,jsx}": [
65
+ "eslint --fix",
66
+ "git add"
67
+ ],
68
+ "*.{json,md,yml,yaml}": [
69
+ "prettier --write",
70
+ "git add"
71
+ ]
72
+ }
73
+ }