SDM(Smart Device Model)
. For more information about SDM
, you can view the SDM documentation.For details, see Panel MiniApp - Setting Up Environment.
Product Name: Robot Sweeper
First, you need to create a laser robot sweeper product, define the functional points of the product, and then implement these functional points in the panel one by one.
Go to IoT Platform, click Product Menu on the left, Product Development, Create Product, choose Standard Category -> Small Home Appliances -> Robot Sweeper -> Laser Robot Sweeper
:
Select functional points, here choose according to your need, unselected functions will not impact video preview.
🎉 In this step, we created a Robot Sweeper product named Robot
.
This part is operated on the MiniApp Developer
platform. Register and log in to MiniApp Developer Platform to proceed.
For detailed operation path, refer to Panel MiniApp - Creating a Panel MiniApp.
This part is operated in Tuya MiniApp Tools
, open IDE and create a panel miniapp project based on SweepRobotTemplate.
For detailed operation path, refer to Panel MiniApp - Initializing Project.
In the above steps, we have initialized a Panel MiniApp
development template. Now let's introduce the project directory.
├── src
│ ├── api
│ │ └── ossApi.ts // API related to oss map download
│ ├── components
│ │ ├── DecisionBar // Confirmation box component
│ │ ├── EmptyMap // Empty map component
│ │ ├── HistoryMapView // History map component
│ │ ├── HomeTopBar // Home TopBar component
│ │ ├── IpcRecordTimer // IPC recording timer component
│ │ ├── IpcRecordTip // IPC recording tip component
│ │ ├── Loading // Map loading component
│ │ ├── MapView // Real-time map component
│ │ ├── RoomNamePopLayout // Room naming popup component
│ │ ├── RoomPreferencePopLayout // Room cleaning preference popup component
│ │ ├── Selector // Selector component
│ │ ├── TopBar // General TopBar component
│ ├── constant
│ │ ├── dpCodes.ts // dpCode constants
│ │ ├── index.ts // All constant configurations
│ ├── devices // Device model
│ ├── hooks // hooks
│ ├── i18n // Internationalization
│ ├── iconfont // IconFont files
│ ├── pages
│ │ ├── addTiming // Add timing page
│ │ ├── cleanRecordDetail // Clean record detail page
│ │ ├── cleanRecords // Clean records list page
│ │ ├── doNotDisturb // Do not disturb mode page
│ │ ├── home // Home page
│ │ ├── ipc // Video monitoring page
│ │ ├── manual // Manual control page
│ │ ├── mapEdit // Map editing page
│ │ ├── multiMap // Multi-map management page
│ │ ├── roomEdit // Room editing page
│ │ ├── setting // Settings page
│ │ ├── timing // Timing list page
│ │ ├── voicePack // Voice pack page
│ ├── redux // redux
│ ├── res // Image resources & svg
│ ├── styles // Global styles
│ ├── utils
│ │ ├── openApi // Map operation methods
│ │ ├── index.ts // Common utility methods for business
│ │ ├── ipc.ts // ipc related utility methods
│ │ ├── robotStatus.ts // Methods for judging robot status
│ ├── app.config.ts
│ ├── app.less
│ ├── app.tsx
│ ├── composeLayout.tsx // Handling listening to subdevice addition, unbinding, DP change, etc.
│ ├── global.config.ts
│ ├── mixins.less // less mixins
│ ├── routes.config.ts // Configuring routes
│ ├── variables.less // less variables
├── typings // Global type definitions
├── webview // html needed for map WebView
To allow developers to focus more on UI processing and not worry too much about other process logic handling, we have modularized the robot sweeper, separating the underlying implementation from business calls. Currently, the main dependencies of the sweeper panel are the following packages:
@ray-js/robot-map-component
: Called directly by the business layer, provides full-screen map and dynamic map components, and exposes common map operation methods.@ray-js/robot-data-stream
: Called directly by the business layer, encapsulates P2P transmission methods between the panel and the device. Developers can ignore the complex process of P2P communication and only need to focus on the business logic itself.@ray-js/robot-protocol
: Called directly by the business layer, provides a complete protocol parsing standard capability, encapsulates the parsing and encoding process of the more complex raw type dp points in the robot sweeper protocol.@ray-js/webview-invoke
: Underlying dependency, provides communication capability between the MiniApp and the underlying SDK, generally not requiring modification.@ray-js/robot-middleware
: Underlying dependency, provides business intermediate processing between the logic layer and the WebView.@ray-js/hybrid-robot-map
: Underlying dependency, basic SDK for the sweeper, providing rendering capability for underlying layers.For general sweeper needs, the focus is mainly on application business logic and UI display, without worrying about the implementation within the dependency packages. Upgrades for these packages will maintain backward compatibility, and individual upgrades can be performed in the project.
Displaying the real-time map is a core function of a sweeper application. How do we render our first map on the home page?
@ray-js/robot-map-component
provides two types of map components:
At the business level, the template encapsulates the Real-Time Map Component , which you can choose to use the Full Screen Component or Dynamic Component based on your business scenario.
For the home page, which usually displays the sweeper map in real-time, it is recommended to use the Full Screen Component. You can introduce it as follows:
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",
}}
/>
);
At this moment, you might have a question, how is the map and path data injected into this component?
The template encapsulates a tool library @ray-js/robot-data-stream
, which has built-in processes such as P2P initialization/connection establishment/data stream download/destruction. By simply calling the useP2PDataStream
hook, you can fetch the real-time map and path data from the sweeper device (provided your sweeper device supports P2P data transmission).
import { useP2PDataStream } from "@ray-js/robot-data-stream";
import { useMapData, usePathData } from "@/hooks";
// useMapData is a hook that processes map data and injects it into the <MapView /> component
const { onMapData } = useMapData();
// usePathData is a hook that processes path data and injects it into the <MapView /> component
const { onPathData } = usePathData();
useP2PDataStream(getDevInfo().devId, onMapData, onPathData);
If all goes well, you might have successfully viewed the map on your phone. However, if you rely on scanning code with your phone for debugging during development, it is very inconvenient. Is there any way to also display the real-time map on the IDE?
The IDE doesn't support P2P connections, but you can do this using the Robot Vacuum Debugger plugin. For detailed usage, refer to the documentation.
Cleaning is the most basic function of a robot vacuum cleaner. The template has built-in 4 cleaning modes: entire house / selected area / spot / zone.
Among them, the selected area / spot / zone modes will involve changes in the map status:
/**
* Modify map status
* @param status Map status
*/
const setMapStatusChange = (status: number) => {
const { mapId } = store.getState().mapState;
const edit = status !== ENativeMapStatusEnum.normal;
// When switching to selected area cleaning, freeze the map and prevent map updates
if (status === ENativeMapStatusEnum.mapClick) {
freezeMapUpdate(mapId, true);
}
// Restore the map when switching back
if (status === ENativeMapStatusEnum.normal) {
freezeMapUpdate(mapId, false);
}
setLaserMapStateAndEdit(mapId, { state: status, edit: edit || false });
};
/**
* Switch cleaning mode
* @param modeValue
* @param mapStatus
*/
const handleSwitchMode = (modeValue: string, mapStatus: number) => {
const { mapId } = store.getState().mapState;
setMapStatus(mapStatus);
// Determine if it is switching to selected area cleaning
if (mapStatus === ENativeMapStatusEnum.mapClick) {
setLaserMapSplitType(mapId, EMapSplitStateEnum.click);
}
if (mapStatus === ENativeMapStatusEnum.normal) {
setLaserMapSplitType(mapId, EMapSplitStateEnum.normal);
}
// For point cleaning, if there's no need to click the map, generate a movable area immediately
if (mapStatus === 1) {
addPosPoints();
}
};
You may have noticed that both the spot and zone cleaning modes support drawing shapes on the map. The template encapsulates several hooks for drawing shapes on the map:
You can use usePoseClean to add a movable point for spot cleaning on the map.
import { usePoseClean } from "@/hooks";
const { drawPoseCleanArea } = usePoseClean();
/**
* Add a movable point for spot cleaning
*/
const addPosPoints = async () => {
const { mapId } = store.getState().mapState;
drawPoseCleanArea(mapId);
};
After setting the cleaning mode, you need to send a command to start cleaning. In addition to necessary boolean DPs like cleaning switch-switch_go, you should pay more attention to raw type DPs like command transmission-command_trans that are used to inform the device of the graphics information for selected area / spot / zone cleaning.
If you've read the Tuya Laser Vacuum Cleaner Protocol document, you might be troubled by how to construct byte-type command data.
No need to worry, the template encapsulates the @ray-js/robot-protocol
vacuum cleaner protocol library to construct/parse command data, covering over 90% of commonly used command functionalities.
If you want to send a command for selected area cleaning and your product uses the "*0x14(0x15) Room Clean" protocol, you can use encodeRoomClean0x14
to construct the command data:
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 contains room info thrown by MapView's onClickSplitArea after room selection
roomHexIds: selectRoomData,
mapVersion: version,
});
actions[commandTransCode].set(data);
};
Similarly, if you find the vacuum cleaner to be in selected area cleaning mode when entering the panel, you need to parse the selected area cleaning command reported by the device to know which rooms are being cleaned. You can use requestRoomClean0x15
and decodeRoomClean0x15
together.
// If entering the panel and it is in selected area cleaning mode, you need to send a query command requesting the device to report specific command data.
actions[commandTransCode].set(
requestRoomClean0x15({ version: PROTOCOL_VERSION })
);
// When receiving reported data from the device, parse the selected area cleaning command
const roomClean = decodeRoomClean0x15({
// command contains the reported DP value from the device
command,
mapVersion,
});
if (roomClean) {
const { roomHexIds } = roomClean;
// After updating selectRoomData, the rooms currently being cleaned in selected area mode will be highlighted on the map
dispatch(updateMapData({ selectRoomData: roomHexIds }));
}
The multi-map management page will display all historical maps stored by the device.
Although the data protocol and rendering methods are consistent, the data sources for historical and real-time maps are fundamentally different. Real-time map data comes from P2P, while historical map data originates from cloud file downloads.
For obtaining multi-map data, please refer to Multi-Map API.
The template has encapsulated multiMapsSlice
in Redux for querying multi-map data, you can refer to the related code.
The template also encapsulates a HistoryMapView
component specifically for displaying historical maps.
import HistoryMapView from "@/components/HistoryMapView";
return (
<HistoryMapView
isFullScreen={false}
// bucket and file data come from the getMultipleMapFiles API request
history={{
bucket,
file,
}}
/>
);
For using and deleting maps, use encodeUseMap0x2e
and encodeDeleteMap0x2c
provided by @ray-js/robot-protocol
:
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,
})
);
};
The map editing page introduces the map in a way similar to the homepage, but with some changes in props.
const uiInterFace = useMemo(() => {
return { isFoldable: true, isShowPileRing: true };
}, []);
<MapView
isFullScreen
// Temporary data for room settings
preCustomConfig={previewCustom}
// Forced room tag folding and showing charger warning ring (warning not to set restricted areas and virtual walls too close)
uiInterFace={uiInterFace}
onMapId={onMapId}
onLaserMapPoints={onLaserMapPoints}
onClickSplitArea={onClickSplitArea}
onMapLoadEnd={onMapLoadEnd}
// Path not displayed
pathVisible={false}
// No selection state
selectRoomData={[]}
/>;
Restricted areas are divided into no-sweep and no-mop zones. You can use the useForbiddenNoGo
and useForbiddenNoMop
hooks to create the corresponding areas.
After creation, if you want to save and issue the restricted area, you can use the encodeVirtualArea0x38
method to package the area information into a dp command and issue it.
import { useForbiddenNoGo, useForbiddenNoMop } from "@/hooks";
import { getMapPointsInfo } from "@/utils/openApi";
import { encodeVirtualArea0x38 } from "@ray-js/robot-protocol";
// Create a no-sweep restricted area
const { drawOneForbiddenNoGo } = useForbiddenNoGo();
// Create a no-mop restricted area
const { drawOneForbiddenNoMop } = useForbiddenNoMop();
// Save and issue the restricted area
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);
};
The implementation of the virtual wall function is similar to that of restricted areas. You can use useCreateVirtualWall
to create a virtual wall.
After creation, if you want to save and issue the virtual wall, you can use the encodeVirtualWall0x12
method to package the wall information into a dp command and issue it.
import { useCreateVirtualWall } from "@/hooks";
import { getMapPointsInfo } from "@/utils/openApi";
import { encodeVirtualWall0x12 } from "@ray-js/robot-protocol";
// Create a virtual wall
const { drawOneVirtualWall } = useCreateVirtualWall();
// Save and issue the virtual wall
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);
};
Since the data is associated with room information, setting the floor material requires setting the preCustomConfig
of MapView
.
After clicking on the room, a floor material selection popup will appear. Once a material is selected, these statuses will be stored temporarily in previewCustom
.
To save and confirm the room material, you can use the encodeSetRoomFloorMaterial0x52
method to convert the temporary floor material information into a dp command for issuing.
import { encodeSetRoomFloorMaterial0x52 } from "@ray-js/robot-protocol";
const [showFloorMaterialPopup, setShowFloorMaterialPopup] = useState(false);
const [previewCustom, setPreviewCustom] = useState<{
[key: string]: { roomId: number; floorMaterial: number };
}>({});
// Set floor material for a room
const handleFloorMaterialConfirm = (hexId: string) => {
const room = {
roomId: roomIdState.roomId,
floorMaterial: parseInt(hexId, 16),
};
const curRoom = {
[roomIdState.roomIdHex]: {
...room,
},
};
setPreviewCustom({ ...previewCustom, ...curRoom });
setShowFloorMaterialPopup(false);
};
// Save and issue all the floor material information
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
// Temporary data for room settings
preCustomConfig={previewCustom}
uiInterFace={uiInterFace}
onMapId={onMapId}
onLaserMapPoints={onLaserMapPoints}
onClickSplitArea={onClickSplitArea}
onMapLoadEnd={onMapLoadEnd}
pathVisible={false}
selectRoomData={[]}
/>
{/*
Floor material selection popup
*/}
<FloorMaterialPopLayout
show={showFloorMaterialPopup}
onConfirm={handleFloorMaterialConfirm}
/>
</View>
);
The room editing page introduces the map in a way similar to the homepage, but with some changes in props.
<MapView
isFullScreen
// Temporary data for room settings
preCustomConfig={previewCustom}
onMapId={onMapId}
onClickSplitArea={onClickSplitArea}
onSplitLine={onSplitLine}
onMapLoadEnd={onMapLoadEnd}
// Path not displayed
pathVisible={false}
// No selection state
selectRoomData={[]}
// Do not display information such as fixed points, zones, restricted areas, virtual walls, etc. on the map
areaInfoList={[]}
/>
Clicking the room merge function will set the map to the room merge state.
import { setMapStatusMerge } from "@/utils/openApi/mapStatus";
import { changeAllMapAreaColor } from "@/utils/openApi";
/**
* Enter the room merge state
*/
const handleMergeStatus = async () => {
// Set the map to the room merge state
setMapStatusMerge(mapId.current);
// Set all room colors to unselected state
changeAllMapAreaColor(mapId.current, true);
};
After selecting the two rooms to be merged, you can use encodePartitionMerge0x1e
to convert the room information into a dp command and issue it.
import { getLaserMapMergeInfo } from "@/utils/openApi";
import { encodePartitionMerge0x1e } from "@ray-js/robot-protocol";
// Issue room merge command
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);
};
Clicking the room splitting function will set the map to room splitting state.
import { setMapStatusSplit } from "@/utils/openApi/mapStatus";
/**
* Enter the room splitting state
*/
const handleSplitStatus = async () => {
// Set the map to room splitting state
setMapStatusSplit(mapId.current);
};
After selecting a room and setting the required division line, you can use encodePartitionDivision0x1c
to convert the room division information into a dp command and issue it.
import { getLaserMapSplitPoint } from "@/utils/openApi";
import { encodePartitionDivision0x1c } from "@ray-js/robot-protocol";
// Issue room splitting command
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);
};
Clicking the room naming function will set the map to room naming state.
import { setMapStatusRename } from "@/utils/openApi/mapStatus";
/**
* Enter the room naming state
*/
const handleRenameStatus = async () => {
// Set the map to room naming state
setMapStatusRename(mapId.current);
};
After selecting a room and entering a name in the popup, the temporary room naming information will be stored in the previewCustom
state. Then you can use encodeSetRoomName0x24
to convert the room naming information into a dp command and issue it.
import { encodeSetRoomName0x24 } from "@ray-js/robot-protocol";
const [showRenameModal, setShowRenameModal] = useState(false);
const [previewCustom, setPreviewCustom] = useState({});
// Room naming popup confirmation
const handleRenameConfirm = (name: string) => {
const room = previewCustom[roomHexId] || {};
const curRoom = {
[roomHexId]: {
...room,
name,
},
};
const newPreviewCustom = { ...previewCustom, ...curRoom };
setShowRenameModal(false);
setPreviewCustom(newPreviewCustom);
};
// Issue room naming command
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
// Temporary data for room settings
preCustomConfig={previewCustom}
onMapId={onMapId}
onClickSplitArea={onClickSplitArea}
onSplitLine={onSplitLine}
onMapLoadEnd={onMapLoadEnd}
selectRoomData={[]}
areaInfoList={[]}
pathVisible={false}
/>
<RoomNamePopLayout
show={showRenameModal}
onConfirm={handleRenameConfirm}
defaultValue=""
/>
</View>
);
Clicking the room order function will set the map to room order setting state.
import { setMapStatusOrder } from "@/utils/openApi/mapStatus";
/**
* Enter the room ordering state
*/
const handleMergeStatus = async () => {
// Set the map to room ordering state
setMapStatusOrder(mapId.current);
};
After setting the order for all rooms, you can use encodeRoomOrder0x26
to convert the room information into a dp command and issue it.
import { getMapPointsInfo } from "@/utils/openApi";
import { encodeRoomOrder0x26 } from "@ray-js/robot-protocol";
// Issue room order setting command
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);
};
Timing uses dp Timing-device_timer.
Through decodeDeviceTimer0x31
, the timing dp can be parsed into timing list data.
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]);
You can perform delete/on-off operations on timing items. Use encodeDeviceTimer0x30
to convert the new timing list into a dp command for issuing.
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[]>([]);
// Delete a timing item
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);
};
// Toggle a timing item on/off
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);
};
Adding timing also uses encodeDeviceTimer0x30
to compose the command.
// Add a timing item
const addTimer = (newTimer: TimerData) => {
const newList = [newTimer, ...timerList];
const command = encodeDeviceTimer0x30({
list: newList,
version: PROTOCOL_VERSION,
number: newList.length,
});
actions[deviceTimerCode].set(command);
};
Do not disturb mode uses dp Do Not Disturb Time Setting-disturb_time_set.
After setting the on/off, start time, and end time information, click save to issue the do not disturb mode. Use encodeDoNotDisturb0x40
to package the relevant information into a dp command.
import { encodeDoNotDisturb0x40 } from "@ray-js/robot-protocol";
// Add your custom logic here
// Save and issue do not disturb mode information
const handleSave = () => {
const command = encodeDoNotDisturb0x40({
// Do not disturb switch
enable,
// Start time - hour
startHour,
// Start time - minute
startMinute,
// End time - hour
endHour,
// End time - minute
endMinute,
});
actions[commandTransCode].set(command);
};
Similarly, you can use decodeDoNotDisturb0x41
to parse the do not disturb mode dp reported by the device and present it on the page.
import { decodeDoNotDisturb0x41 } from "@ray-js/robot-protocol";
const dpDisturbTimeSet = useProps((props) => props[disturbTimeSetCode]);
// Do not disturb mode dp parsed into structured data
const { enable, startHour, startMinute, endHour, endMinute } =
decodeDoNotDisturb0x41(dpDisturbTimeSet) ?? DEFAULT_VALUE;
// Add your custom logic here
For obtaining cleaning record data, please refer to the Cleaning Record API.
The template has wrapped cleanRecordsSlice
in Redux for deleting, editing, and querying cleaning record data. You can refer to the related code.
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>
);
Details need to display the actual cleaned map and path. Cleaning records are similar to multi-map management as they are historical maps. Therefore, the HistoryMapView
component is also used.
To introduce maps, you can refer to:
import HistoryMapView from "@/components/HistoryMapView";
return (
<HistoryMapView
// Choose to use the full-screen map component here
isFullScreen={true}
// Bucket and file data come from the getMultipleMapFiles API request
history={{
bucket,
file,
}}
pathVisible
/>
);
For obtaining voice pack data, please refer to the Machine Voice 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>
);
Voice pack issuance and reporting use dp Voice Pack Data Issuance-voice_data. You can use encodeVoice0x34
and decodeVoice0x35
provided by @ray-js/robot-protocol
to complete dp data assembly and parsing.
When issuing a command to use a voice pack:
import { useActions } from "@ray-js/panel-sdk";
const actions = useActions();
const handleUse = () => {
actions[voiceDataCode].set(
encodeVoice0x34({
// id, url, and md5 data all come from the Machine Voice API
id: extendData.extendId,
url: officialUrl,
md5: desc,
})
);
};
Parse voice pack reported data to get voice pack information, download progress, and usage status:
import { useProps } from "@ray-js/panel-sdk";
const dpVoiceData = useProps((props) => props[voiceDataCode]);
const { languageId, status, progress } = decodeVoice0x35({
command: dpVoiceData,
});
For voice pack audition, you can refer to the methods in Audio Capability.
Manual control is a general dp issuance function, using dp Direction-direction_control.
The template has packaged a simple manual control component and page. Please refer to the src/pages/manual
page.
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"),
});
// Entering remote control requires issuing manual mode
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;
The template has a built-in Video Surveillance page.
For more details, please refer to the IPC General Template tutorial.