@ray-js/robot-data-stream 0.0.17-beta.8 → 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/cruise.d.ts +2 -0
- package/lib/mqtt/cruise.js +9 -0
- 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 +8 -1
- package/lib/mqtt/index.js +12 -1
- package/lib/mqtt/promise.d.ts +21 -6
- package/lib/mqtt/promise.js +47 -12
- package/lib/mqtt/spotPolygons.d.ts +7 -0
- package/lib/mqtt/spotPolygons.js +18 -0
- package/lib/mqtt/type/protocols/carpetProtocol.d.ts +2 -0
- package/lib/mqtt/type/protocols/cruiseProtocol.d.ts +6 -3
- package/lib/mqtt/type/protocols/index.d.ts +3 -0
- package/lib/mqtt/type/protocols/index.js +4 -1
- package/lib/mqtt/type/protocols/spotPolygonsProtocol.d.ts +21 -0
- package/lib/mqtt/type/protocols/spotPolygonsProtocol.js +1 -0
- 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 +8 -0
- package/lib/mqtt/type/requestType.js +14 -0
- package/lib/mqtt/useCarpet.js +12 -4
- package/lib/mqtt/useCarpetClean.js +6 -2
- package/lib/mqtt/useCruise.js +4 -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.d.ts +9 -0
- package/lib/mqtt/useSpotPolygons.js +50 -0
- 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/lib/index.js
CHANGED
|
@@ -7,6 +7,8 @@ import { trace } from './trace';
|
|
|
7
7
|
import { emitter, logger, logP2PDataIfEnabled } from './utils';
|
|
8
8
|
export * from './mqtt';
|
|
9
9
|
export * from './mqtt/type/requestType';
|
|
10
|
+
// 手拼标准指令 payload(自定义指令的字段级扩展场景)需要复用的纯工具
|
|
11
|
+
export { pointsToString, pickWaterFields } from './utils';
|
|
10
12
|
const useP2PDataStream = (devId, onReceiveMapData, onReceivePathData, opt) => {
|
|
11
13
|
const traceId = `${devId}_${Date.now()}`;
|
|
12
14
|
global.robotPanelTraceId = traceId;
|
|
@@ -17,9 +19,61 @@ const useP2PDataStream = (devId, onReceiveMapData, onReceivePathData, opt) => {
|
|
|
17
19
|
onReceiveAIPicHDData,
|
|
18
20
|
onReceiveWifiMapData,
|
|
19
21
|
onLogger,
|
|
20
|
-
onDefineStructuredMode
|
|
22
|
+
onDefineStructuredMode,
|
|
23
|
+
onError,
|
|
24
|
+
firstFrameTimeout = 8000
|
|
21
25
|
} = opt || {};
|
|
22
26
|
SweeperP2pInstance.enableCustomLog = enableCustomLog;
|
|
27
|
+
SweeperP2pInstance.onError = onError;
|
|
28
|
+
const onReceiveMapDataRef = useRef(onReceiveMapData);
|
|
29
|
+
onReceiveMapDataRef.current = onReceiveMapData;
|
|
30
|
+
const onReceivePathDataRef = useRef(onReceivePathData);
|
|
31
|
+
onReceivePathDataRef.current = onReceivePathData;
|
|
32
|
+
const onErrorRef = useRef(onError);
|
|
33
|
+
onErrorRef.current = onError;
|
|
34
|
+
const firstFrameTimer = useRef(null);
|
|
35
|
+
const hasReceivedFirstFrame = useRef(false);
|
|
36
|
+
const clearFirstFrameTimer = useCallback(() => {
|
|
37
|
+
if (firstFrameTimer.current) {
|
|
38
|
+
clearTimeout(firstFrameTimer.current);
|
|
39
|
+
firstFrameTimer.current = null;
|
|
40
|
+
}
|
|
41
|
+
hasReceivedFirstFrame.current = true;
|
|
42
|
+
}, []);
|
|
43
|
+
const startFirstFrameWatchdog = useCallback(() => {
|
|
44
|
+
clearFirstFrameTimer();
|
|
45
|
+
hasReceivedFirstFrame.current = false;
|
|
46
|
+
if (firstFrameTimeout && firstFrameTimeout > 0) {
|
|
47
|
+
firstFrameTimer.current = setTimeout(() => {
|
|
48
|
+
if (!hasReceivedFirstFrame.current) {
|
|
49
|
+
var _onErrorRef$current;
|
|
50
|
+
logger('warn', {
|
|
51
|
+
msg: `First frame timeout (${firstFrameTimeout}ms), triggering reDownloadStream`
|
|
52
|
+
}, onLogger);
|
|
53
|
+
logP2PDataIfEnabled(enableCustomLog, devId, 'first_frame_timeout', `First frame timeout after ${firstFrameTimeout}ms, triggering reDownloadStream`);
|
|
54
|
+
(_onErrorRef$current = onErrorRef.current) === null || _onErrorRef$current === void 0 || _onErrorRef$current.call(onErrorRef, {
|
|
55
|
+
code: 'FIRST_FRAME_TIMEOUT',
|
|
56
|
+
message: `First frame not received within ${firstFrameTimeout}ms`
|
|
57
|
+
});
|
|
58
|
+
SweeperP2pInstance.reDownloadStream('first_frame_timeout');
|
|
59
|
+
}
|
|
60
|
+
}, firstFrameTimeout);
|
|
61
|
+
}
|
|
62
|
+
}, [firstFrameTimeout, devId, enableCustomLog, onLogger, clearFirstFrameTimer]);
|
|
63
|
+
const handleReceiveMapData = useCallback(data => {
|
|
64
|
+
var _onReceiveMapDataRef$;
|
|
65
|
+
if (data && data.length > 0) {
|
|
66
|
+
clearFirstFrameTimer();
|
|
67
|
+
}
|
|
68
|
+
(_onReceiveMapDataRef$ = onReceiveMapDataRef.current) === null || _onReceiveMapDataRef$ === void 0 || _onReceiveMapDataRef$.call(onReceiveMapDataRef, data);
|
|
69
|
+
}, [clearFirstFrameTimer]);
|
|
70
|
+
const handleReceivePathData = useCallback(data => {
|
|
71
|
+
var _onReceivePathDataRef;
|
|
72
|
+
if (data && data.length > 0) {
|
|
73
|
+
clearFirstFrameTimer();
|
|
74
|
+
}
|
|
75
|
+
(_onReceivePathDataRef = onReceivePathDataRef.current) === null || _onReceivePathDataRef === void 0 || _onReceivePathDataRef.call(onReceivePathDataRef, data);
|
|
76
|
+
}, [clearFirstFrameTimer]);
|
|
23
77
|
logP2PDataIfEnabled(enableCustomLog, devId, 'hooks_init', `useP2PDataStream initialized, traceId: ${traceId}`);
|
|
24
78
|
const isInit = useRef(false);
|
|
25
79
|
const offSessionStatusChange = useRef(null);
|
|
@@ -145,7 +199,8 @@ const useP2PDataStream = (devId, onReceiveMapData, onReceivePathData, opt) => {
|
|
|
145
199
|
});
|
|
146
200
|
isInit.current = true;
|
|
147
201
|
logP2PDataIfEnabled(enableCustomLog, devId, 'start_observer_data', 'Starting to observe sweeper data by P2P');
|
|
148
|
-
|
|
202
|
+
startFirstFrameWatchdog();
|
|
203
|
+
SweeperP2pInstance.startObserverSweeperDataByP2P(1, devId, handleReceiveMapData, handleReceivePathData, onReceiveAIPicData, onReceiveAIPicHDData, onReceiveWifiMapData, onDefineStructuredMode);
|
|
149
204
|
offSessionStatusChange.current = SweeperP2pInstance.onSessionStatusChange(SweeperP2pInstance.sessionStatusCallback);
|
|
150
205
|
}, () => {
|
|
151
206
|
logP2PDataIfEnabled(enableCustomLog, devId, 'connect_p2p_fail', 'P2P connection failed, attempting reconnect');
|
|
@@ -161,7 +216,8 @@ const useP2PDataStream = (devId, onReceiveMapData, onReceivePathData, opt) => {
|
|
|
161
216
|
eventName: 'reconnectP2p'
|
|
162
217
|
});
|
|
163
218
|
logP2PDataIfEnabled(enableCustomLog, devId, 'start_observer_data_after_reconnect', 'Starting to observe sweeper data after reconnect');
|
|
164
|
-
|
|
219
|
+
startFirstFrameWatchdog();
|
|
220
|
+
SweeperP2pInstance.startObserverSweeperDataByP2P(1, devId, handleReceiveMapData, handleReceivePathData, onReceiveAIPicData, onReceiveAIPicHDData, onReceiveWifiMapData, onDefineStructuredMode);
|
|
165
221
|
// 这里失败重连需要注册断开重连的事件
|
|
166
222
|
offSessionStatusChange.current = SweeperP2pInstance.onSessionStatusChange(SweeperP2pInstance.sessionStatusCallback);
|
|
167
223
|
}, '');
|
|
@@ -177,6 +233,7 @@ const useP2PDataStream = (devId, onReceiveMapData, onReceivePathData, opt) => {
|
|
|
177
233
|
}, onLogger);
|
|
178
234
|
logP2PDataIfEnabled(enableCustomLog, devId, 'unmount_p2p', 'Unmounting P2P connection');
|
|
179
235
|
isInit.current = false;
|
|
236
|
+
clearFirstFrameTimer();
|
|
180
237
|
SweeperP2pInstance.stopObserverSweeperDataByP2P();
|
|
181
238
|
if (offSessionStatusChange.current) {
|
|
182
239
|
offSessionStatusChange.current();
|
|
@@ -43,25 +43,7 @@ export interface CreateSetCommonParamsOutput {
|
|
|
43
43
|
options: MqttOptions;
|
|
44
44
|
/** 协议号 */
|
|
45
45
|
protocol: ProtocolEnum;
|
|
46
|
+
/** 本次下发生成的 taskId(已拼进 message;LAN 场景 message 为 JSON 字符串,从这里取) */
|
|
47
|
+
taskId: string;
|
|
46
48
|
}
|
|
47
|
-
/**
|
|
48
|
-
* 创建 MQTT 通用参数
|
|
49
|
-
*
|
|
50
|
-
* @param params - 参数对象
|
|
51
|
-
* @param params.deviceId - 设备ID
|
|
52
|
-
* @param params.reqType - 请求类型
|
|
53
|
-
* @param params.message - 消息体(可选)
|
|
54
|
-
* @param params.version - 版本号(可选,默认为 1.0.0)
|
|
55
|
-
* @param params.options - 选项(可选)
|
|
56
|
-
* @returns MQTT 通用参数对象
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```ts
|
|
60
|
-
* const params = createSetCommonParams({
|
|
61
|
-
* deviceId: 'device123',
|
|
62
|
-
* reqType: 'devInfoQry',
|
|
63
|
-
* message: { customField: 'value' }
|
|
64
|
-
* });
|
|
65
|
-
* ```
|
|
66
|
-
*/
|
|
67
49
|
export declare const createSetCommonParams: ({ deviceId, reqType, isLocalOnLine, message, version, options, }: CreateSetCommonParamsInput) => CreateSetCommonParamsOutput;
|
|
@@ -42,6 +42,13 @@ const DEFAULT_VERSION = '1.0.0';
|
|
|
42
42
|
* });
|
|
43
43
|
* ```
|
|
44
44
|
*/
|
|
45
|
+
/**
|
|
46
|
+
* 同毫秒内的自增序号:保证并发下发(如双击连发)也能拿到互不相同的 taskId,
|
|
47
|
+
* 否则同一毫秒内的两条请求 taskId 相同,响应匹配无法区分。
|
|
48
|
+
* 固件侧 taskId 按不透明字符串暂存并原样带回(__mqtt_task_id_pack),追加后缀不影响协议。
|
|
49
|
+
*/
|
|
50
|
+
let taskSequence = 0;
|
|
51
|
+
const createTaskId = () => `${Date.now()}_${++taskSequence}`;
|
|
45
52
|
export const createSetCommonParams = _ref => {
|
|
46
53
|
let {
|
|
47
54
|
deviceId,
|
|
@@ -51,12 +58,16 @@ export const createSetCommonParams = _ref => {
|
|
|
51
58
|
version = DEFAULT_VERSION,
|
|
52
59
|
options = {}
|
|
53
60
|
} = _ref;
|
|
54
|
-
const taskId =
|
|
55
|
-
|
|
61
|
+
const taskId = createTaskId();
|
|
62
|
+
|
|
63
|
+
// taskId 由本库统一生成并权威覆盖:message 先展开,避免调用方(或照协议文档抄示例的
|
|
64
|
+
// 定制开发者)在 message 里夹带 taskId 覆盖线上值——那会让回执与固件回显永远对不上,
|
|
65
|
+
// 双匹配直接退化成必然超时。回执 taskId 始终与线上 message.taskId 一致。
|
|
66
|
+
const messageData = _objectSpread(_objectSpread({}, message), {}, {
|
|
56
67
|
reqType,
|
|
57
68
|
version,
|
|
58
|
-
taskId
|
|
59
|
-
}
|
|
69
|
+
taskId
|
|
70
|
+
});
|
|
60
71
|
return {
|
|
61
72
|
deviceId,
|
|
62
73
|
message: !isLocalOnLine ? messageData : JSON.stringify({
|
|
@@ -64,6 +75,7 @@ export const createSetCommonParams = _ref => {
|
|
|
64
75
|
reqType
|
|
65
76
|
}),
|
|
66
77
|
options,
|
|
67
|
-
protocol: ProtocolEnum.appToRobot
|
|
78
|
+
protocol: ProtocolEnum.appToRobot,
|
|
79
|
+
taskId
|
|
68
80
|
};
|
|
69
81
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
|
2
|
+
export const createCruisePayload = message => {
|
|
3
|
+
if (message.polygons && message.num === undefined) {
|
|
4
|
+
return _objectSpread(_objectSpread({}, message), {}, {
|
|
5
|
+
num: message.polygons.length
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
return message;
|
|
9
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { BaseResponse } from './type/protocols/base';
|
|
2
|
+
/**
|
|
3
|
+
* 指令声明配置
|
|
4
|
+
*/
|
|
5
|
+
export interface RobotCommandConfig {
|
|
6
|
+
/** 下发 reqType */
|
|
7
|
+
reqType: string;
|
|
8
|
+
/**
|
|
9
|
+
* 响应匹配的事件名(reqType)。
|
|
10
|
+
* 缺省与下发同名;set 类指令回复走 qry 事件名、
|
|
11
|
+
* 或回复 reqType 与下发不同名(如 aiObjectIgnoreSet → aiObjectIgnoreRst)时必填。
|
|
12
|
+
*/
|
|
13
|
+
responseType?: string;
|
|
14
|
+
/** 协议版本号,默认 '1.0.0' */
|
|
15
|
+
version?: string;
|
|
16
|
+
/** 超时时间(毫秒),默认 10000 */
|
|
17
|
+
timeout?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* defineRobotCommand 的返回:两个独立 hook,在组件内分别调用
|
|
21
|
+
*/
|
|
22
|
+
export interface DefinedRobotCommand<TReq = Record<string, any>, TRes extends BaseResponse = BaseResponse> {
|
|
23
|
+
/**
|
|
24
|
+
* 发送 hook,与内置 hook 同构(Promise 命令函数、无 loading 态)。
|
|
25
|
+
* 固件侧指令标准化入库后,从 useSend 换成对应内置 useXxx ≈ 改一行 import。
|
|
26
|
+
*/
|
|
27
|
+
useSend: () => {
|
|
28
|
+
send: (data: TReq) => Promise<TRes>;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 类型化监听 hook:订阅 responseType(缺省 reqType)的上报,
|
|
32
|
+
* 是 StreamDataNotificationCenter.on 的类型化薄包装。
|
|
33
|
+
* handler 随渲染更新,订阅本身只在挂载期间维持一份。
|
|
34
|
+
*/
|
|
35
|
+
useResponse: (handler: (data: TRes) => void) => void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 声明一条自定义指令。
|
|
39
|
+
*
|
|
40
|
+
* 面向定制面板:协议描述(reqType / 响应名 / 类型)归消费方,库只负责执行。
|
|
41
|
+
* 在模块顶层调用一次,组件内使用返回的 useSend / useResponse。
|
|
42
|
+
* 完整示例见 docs/reference/custom-commands.md。
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* export const peakValleyCharge = defineRobotCommand<SetPeakValleyData, PeakValleyResponse>({
|
|
47
|
+
* reqType: 'peakValleyChargeSet',
|
|
48
|
+
* responseType: 'peakValleyChargeQry',
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare const defineRobotCommand: <TReq extends Record<string, any> = Record<string, any>, TRes extends BaseResponse = BaseResponse>(config: RobotCommandConfig) => DefinedRobotCommand<TReq, TRes>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
|
2
|
+
// 自定义指令声明工厂:把「一条指令 = reqType + 类型」固化成可复用的 hook 集
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { emitter } from '../utils';
|
|
5
|
+
import { normalizeResultFields, unwrapMqttResponse } from './promise';
|
|
6
|
+
import { useCustomCommand } from './useCustomCommand';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 指令声明配置
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* defineRobotCommand 的返回:两个独立 hook,在组件内分别调用
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 声明一条自定义指令。
|
|
18
|
+
*
|
|
19
|
+
* 面向定制面板:协议描述(reqType / 响应名 / 类型)归消费方,库只负责执行。
|
|
20
|
+
* 在模块顶层调用一次,组件内使用返回的 useSend / useResponse。
|
|
21
|
+
* 完整示例见 docs/reference/custom-commands.md。
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* export const peakValleyCharge = defineRobotCommand<SetPeakValleyData, PeakValleyResponse>({
|
|
26
|
+
* reqType: 'peakValleyChargeSet',
|
|
27
|
+
* responseType: 'peakValleyChargeQry',
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const defineRobotCommand = config => {
|
|
32
|
+
const {
|
|
33
|
+
reqType,
|
|
34
|
+
responseType,
|
|
35
|
+
version,
|
|
36
|
+
timeout
|
|
37
|
+
} = config;
|
|
38
|
+
const listenType = responseType || reqType;
|
|
39
|
+
return {
|
|
40
|
+
useSend: () => {
|
|
41
|
+
const {
|
|
42
|
+
sendCustomCommand
|
|
43
|
+
} = useCustomCommand();
|
|
44
|
+
return {
|
|
45
|
+
send: data => sendCustomCommand(reqType, data, {
|
|
46
|
+
responseType,
|
|
47
|
+
version,
|
|
48
|
+
timeout
|
|
49
|
+
})
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
useResponse: handler => {
|
|
53
|
+
const handlerRef = useRef(handler);
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
handlerRef.current = handler;
|
|
56
|
+
});
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
// 与 normalResolve 同一套归一(unwrapMqttResponse + normalizeResultFields):
|
|
59
|
+
// LAN socket 通道不经过 mqttProvider 归一,这里补齐,保证两条通道形状一致
|
|
60
|
+
const wrapped = message => {
|
|
61
|
+
const payload = unwrapMqttResponse(message);
|
|
62
|
+
handlerRef.current(_objectSpread(_objectSpread({}, payload), normalizeResultFields(payload)));
|
|
63
|
+
};
|
|
64
|
+
emitter.on(listenType, wrapped);
|
|
65
|
+
return () => {
|
|
66
|
+
emitter.off(listenType, wrapped);
|
|
67
|
+
};
|
|
68
|
+
}, [listenType]);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
@@ -5,7 +5,11 @@ import { BaseResponse } from '../type/protocols/base';
|
|
|
5
5
|
* 提供统一的 sendMqttMessage 方法,自动从 Context 获取 devId 和配置
|
|
6
6
|
*/
|
|
7
7
|
export declare const useStructuredMessage: () => {
|
|
8
|
-
sendStructuredMessage: <T extends BaseResponse>(reqType: string, message: Record<string, any> | undefined, resolve: (value: T) => void, reject: (reason?: any) => void,
|
|
8
|
+
sendStructuredMessage: <T extends BaseResponse>(reqType: string, message: Record<string, any> | undefined, resolve: (value: T) => void, reject: (reason?: any) => void, options?: {
|
|
9
|
+
responseType?: string;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
version?: string;
|
|
12
|
+
}) => void;
|
|
9
13
|
};
|
|
10
14
|
/**
|
|
11
15
|
* 错误处理工具函数
|
|
@@ -91,12 +91,13 @@ export const useStructuredMessage = () => {
|
|
|
91
91
|
};
|
|
92
92
|
|
|
93
93
|
// 创建消息参数
|
|
94
|
-
const createMessageParams = (reqType, message, useLocal) => {
|
|
94
|
+
const createMessageParams = (reqType, message, useLocal, version) => {
|
|
95
95
|
return createSetCommonParams({
|
|
96
96
|
deviceId: devId,
|
|
97
97
|
reqType,
|
|
98
98
|
message,
|
|
99
|
-
isLocalOnLine: useLocal
|
|
99
|
+
isLocalOnLine: useLocal,
|
|
100
|
+
version
|
|
100
101
|
});
|
|
101
102
|
};
|
|
102
103
|
|
|
@@ -139,7 +140,12 @@ export const useStructuredMessage = () => {
|
|
|
139
140
|
/**
|
|
140
141
|
* 发送结构化消息
|
|
141
142
|
*/
|
|
142
|
-
const sendStructuredMessage = useCallback((reqType, message, resolve, reject,
|
|
143
|
+
const sendStructuredMessage = useCallback((reqType, message, resolve, reject, options) => {
|
|
144
|
+
const {
|
|
145
|
+
responseType,
|
|
146
|
+
timeout,
|
|
147
|
+
version
|
|
148
|
+
} = options !== null && options !== void 0 ? options : {};
|
|
143
149
|
// 参数验证
|
|
144
150
|
if (!devId) {
|
|
145
151
|
reject(new Error('设备ID (devId) 为必填项'));
|
|
@@ -162,14 +168,24 @@ export const useStructuredMessage = () => {
|
|
|
162
168
|
method,
|
|
163
169
|
useLocal
|
|
164
170
|
} = selectedMethod;
|
|
165
|
-
const messageParams = createMessageParams(reqType, message, useLocal);
|
|
171
|
+
const messageParams = createMessageParams(reqType, message, useLocal, version);
|
|
166
172
|
|
|
167
173
|
// 成功回调
|
|
168
174
|
|
|
169
175
|
// 失败回调
|
|
170
176
|
|
|
171
|
-
//
|
|
172
|
-
|
|
177
|
+
// 处理响应:先挂监听再发送,避免回复先于监听注册到达而丢响应;
|
|
178
|
+
// taskId 参与匹配,同 reqType 并发请求不会互相串包
|
|
179
|
+
|
|
180
|
+
normalResolve(responseType || reqType, messageParams.taskId, resolve, reject, timeout);
|
|
181
|
+
|
|
182
|
+
// 发送消息(taskId 是给调用方的回执,不进传输参数)
|
|
183
|
+
method.send({
|
|
184
|
+
deviceId: messageParams.deviceId,
|
|
185
|
+
message: messageParams.message,
|
|
186
|
+
options: messageParams.options,
|
|
187
|
+
protocol: messageParams.protocol
|
|
188
|
+
}, () => {
|
|
173
189
|
const channelName = useLocal ? '局域网' : 'WiFi MQTT';
|
|
174
190
|
const fallbackSuffix = method === mqttCommunication && preferredChannel === COMMUNICATION_CHANNELS.LAN ? ' (备用)' : '';
|
|
175
191
|
logCommunication(LOG_LEVEL.INFO, reqType, `${channelName} 消息发送成功${fallbackSuffix}`, messageParams);
|
|
@@ -178,10 +194,6 @@ export const useStructuredMessage = () => {
|
|
|
178
194
|
error: JSON.stringify(error)
|
|
179
195
|
}));
|
|
180
196
|
});
|
|
181
|
-
|
|
182
|
-
// 处理响应
|
|
183
|
-
|
|
184
|
-
normalResolve(responseType || reqType, resolve, reject);
|
|
185
197
|
} catch (error) {
|
|
186
198
|
logCommunication(LOG_LEVEL.ERROR, reqType, `发送消息时发生异常: ${error}`);
|
|
187
199
|
reject(error);
|
package/lib/mqtt/index.d.ts
CHANGED
|
@@ -21,5 +21,12 @@ import { useDeviceModel } from './useDeviceModel';
|
|
|
21
21
|
import { useFurnitureModel } from './useFurnitureModel';
|
|
22
22
|
import { usePurifierSchedule } from './usePurifierSchedule';
|
|
23
23
|
import { useCruise } from './useCruise';
|
|
24
|
+
import { useSpotPolygons } from './useSpotPolygons';
|
|
24
25
|
import { useMcsRoomInfo } from './useMcsRoomInfo';
|
|
25
|
-
|
|
26
|
+
import { useThreshold } from './useThreshold';
|
|
27
|
+
import { useCustomCommand } from './useCustomCommand';
|
|
28
|
+
import { defineRobotCommand } from './defineRobotCommand';
|
|
29
|
+
export { useCarpet, useCarpetClean, useVoice, MqttProvider, useDeviceModel, useFurnitureModel, useDevInfo, usePartMerge, usePartDivision, usePassword, useWifiMap, useQuiteHours, useVirtualArea, useSpotClean, useZoneClean, useSelectRoomClean, useVirtualWall, useSchedule, useHistoryMap, useRoomProperty, useResetMap, usePurifierSchedule, useCruise, useSpotPolygons, useMcsRoomInfo, useThreshold, useCustomCommand, defineRobotCommand, };
|
|
30
|
+
export type { CustomCommandOptions, TSendCustomCommand } from './useCustomCommand';
|
|
31
|
+
export type { RobotCommandConfig, DefinedRobotCommand } from './defineRobotCommand';
|
|
32
|
+
export * from './type/protocols';
|
package/lib/mqtt/index.js
CHANGED
|
@@ -21,5 +21,16 @@ import { useDeviceModel } from './useDeviceModel';
|
|
|
21
21
|
import { useFurnitureModel } from './useFurnitureModel';
|
|
22
22
|
import { usePurifierSchedule } from './usePurifierSchedule';
|
|
23
23
|
import { useCruise } from './useCruise';
|
|
24
|
+
import { useSpotPolygons } from './useSpotPolygons';
|
|
24
25
|
import { useMcsRoomInfo } from './useMcsRoomInfo';
|
|
25
|
-
|
|
26
|
+
import { useThreshold } from './useThreshold';
|
|
27
|
+
import { useCustomCommand } from './useCustomCommand';
|
|
28
|
+
import { defineRobotCommand } from './defineRobotCommand';
|
|
29
|
+
export { useCarpet, useCarpetClean, useVoice, MqttProvider, useDeviceModel, useFurnitureModel, useDevInfo, usePartMerge, usePartDivision, usePassword, useWifiMap, useQuiteHours, useVirtualArea, useSpotClean, useZoneClean, useSelectRoomClean, useVirtualWall, useSchedule, useHistoryMap, useRoomProperty, useResetMap, usePurifierSchedule, useCruise, useSpotPolygons, useMcsRoomInfo, useThreshold, useCustomCommand, defineRobotCommand };
|
|
30
|
+
|
|
31
|
+
// 自定义指令相关类型
|
|
32
|
+
|
|
33
|
+
// 内置指令的 payload / 响应类型公共导出:
|
|
34
|
+
// 自定义指令在标准指令上加字段(手拼完整 payload 复用类型)、或包装内置 hook 时直接 import,
|
|
35
|
+
// 不再需要手抄(此前消费方因未导出抄过 SetRoomPropertyData)
|
|
36
|
+
export * from './type/protocols';
|
package/lib/mqtt/promise.d.ts
CHANGED
|
@@ -54,15 +54,30 @@ export interface MqttResponseMessage {
|
|
|
54
54
|
export type PromiseWithRejection = Promise<any> & {
|
|
55
55
|
reject: (reason?: MqttError) => void;
|
|
56
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* 拆掉响应外层的「信封」:仅当外层是纯信封形状(除 data / reqType 外没有别的字段,
|
|
59
|
+
* 且 data 为对象)才取内层——LAN 通道与部分固件的 set-ack 会把真实载荷包在 data 里。
|
|
60
|
+
* 业务 payload 自带 data 对象字段时不误拆,保证两条通道推出一致形状。
|
|
61
|
+
*/
|
|
62
|
+
export declare const unwrapMqttResponse: (message: any) => MqttResponseMessage;
|
|
63
|
+
/**
|
|
64
|
+
* 归一 success / errCode:errCode 优先,缺省按 success 推导(success === false → -1,否则 0)。
|
|
65
|
+
*/
|
|
66
|
+
export declare const normalizeResultFields: (payload: MqttResponseMessage) => {
|
|
67
|
+
errCode: number;
|
|
68
|
+
success: boolean;
|
|
69
|
+
};
|
|
57
70
|
/**
|
|
58
71
|
* 创建 MQTT 请求的 Promise,监听响应消息
|
|
59
72
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
73
|
+
* 匹配语义(taskId + reqType 双匹配):
|
|
74
|
+
* - 事件按 reqType 订阅;
|
|
75
|
+
* - 下发方传了 taskId、且响应携带 taskId 时,必须等值才视为本次请求的响应,
|
|
76
|
+
* 其余响应忽略并继续等待——同 reqType 并发请求不会互相串包;
|
|
77
|
+
* - 响应缺 taskId(部分固件不回 echo)时放行,避免退化成必然超时。
|
|
63
78
|
*
|
|
64
79
|
* @param reqType 请求类型,用于匹配响应消息
|
|
65
|
-
* @param taskId
|
|
80
|
+
* @param taskId 下发时生成的任务ID,用于并发区分;传 undefined 退化为 reqType 首响应匹配
|
|
66
81
|
* @param resolve Promise 的 resolve 回调
|
|
67
82
|
* @param reject Promise 的 reject 回调
|
|
68
83
|
* @param timeout 超时时间(毫秒),默认 10 秒
|
|
@@ -70,9 +85,9 @@ export type PromiseWithRejection = Promise<any> & {
|
|
|
70
85
|
* @example
|
|
71
86
|
* ```typescript
|
|
72
87
|
* return new Promise((resolve, reject) => {
|
|
73
|
-
* normalResolve('carpetQry', '', resolve, reject);
|
|
88
|
+
* normalResolve('carpetQry', '1730375500113_1', resolve, reject);
|
|
74
89
|
* });
|
|
75
90
|
* ```
|
|
76
91
|
*/
|
|
77
|
-
export declare function normalResolve(reqType: string, resolve: (value: any) => void, reject: (reason?: any) => void, timeout?: number): void;
|
|
92
|
+
export declare function normalResolve(reqType: string, taskId: string | undefined, resolve: (value: any) => void, reject: (reason?: any) => void, timeout?: number): void;
|
|
78
93
|
export type MyError = MqttError;
|
package/lib/mqtt/promise.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import "core-js/modules/esnext.iterator.constructor.js";
|
|
2
|
+
import "core-js/modules/esnext.iterator.every.js";
|
|
1
3
|
import { emitter, logger } from '../utils';
|
|
2
4
|
|
|
3
5
|
/**
|
|
@@ -86,15 +88,41 @@ export function dispatchUnhandledRejection(reason) {
|
|
|
86
88
|
* Promise 扩展类型,支持手动拒绝
|
|
87
89
|
*/
|
|
88
90
|
|
|
91
|
+
/**
|
|
92
|
+
* 拆掉响应外层的「信封」:仅当外层是纯信封形状(除 data / reqType 外没有别的字段,
|
|
93
|
+
* 且 data 为对象)才取内层——LAN 通道与部分固件的 set-ack 会把真实载荷包在 data 里。
|
|
94
|
+
* 业务 payload 自带 data 对象字段时不误拆,保证两条通道推出一致形状。
|
|
95
|
+
*/
|
|
96
|
+
export const unwrapMqttResponse = message => {
|
|
97
|
+
if (message != null && typeof message === 'object' && message.data != null && typeof message.data === 'object' && Object.keys(message).every(key => key === 'data' || key === 'reqType')) {
|
|
98
|
+
return message.data;
|
|
99
|
+
}
|
|
100
|
+
return message !== null && message !== void 0 ? message : {};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 归一 success / errCode:errCode 优先,缺省按 success 推导(success === false → -1,否则 0)。
|
|
105
|
+
*/
|
|
106
|
+
export const normalizeResultFields = payload => {
|
|
107
|
+
var _payload$errCode, _payload$success;
|
|
108
|
+
const errCode = (_payload$errCode = payload.errCode) !== null && _payload$errCode !== void 0 ? _payload$errCode : payload.success === false ? -1 : 0;
|
|
109
|
+
return {
|
|
110
|
+
errCode,
|
|
111
|
+
success: (_payload$success = payload.success) !== null && _payload$success !== void 0 ? _payload$success : errCode === 0
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
89
115
|
/**
|
|
90
116
|
* 创建 MQTT 请求的 Promise,监听响应消息
|
|
91
117
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
118
|
+
* 匹配语义(taskId + reqType 双匹配):
|
|
119
|
+
* - 事件按 reqType 订阅;
|
|
120
|
+
* - 下发方传了 taskId、且响应携带 taskId 时,必须等值才视为本次请求的响应,
|
|
121
|
+
* 其余响应忽略并继续等待——同 reqType 并发请求不会互相串包;
|
|
122
|
+
* - 响应缺 taskId(部分固件不回 echo)时放行,避免退化成必然超时。
|
|
95
123
|
*
|
|
96
124
|
* @param reqType 请求类型,用于匹配响应消息
|
|
97
|
-
* @param taskId
|
|
125
|
+
* @param taskId 下发时生成的任务ID,用于并发区分;传 undefined 退化为 reqType 首响应匹配
|
|
98
126
|
* @param resolve Promise 的 resolve 回调
|
|
99
127
|
* @param reject Promise 的 reject 回调
|
|
100
128
|
* @param timeout 超时时间(毫秒),默认 10 秒
|
|
@@ -102,12 +130,12 @@ export function dispatchUnhandledRejection(reason) {
|
|
|
102
130
|
* @example
|
|
103
131
|
* ```typescript
|
|
104
132
|
* return new Promise((resolve, reject) => {
|
|
105
|
-
* normalResolve('carpetQry', '', resolve, reject);
|
|
133
|
+
* normalResolve('carpetQry', '1730375500113_1', resolve, reject);
|
|
106
134
|
* });
|
|
107
135
|
* ```
|
|
108
136
|
*/
|
|
109
|
-
export function normalResolve(reqType, resolve, reject) {
|
|
110
|
-
let timeout = arguments.length >
|
|
137
|
+
export function normalResolve(reqType, taskId, resolve, reject) {
|
|
138
|
+
let timeout = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : DEFAULT_TIMEOUT;
|
|
111
139
|
let isResolved = false;
|
|
112
140
|
|
|
113
141
|
// 设置超时定时器
|
|
@@ -123,14 +151,21 @@ export function normalResolve(reqType, resolve, reject) {
|
|
|
123
151
|
}
|
|
124
152
|
}, timeout);
|
|
125
153
|
const handle = message => {
|
|
126
|
-
|
|
127
|
-
const response = typeof message.data === 'object' ? message.data : message;
|
|
154
|
+
const response = unwrapMqttResponse(message);
|
|
128
155
|
|
|
129
156
|
// 如果已经处理过,直接返回(避免重复处理)
|
|
130
157
|
if (isResolved) {
|
|
131
158
|
return;
|
|
132
159
|
}
|
|
133
160
|
|
|
161
|
+
// taskId 匹配:响应携带 taskId 且与本次下发不一致时忽略,继续等待本请求的响应
|
|
162
|
+
if (taskId && response.taskId != null && `${response.taskId}` !== `${taskId}`) {
|
|
163
|
+
logger('info', {
|
|
164
|
+
msg: `${reqType} normalResolve: taskId mismatch, keep waiting (${response.taskId} != ${taskId})`
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
134
169
|
// 标记为已处理(防止其他监听器处理)
|
|
135
170
|
isResolved = true;
|
|
136
171
|
clearTimeout(timer);
|
|
@@ -140,9 +175,9 @@ export function normalResolve(reqType, resolve, reject) {
|
|
|
140
175
|
|
|
141
176
|
// 移除监听器(使用函数引用确保移除正确的监听器)
|
|
142
177
|
emitter.off(reqType, handle);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
178
|
+
const {
|
|
179
|
+
errCode
|
|
180
|
+
} = normalizeResultFields(response);
|
|
146
181
|
if (errCode === 0) {
|
|
147
182
|
resolve(response);
|
|
148
183
|
} else {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SetSpotPolygonsData } from './type/protocols/spotPolygonsProtocol';
|
|
2
|
+
export declare const createSpotPolygonsPayload: ({ polygons, spotNames, version, }: SetSpotPolygonsData) => {
|
|
3
|
+
version?: string | undefined;
|
|
4
|
+
num: number;
|
|
5
|
+
polygons: string[];
|
|
6
|
+
spotNames: string[];
|
|
7
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
|
|
2
|
+
export const createSpotPolygonsPayload = _ref => {
|
|
3
|
+
let {
|
|
4
|
+
polygons,
|
|
5
|
+
spotNames,
|
|
6
|
+
version
|
|
7
|
+
} = _ref;
|
|
8
|
+
if (polygons.length !== spotNames.length) {
|
|
9
|
+
throw new Error('polygons and spotNames must have the same length');
|
|
10
|
+
}
|
|
11
|
+
return _objectSpread({
|
|
12
|
+
num: polygons.length,
|
|
13
|
+
polygons,
|
|
14
|
+
spotNames
|
|
15
|
+
}, version && {
|
|
16
|
+
version
|
|
17
|
+
});
|
|
18
|
+
};
|
|
@@ -38,6 +38,7 @@ export interface SetCarpetData {
|
|
|
38
38
|
}
|
|
39
39
|
export interface AddCarpetResponse extends BaseResponse {
|
|
40
40
|
ids: StringOrNumber[];
|
|
41
|
+
mapId?: number;
|
|
41
42
|
}
|
|
42
43
|
export interface CarpetQueryResponse extends BaseResponse {
|
|
43
44
|
num: number;
|
|
@@ -49,6 +50,7 @@ export interface CarpetQueryResponse extends BaseResponse {
|
|
|
49
50
|
autoBoost?: number[];
|
|
50
51
|
fineCleaning?: number[];
|
|
51
52
|
sideBrushRotating?: number[];
|
|
53
|
+
mapId?: number;
|
|
52
54
|
}
|
|
53
55
|
export type TQueryCarpet = (data?: QueryCarpetData) => Promise<CarpetQueryResponse>;
|
|
54
56
|
export type TAddCarpet = (data: AddCarpetData) => Promise<AddCarpetResponse>;
|
|
@@ -20,9 +20,12 @@ export interface RequestCruiseData {
|
|
|
20
20
|
version?: string;
|
|
21
21
|
}
|
|
22
22
|
export interface CruiseResponse extends BaseResponse {
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
/** Wiki 协议中的当前巡航指令/状态字段 */
|
|
24
|
+
cmd?: CruiseCmd;
|
|
25
|
+
/** 兼容旧设备的巡航状态 */
|
|
26
|
+
status?: CruiseStatus;
|
|
27
|
+
/** 当前地图 ID,设备可不返回 */
|
|
28
|
+
mapId?: number;
|
|
26
29
|
/** 巡航类型;idle 时可不携带 */
|
|
27
30
|
cruiseType?: CruiseType;
|
|
28
31
|
/** spot 定点巡航时的净化点坐标列表,格式 "x, y" */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from './base';
|
|
1
2
|
export * from './scheduleProtocol';
|
|
2
3
|
export * from './carpetProtocol';
|
|
3
4
|
export * from './zoneCleanProtocol';
|
|
@@ -20,4 +21,6 @@ export * from './devInfoProtocol';
|
|
|
20
21
|
export * from './roomPropertyProtocol';
|
|
21
22
|
export * from './purifierScheduleProtocol';
|
|
22
23
|
export * from './cruiseProtocol';
|
|
24
|
+
export * from './spotPolygonsProtocol';
|
|
23
25
|
export * from './mcsRoomInfoProtocol';
|
|
26
|
+
export * from './thresholdProtocol';
|