Prerequisites

Development Environment

For details, see Panel MiniApp - Setting Up Environment.

Product Name: Robot Sweeper

Requirement Prototype

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.

Create Panel MiniApp on Developer Platform

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.

Create Project in IDE Based on Template

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:

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?

Map Component

@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",
    }}
  />
);

Data Integration

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);

IDE Map Debugging

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 Modes

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);
};

Cleaning Commands

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 Area

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);
};

Virtual Wall

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);
};

Floor Material

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={[]}
/>

Room Merging

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);
};

Room Splitting

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);
};

Room Naming

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>
);

Room Order

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.

Timing List

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);
};

Add Timing

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

Cleaning Record List

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>
);

Cleaning Record Details

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.