SDM(Smart Device Model)
开发,关于 SDM
相关可以 查看 SDM 文档详见 面板小程序 - 搭建环境
产品名称:扫地机器人
首先需要创建一个激光型扫地机器人产品,定义产品有哪些功能点,然后面板中再根据这些功能点一一实现。
进入IoT 平台,点击左侧产品菜单,产品开发,创建产品,选择标准类目 -> 小家电 -> 扫地机器人 -> 激光型扫地机
:
选择功能点,这里根据自己需求选择即可,这些功能未选择不影响视频预览。
🎉 在这一步,我们创建了一个名为 Robot
的扫地机器人产品。
这部分我们在 小程序开发者
平台上进行操作,注册登录 小程序开发者平台 进行操作。
详细的操作路径可参考 面板小程序 - 创建面板小程序
这部分我们在 Tuya MiniApp Tools
上进行操作,打开 IDE 创建一个基于扫地机小程序模板的面板小程序项目。
详细的操作路径可参考 面板小程序 - 初始化项目工程
上面的步骤我们已经初始化好了一个面板小程序
的开发模板,下面我们介绍下工程目录。
├── src
│ ├── api
│ │ └── ossApi.ts // oss地图下载相关的API
│ ├── components
│ │ ├── DecisionBar // 确认框组件
│ │ ├── EmptyMap // 空地图组件
│ │ ├── HistoryMapView // 历史地图组件
│ │ ├── HomeTopBar // 首页TopBar组件
│ │ ├── IpcRecordTimer // IPC录制时间组件
│ │ ├── IpcRecordTip // IPC录制提示组件
│ │ ├── Loading // 地图Loading组件
│ │ ├── MapView // 实时地图组件
│ │ ├── RoomNamePopLayout // 房间命名弹窗组件
│ │ ├── RoomPreferencePopLayout // 房间清洁偏好弹窗组件
│ │ ├── Selector // 选择器组件
│ │ ├── TopBar // 通用TopBar组件
│ ├── constant
│ │ ├── dpCodes.ts // dpCode常量
│ │ ├── index.ts // 存放所有的常量配置
│ ├── devices // 设备模型
│ ├── hooks // hooks
│ ├── i18n // 多语言
│ ├── iconfont // IconFont文件
│ ├── pages
│ │ ├── addTiming // 添加定时页面
│ │ ├── cleanRecordDetail // 清扫记录详情页面
│ │ ├── cleanRecords // 清扫记录列表页面
│ │ ├── doNotDisturb // 勿扰模式页面
│ │ ├── home // 首页
│ │ ├── ipc // 视频监控页面
│ │ ├── manual // 手动控制页面
│ │ ├── mapEdit // 地图编辑页面
│ │ ├── multiMap // 多地图管理页面
│ │ ├── roomEdit // 房间编辑页面
│ │ ├── setting // 设置页
│ │ ├── timing // 定时列表页面
│ │ ├── voicePack // 语音包页面
│ ├── redux // redux
│ ├── res // 图片资源 & svg相关
│ ├── styles // 全局样式
│ ├── utils
│ │ ├── openApi // 地图操作方法
│ │ ├── index.ts // 业务常用工具方法
│ │ ├── ipc.ts // ipc相关工具方法
│ │ ├── robotStatus.ts // 扫地机状态判断方法
│ ├── app.config.ts
│ ├── app.less
│ ├── app.tsx
│ ├── composeLayout.tsx // 处理监听子设备添加,解绑,DP变化等
│ ├── global.config.ts
│ ├── mixins.less // less mixins
│ ├── routes.config.ts // 配置路由
│ ├── variables.less // less variables
├── typings // 全局类型定义
├── webview // 地图WebView需要的html
为了能够让开发者更关注在 UI 层面的处理,而不需要过多关心一些其他的流程逻辑处理,我们将扫地机进行模块拆分,将底层实现与业务调用独立。目前扫地机面板主要依赖的包有以下几个
@ray-js/robot-map-component
业务层直接调用, 提供了全屏地图和动态地图组件,并且暴露了地图操作的常用方法。@ray-js/robot-data-stream
业务层直接调用,封装了面板与设备的 P2P 传输方法,开发者可以忽略 P2P 通信过程中的复杂过程,只需要关注业务本身逻辑@ray-js/robot-protocol
业务层直接调用,提供完整协议解析标准能力,将扫地机协议中比较复杂的 raw 类型 dp 点的解析、编码过程进行了封装@ray-js/webview-invoke
底层依赖,提供了小程序与底层 SDK 的通信能力,一般情况下不需要修改@ray-js/robot-middleware
底层依赖,提供逻辑层和 WebView 的业务中间处理@ray-js/hybrid-robot-map
底层依赖,扫地机基础 SDK,提供底层图层的渲染能力对于普遍的扫地机需求,基本上只关注应用业务逻辑和 UI 展示,不需要关心内部依赖包中的实现,依赖包的升级会做到向下兼容,可以在项目中针对依赖包进行单独升级。
实时地图的显示是一个扫地机应用的核心功能,如何在首页渲染出我们的第一张地图呢?
@ray-js/robot-map-component
提供了两种类型的地图组件:
业务层面模板封装了实时地图组件,你可按以业务场景需要选择使用全屏组件还是动态组件
首页通常为实时展示的扫地机地图,更推荐使用全屏组件,可以按以下方式引入:
import MapView from "@/components/MapView";
// Add your custom logic here
return (
<MapView
isFullScreen
onMapId={onMapId}
onClickSplitArea={onClickSplitArea}
onDecodeMapData={onDecodeMapData}
onClickRoomProperties={onClickRoomProperties}
onClickMaterial={onClickMaterial}
onClickRoom={onClickRoom}
style={{
height: "75vh",
}}
/>
);
此刻你可能产生了一个疑问,地图和路径数据是怎么注入到这个组件的呢?
模板封装了@ray-js/robot-data-stream
工具库,内置了 P2P 初始化/建立连接/下载数据流/销毁等一系列流程,你只需要调用useP2PDataStream
这个 hooks,就能实时获得从扫地机设备传输过来的地图、路径数据(前提是你的扫地机设备已经开发支持 P2P 数据传输哦):
import { useP2PDataStream } from "@ray-js/robot-data-stream";
import { useMapData, usePathData } from "@/hooks";
// useMapData是将地图数据进行一些处理后注入到<MapView />的hooks
const { onMapData } = useMapData();
// usePathData是将路径数据进行一些处理后注入到<MapView />的hooks
const { onPathData } = usePathData();
useP2PDataStream(getDevInfo().devId, onMapData, onPathData);
顺利的话,也许你已经成功在手机上看到了地图,但如果开发阶段就要依赖于手机扫码的方式进行调试,那实在太不方便了。 有没有什么办法能在 IDE 上也展示实时地图呢?
IDE 并不具备 P2P 连接的环境,但你可以借助扫地机调试助手插件做到这一点,具体使用方式可查阅文档。
清扫是扫地机器人最基本的功能。模板内置了 4 种清扫模式:全屋 / 选区 / 定点 / 划区。
其中,选区 / 定点 / 划区模式会涉及到地图状态的变更:
/**
* 修改地图状态
* @param status 地图状态
*/
const setMapStatusChange = (status: number) => {
const { mapId } = store.getState().mapState;
const edit = status !== ENativeMapStatusEnum.normal;
// 当切换为选区清扫时,冻结地图,阻止地图更新
if (status === ENativeMapStatusEnum.mapClick) {
freezeMapUpdate(mapId, true);
}
// 切换回来时恢复地图
if (status === ENativeMapStatusEnum.normal) {
freezeMapUpdate(mapId, false);
}
setLaserMapStateAndEdit(mapId, { state: status, edit: edit || false });
};
/**
* 切换清扫模式
* @param modeValue
* @param mapStatus
*/
const handleSwitchMode = (modeValue: string, mapStatus: number) => {
const { mapId } = store.getState().mapState;
setMapStatus(mapStatus);
// 是否是切换到选区清扫
if (mapStatus === ENativeMapStatusEnum.mapClick) {
setLaserMapSplitType(mapId, EMapSplitStateEnum.click);
}
if (mapStatus === ENativeMapStatusEnum.normal) {
setLaserMapSplitType(mapId, EMapSplitStateEnum.normal);
}
// 指哪扫哪如果不需要点击地图,立即生成一个可移动区时
if (mapStatus === 1) {
addPosPoints();
}
};
你应该注意到了,定点和划区清扫模式,都支持在地图上绘制图形,模板封装了几个 hooks 用于在地图上绘制图形的功能:
你可以用usePoseClean在地图上添加一个定点清扫的移动点。
import { usePoseClean } from "@/hooks";
const { drawPoseCleanArea } = usePoseClean();
/**
* 增加一个定点清扫的移动点
*/
const addPosPoints = async () => {
const { mapId } = store.getState().mapState;
drawPoseCleanArea(mapId);
};
当你设定好清扫模式后,你需要下发指令让机器开始清扫,除了像清扫开关-switch_go这样必要的布尔型 dp 外,你更应该注意到指令传输-command_trans这样的 raw 类型 dp 用于告诉设备选区 / 定点 / 划区模式下的图元信息。
如果你看过涂鸦激光扫地机协议文档,肯定会为如何组装出一个字节型的指令数据而头疼。
不必担心,模板封装了@ray-js/robot-protocol
扫地机协议库用于组装/解析指令数据,目前已经涵盖 90%以上的常用指令功能。
如果要进行选区清扫的指令下发,假设你们的产品约定使用「*0x14(0x15) 选区清扫 Room Clean」这个选区清扫协议。
那么你们可以使用 encodeRoomClean0x14
来进行指令数据的组装:
import { encodeRoomClean0x14 } from "@ray-js/robot-protocol";
import { useActions } from "@ray-js/panel-sdk";
const actions = useActions();
const roomCleanFunc = () => {
const { version, selectRoomData } = store.getState().mapState;
const data = encodeRoomClean0x14({
cleanTimes: 1,
// selectRoomData是在选择房间后由MapView的onClickSplitArea抛出的房间信息
roomHexIds: selectRoomData,
mapVersion: version,
});
actions[commandTransCode].set(data);
};
同理,如果进入面板后发现处于选区清扫状态,你需要解析设备上报的选区清扫指令,来得知设备当前正在清扫哪几个房间,你可以结合使用 requestRoomClean0x15
和 decodeRoomClean0x15
// 如果进入面板后处于选区清扫的状态,那么需要下发查询型的指令,请求设备上报具体的指令数据。
actions[commandTransCode].set(
requestRoomClean0x15({ version: PROTOCOL_VERSION })
);
// 当收到设备上报的数据后,进行选区清扫解析
const roomClean = decodeRoomClean0x15({
// command是设备上报的指令的dp值
command,
mapVersion,
});
if (roomClean) {
const { roomHexIds } = roomClean;
// 更新selectRoomData后,地图上正在选区清扫的房间就会高亮
dispatch(updateMapData({ selectRoomData: roomHexIds }));
}
多地图管理页面会展示设备已经存放的所有历史地图。
尽管数据协议和渲染方式一致,历史地图和实时地图的数据来源是截然不同的。实时地图的数据来源于 P2P,而历史地图的数据来源于云端文件下载。
多地图数据的获取请参考多地图 API
模板已经在 Redux 里封装了multiMapsSlice
用于多地图数据的查询,可以参考相关代码。
模板封装了HistoryMapView
组件专用于历史地图的展示。
import HistoryMapView from "@/components/HistoryMapView";
return (
<HistoryMapView
isFullScreen={false}
// bucket和file数据来源于getMultipleMapFiles接口请求
history={{
bucket,
file,
}}
/>
);
使用地图与删除地图功能使用@ray-js/robot-protocol
提供的encodeUseMap0x2e
和encodeDeleteMap0x2c
即可:
import { encodeDeleteMap0x2c, encodeUseMap0x2e } from "@ray-js/robot-protocol";
const actions = useActions();
const handleDelete = () => {
actions[commandTransCode].set(encodeDeleteMap0x2c({ id }));
};
const handleUseMap = () => {
actions[commandTransCode].set(
encodeUseMap0x2e({
mapId,
url: file,
})
);
};
地图编辑页面引入地图的方式和首页地图类似,但部分 prop 有变化。
const uiInterFace = useMemo(() => {
return { isFoldable: true, isShowPileRing: true };
}, []);
<MapView
isFullScreen
// 房间设置临时数据
preCustomConfig={previewCustom}
// 强制折叠房间属性标签 显示充电桩预警圈(警示禁区和虚拟墙不能设置过近)
uiInterFace={uiInterFace}
onMapId={onMapId}
onLaserMapPoints={onLaserMapPoints}
onClickSplitArea={onClickSplitArea}
onMapLoadEnd={onMapLoadEnd}
// 不显示路径
pathVisible={false}
// 无选区状态
selectRoomData={[]}
/>;
禁区分为扫地禁区和拖地禁区,可以使用useForbiddenNoGo
和useForbiddenNoMop
这两个 hooks 来创建对应的禁区。
创建完成后,如果要保存下发禁区,可以通过encodeVirtualArea0x38
方法将禁区信息组装成 dp 指令并下发。
import { useForbiddenNoGo, useForbiddenNoMop } from "@/hooks";
import { getMapPointsInfo } from "@/utils/openApi";
import { encodeVirtualArea0x38 } from "@ray-js/robot-protocol";
// 创建一个禁扫禁区
const { drawOneForbiddenNoGo } = useForbiddenNoGo();
// 创建一个禁拖禁区
const { drawOneForbiddenNoMop } = useForbiddenNoMop();
// 保存下发禁区
const handleSave = () => {
const { origin } = store.getState().mapState;
const { data } = await getMapPointsInfo(mapId.current);
const command = encodeVirtualArea0x38({
version: PROTOCOL_VERSION,
protocolVersion: 1,
virtualAreas: data.map((item) => {
return {
points: item.points,
mode: item.extend.forbidType === "sweep" ? 1 : 2,
name: item.content.text,
};
}),
origin,
});
actions[commandTransCode].set(command);
};
虚拟墙功能的实现禁区类似,可以使用useCreateVirtualWall
创建虚拟墙。
创建完成后,如果要保存下发虚拟墙,可以通过encodeVirtualWall0x12
方法将虚拟墙信息组装成 dp 指令并下发。
import { useCreateVirtualWall } from "@/hooks";
import { getMapPointsInfo } from "@/utils/openApi";
import { encodeVirtualWall0x12 } from "@ray-js/robot-protocol";
// 创建一个虚拟墙
const { drawOneVirtualWall } = useCreateVirtualWall();
// 保存下发虚拟墙
const handleSave = () => {
const { origin } = store.getState().mapState;
const { data } = await getMapPointsInfo(mapId.current);
const command = encodeVirtualWall0x12({
version: PROTOCOL_VERSION,
origin,
walls: data.map((item) => item.points),
});
actions[commandTransCode].set(command);
};
由于是与房间信息关联的数据,设置地板材质需要设定MapView
的preCustomConfig
。
在点击房间后将弹出地板材质选择弹窗,选择材质后会将这些状态保存在临时的previewCustom
里。
保存确认房间材质可以通过encodeSetRoomFloorMaterial0x52
方法将临时的地板材质信息转换为 dp 指令去下发。
import { encodeSetRoomFloorMaterial0x52 } from "@ray-js/robot-protocol";
const [showFloorMaterialPopup, setShowFloorMaterialPopup] = useState(false);
const [previewCustom, setPreviewCustom] = useState<{
[key: string]: { roomId: number; floorMaterial: number };
}>({});
// 对某个房间设置地板材质
const handleFloorMaterialConfirm = (hexId: string) => {
const room = {
roomId: roomIdState.roomId,
floorMaterial: parseInt(hexId, 16),
};
const curRoom = {
[roomIdState.roomIdHex]: {
...room,
},
};
setPreviewCustom({ ...previewCustom, ...curRoom });
setShowFloorMaterialPopup(false);
};
// 保存下发所有的地板材质信息
const handleSave = () => {
const onConfirm = () => {
const rooms = Object.keys(previewCustom).map((roomIdHex: string) => {
const room = previewCustom[roomIdHex];
return {
roomId: room.roomId,
material: room.floorMaterial,
};
});
const command = encodeSetRoomFloorMaterial0x52({
version: PROTOCOL_VERSION,
rooms,
});
actions[commandTransCode].set(command);
};
};
return (
<View>
<MapView
isFullScreen
// 房间设置临时数据
preCustomConfig={previewCustom}
uiInterFace={uiInterFace}
onMapId={onMapId}
onLaserMapPoints={onLaserMapPoints}
onClickSplitArea={onClickSplitArea}
onMapLoadEnd={onMapLoadEnd}
pathVisible={false}
selectRoomData={[]}
/>
{/*
地板材质选择弹窗
*/}
<FloorMaterialPopLayout
show={showFloorMaterialPopup}
onConfirm={handleFloorMaterialConfirm}
/>
</View>
);
房间编辑页面引入地图的方式和首页类似,但部分 prop 有变化。
<MapView
isFullScreen
// 房间设置临时数据
preCustomConfig={previewCustom}
onMapId={onMapId}
onClickSplitArea={onClickSplitArea}
onSplitLine={onSplitLine}
onMapLoadEnd={onMapLoadEnd}
// 不显示路径
pathVisible={false}
// 无选区状态
selectRoomData={[]}
// 不显示地图上的定点/划区/禁区/虚拟墙等信息
areaInfoList={[]}
/>
点击房间合并功能,将地图设置为房间合并状态
import { setMapStatusMerge } from "@/utils/openApi/mapStatus";
import { changeAllMapAreaColor } from "@/utils/openApi";
/**
* 进入房间合并状态
*/
const handleMergeStatus = async () => {
// 将地图设置为房间合并状态
setMapStatusMerge(mapId.current);
// 设置所有房间颜色为未选中状态
changeAllMapAreaColor(mapId.current, true);
};
在选中需要合并的两个房间后,可以通过encodePartitionMerge0x1e
将房间信息转换为 dp 指令并做下发。
import { getLaserMapMergeInfo } from "@/utils/openApi";
import { encodePartitionMerge0x1e } from "@ray-js/robot-protocol";
// 下发房间合并指令
const handleSave = () => {
const { version } = store.getState().mapState;
const res = await getLaserMapMergeInfo(mapId.current);
const { type, data } = res;
const roomIds = data.map((room) => parseRoomId(room.pixel, version));
const command = encodePartitionMerge0x1e({
roomIds,
version: PROTOCOL_VERSION,
});
actions[commandTransCode].set(command);
};
点击房间分割功能,将地图设置为房间分割状态
import { setMapStatusSplit } from "@/utils/openApi/mapStatus";
/**
* 进入房间分割状态
*/
const handleSplitStatus = async () => {
// 将地图设置为房间分割状态
setMapStatusSplit(mapId.current);
};
在选中房间并设置需要的分割线后,可以通过encodePartitionDivision0x1c
将房间分割信息转换为 dp 指令并做下发。
import { getLaserMapSplitPoint } from "@/utils/openApi";
import { encodePartitionDivision0x1c } from "@ray-js/robot-protocol";
// 下发房间分割指令
const handleSave = () => {
const { version } = store.getState().mapState;
const {
type,
data: [{ points, pixel }],
} = await getLaserMapSplitPoint(mapId.current);
const roomId = parseRoomId(pixel, version);
const command = encodePartitionDivision0x1c({
roomId,
points,
origin,
version: PROTOCOL_VERSION,
});
actions[commandTransCode].set(command);
};
点击房间命名功能,将地图设置为房间命名状态
import { setMapStatusRename } from "@/utils/openApi/mapStatus";
/**
* 进入房间命名状态
*/
const handleRenameStatus = async () => {
// 将地图设置为房间命名状态
setMapStatusRename(mapId.current);
};
在选中房间并在弹窗里填入名称后,临时的房间命名信息会存放在previewCustom
状态里,然后可以通过encodeSetRoomName0x24
将房间命名信息转换为 dp 指令并做下发。
import { encodeSetRoomName0x24 } from "@ray-js/robot-protocol";
const [showRenameModal, setShowRenameModal] = useState(false);
const [previewCustom, setPreviewCustom] = useState({});
// 房间命名弹窗确认
const handleRenameConfirm = (name: string) => {
const room = previewCustom[roomHexId] || {};
const curRoom = {
[roomHexId]: {
...room,
name,
},
};
const newPreviewCustom = { ...previewCustom, ...curRoom };
setShowRenameModal(false);
setPreviewCustom(newPreviewCustom);
};
// 下发房间命名指令
const handleSave = () => {
const { version } = store.getState().mapState;
const keys = Object.keys(previewCustom);
const command = encodeSetRoomName0x24({
mapVersion: version,
version: PROTOCOL_VERSION,
rooms: keys.map((key) => {
return {
roomHexId: key,
name: previewCustom[key].name,
};
}),
});
actions[commandTransCode].set(command);
};
return (
<View>
<MapView
isFullScreen
// 房间设置临时数据
preCustomConfig={previewCustom}
onMapId={onMapId}
onClickSplitArea={onClickSplitArea}
onSplitLine={onSplitLine}
onMapLoadEnd={onMapLoadEnd}
selectRoomData={[]}
areaInfoList={[]}
pathVisible={false}
/>
<RoomNamePopLayout
show={showRenameModal}
onConfirm={handleRenameConfirm}
defaultValue=""
/>
</View>
);
点击房间排序功能,将地图设置为房间顺序设置状态
import { setMapStatusOrder } from "@/utils/openApi/mapStatus";
/**
* 进入房间顺序设置状态
*/
const handleMergeStatus = async () => {
// 将地图设置为房间顺序设置状态
setMapStatusOrder(mapId.current);
};
在对所有房间设置顺序后,可以通过encodeRoomOrder0x26
将房间信息转换为 dp 指令并做下发。
import { getMapPointsInfo } from "@/utils/openApi";
import { encodeRoomOrder0x26 } from "@ray-js/robot-protocol";
// 下发房间顺序设置指令
const handleSave = () => {
const { version } = store.getState().mapState;
const { data } = await getMapPointsInfo(mapId.current);
const roomIdHexs = data
.sort((a: { order: number }, b: { order: number }) => a.order - b.order)
.map((item) => item.pixel);
const command = encodeRoomOrder0x26({
version: PROTOCOL_VERSION,
roomIdHexs,
mapVersion: version,
});
actions[commandTransCode].set(command);
};
定时使用 dp 定时-device_timer
通过decodeDeviceTimer0x31
可以将定时 dp 解析为定时列表数据
import { decodeDeviceTimer0x31 } from "@ray-js/robot-protocol";
type TimerData = {
effectiveness: number;
week: number[];
time: {
hour: number;
minute: number;
};
roomIds: number[];
cleanMode: number;
fanLevel: number;
waterLevel: number;
sweepCount: number;
roomNum: number;
};
const [timerList, setTimerList] = useState<TimerData[]>([]);
const dpDeviceTimer = useProps((props) => props[deviceTimerCode]);
useEffect(() => {
if (dpDeviceTimer) {
const { list } = decodeDeviceTimer0x31({
command: dpDeviceTimer,
version: PROTOCOL_VERSION,
}) ?? { list: [] };
setTimerList(list);
}
}, [dpDeviceTimer]);
对定时项可以进行删除 / 开关操作,可以使用encodeDeviceTimer0x30
把新的定时列表转换为 dp 指令进行下发。
import { encodeDeviceTimer0x30 } from "@ray-js/robot-protocol";
import produce from "immer";
type TimerData = {
effectiveness: number;
week: number[];
time: {
hour: number;
minute: number;
};
roomIds: number[];
cleanMode: number;
fanLevel: number;
waterLevel: number;
sweepCount: number;
roomNum: number;
};
const [timerList, setTimerList] = useState<TimerData[]>([]);
// 删除一条定时
const deleteTimer = (index: number) => {
const newList = [...timerList];
newList.splice(index, 1);
const command = encodeDeviceTimer0x30({
list: newList,
version: PROTOCOL_VERSION,
number: newList.length,
});
actions[deviceTimerCode].set(command);
};
// 打开/关闭一条定时
const toggleTimer = (index: number, enable: boolean) => {
const newList = produce(timerList, (draft) => {
draft[index].effectiveness = enable;
});
const command = encodeDeviceTimer0x30({
list: newList,
version: PROTOCOL_VERSION,
number: newList.length,
});
actions[deviceTimerCode].set(command);
};
添加定时同样使用encodeDeviceTimer0x30
来组装指令。
// 添加一条定时
const addTimer = (newTimer: TimerData) => {
const newList = [newTimer, ...timerList];
const command = encodeDeviceTimer0x30({
list: newList,
version: PROTOCOL_VERSION,
number: newList.length,
});
actions[deviceTimerCode].set(command);
};
勿扰模式使用 dp 勿扰时间设置-disturb_time_set
在设置完开关 / 开始时间 / 结束时间信息后,点击保存即可下发勿扰模式,通过encodeDoNotDisturb0x40
可以将相关信息组装为 dp 指令。
import { encodeDoNotDisturb0x40 } from "@ray-js/robot-protocol";
// Add your custom logic here
// 保存下发勿扰模式信息
const handleSave = () => {
const command = encodeDoNotDisturb0x40({
// 勿扰开关
enable,
// 开始时间-小时
startHour,
// 开始时间-分钟
startMinute,
// 结束时间-小时
endHour,
// 结束时间-分钟
endMinute,
});
actions[commandTransCode].set(command);
};
同样,可以使用decodeDoNotDisturb0x41
将设备上报的勿扰模式 dp 解析并呈现在页面上。
import { decodeDoNotDisturb0x41 } from "@ray-js/robot-protocol";
const dpDisturbTimeSet = useProps((props) => props[disturbTimeSetCode]);
// 勿扰模式dp解析为结构化数据
const { enable, startHour, startMinute, endHour, endMinute } =
decodeDoNotDisturb0x41(dpDisturbTimeSet) ?? DEFAULT_VALUE;
// Add your custom logic here
清扫记录数据的获取请参考清扫记录 API
模板已经在 Redux 里封装了cleanRecordsSlice
用于清扫记录数据的删改查,可以参考相关代码。
import {
deleteCleanRecord,
fetchCleanRecords,
selectCleanRecords,
} from "@/redux/modules/cleanRecordsSlice";
const records = useSelector(selectCleanRecords);
const handleDelete = (id: number) => {
dispatch(deleteCleanRecord(id));
};
useEffect(() => {
(dispatch as AppDispatch)(fetchCleanRecords());
}, []);
return (
<View className={styles.container}>
{records.map((record) => (
<Item key={record.id} data={record} onDeleted={handleDelete} />
))}
</View>
);
详情需要展示实际清扫的地图与路径,清扫记录与多地图管理类似,也是历史地图,所以同样使用HistoryMapView
组件。
地图的引入可以参考:
import HistoryMapView from "@/components/HistoryMapView";
return (
<HistoryMapView
// 这里选择使用全屏地图组件
isFullScreen={true}
// bucket和file数据来源于getMultipleMapFiles接口请求
history={{
bucket,
file,
}}
pathVisible
/>
);
语音包数据的获取请参考机器语音 API
import { getVoiceList } from "@ray-js/ray";
type Voice = {
auditionUrl: string;
desc?: string;
extendData: {
extendId: number;
version: string;
};
id: number;
imgUrl: string;
name: string;
officialUrl: string;
productId: string;
region: string[];
};
const [voices, setVoices] = useState<Voice[]>([]);
useEffect(() => {
const fetchVoices = async () => {
const res = await getVoiceList({
devId: getDevInfo().devId,
offset: 0,
limit: 100,
});
setVoices(res.datas);
};
fetchVoices();
}, []);
return (
<View className={styles.container}>
{voices.map((voice) => (
<Item key={voice.id} data={voice} deviceVoice={deviceVoice} />
))}
</View>
);
语音包的下发上报使用 dp语音包数据下发-voice_data,可使用@ray-js/robot-protocol
提供的encodeVoice0x34
和decodeVoice0x35
完成 dp 数据的组装和解析
当下发使用某个语音包:
import { useActions } from "@ray-js/panel-sdk";
const actions = useActions();
const handleUse = () => {
actions[voiceDataCode].set(
encodeVoice0x34({
// id url md5数据均来源于机器语音 API
id: extendData.extendId,
url: officialUrl,
md5: desc,
})
);
};
解析语音包上报的数据,获取语音包信息、下载进度、使用状态:
import { useProps } from "@ray-js/panel-sdk";
const dpVoiceData = useProps((props) => props[voiceDataCode]);
const { languageId, status, progress } = decodeVoice0x35({
command: dpVoiceData,
});
语音包的试听可以参考使用音频能力里的相关方法
手动控制是一般的 dp 下发功能,使用 dp方向-direction_control。
模板已经封装了简易的手动控制组件及页面,请参考src/pages/manual
页面。
import React, { FC, useEffect } from "react";
import {
View,
navigateBack,
onNavigationBarBack,
setNavigationBarBack,
} from "@ray-js/ray";
import Strings from "@/i18n";
import { Dialog, DialogInstance } from "@ray-js/smart-ui";
import { useActions } from "@ray-js/panel-sdk";
import { directionControlCode, modeCode } from "@/constant/dpCodes";
import ManualPanel from "@/components/ManualPanel";
import styles from "./index.module.less";
const Manual: FC = () => {
const actions = useActions();
useEffect(() => {
ty.setNavigationBarTitle({
title: Strings.getLang("dsc_manual"),
});
// 进入远程控制需要下发手动模式
actions[modeCode].set("manual");
setNavigationBarBack({ type: "custom" });
onNavigationBarBack(async () => {
try {
await DialogInstance.confirm({
context: this,
title: Strings.getLang("dsc_tips"),
icon: true,
message: Strings.getLang("dsc_exit_manual_tips"),
confirmButtonText: Strings.getLang("dsc_confirm"),
cancelButtonText: Strings.getLang("dsc_cancel"),
});
actions[directionControlCode].set("exit");
setNavigationBarBack({ type: "system" });
setTimeout(() => {
navigateBack();
}, 0);
} catch (err) {
// do nothing
}
});
return () => {
setNavigationBarBack({ type: "system" });
};
}, []);
return (
<View className={styles.container}>
<ManualPanel />
<Dialog id="smart-dialog" />
</View>
);
};
export default Manual;
模板已内置一个视频监控页面。
具体可参考IPC 通用模板教程