2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.miio.internal.robot;
15 import java.io.ByteArrayOutputStream;
17 import java.io.FileInputStream;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.PrintWriter;
21 import java.io.StringWriter;
22 import java.security.MessageDigest;
23 import java.security.NoSuchAlgorithmException;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.HashMap;
28 import java.util.Map.Entry;
29 import java.util.zip.GZIPInputStream;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.miio.internal.Utils;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
38 * The {@link RRMapFileParser} is used to parse the RR map file format created by Xiaomi / RockRobo vacuum
40 * @author Marcel Verpaalen - Initial contribution
43 public class RRMapFileParser {
44 public static final int CHARGER = 1;
45 public static final int IMAGE = 2;
46 public static final int PATH = 3;
47 public static final int GOTO_PATH = 4;
48 public static final int GOTO_PREDICTED_PATH = 5;
49 public static final int CURRENTLY_CLEANED_ZONES = 6;
50 public static final int GOTO_TARGET = 7;
51 public static final int ROBOT_POSITION = 8;
52 public static final int NO_GO_AREAS = 9;
53 public static final int VIRTUAL_WALLS = 10;
54 public static final int BLOCKS = 11;
55 public static final int MOB_FORBIDDEN_AREA = 12;
56 public static final int OBSTACLES = 13;
57 public static final int IGNORED_OBSTACLES = 14;
58 public static final int OBSTACLES2 = 15;
59 public static final int IGNORED_OBSTACLES2 = 16;
60 public static final int CARPET_MAP = 17;
61 public static final int MOP_PATH = 18;
62 public static final int CARPET_FORBIDDEN_AREA = 19;
63 public static final int SMART_ZONES_PATH_TYPE = 20;
64 public static final int SMART_ZONES = 21;
65 public static final int CUSTOM_CARPET = 22;
66 public static final int CL_FORBIDDEN_ZONES = 23;
67 public static final int FLOOR_MAP = 24;
68 public static final int FURNITURES = 25;
69 public static final int DOCK_TYPE = 26;
70 public static final int ENEMIES = 27;
71 public static final int DOOR_ZONES = 28;
72 public static final int STUCK_POINTS = 29;
73 public static final int CLIFF_ZONES = 30;
74 public static final int SMARTDS = 31;
75 public static final int FLDIREC = 32;
76 public static final int MAP_DATE = 33;
77 public static final int NONCE_DATA = 34;
78 public static final int DIGEST = 1024;
79 public static final int HEADER = 0x7272;
81 public static final String PATH_POINT_LENGTH = "pointLength";
82 public static final String PATH_POINT_SIZE = "pointSize";
83 public static final String PATH_ANGLE = "angle";
85 private byte[] image = new byte[] { 0 };
86 private final int majorVersion;
87 private final int minorVersion;
88 private final int mapIndex;
89 private final int mapSequence;
90 private boolean isValid;
92 private int imgHeight;
94 private int imageSize;
103 private float gotoX = 0;
104 private float gotoY = 0;
105 private Map<Integer, ArrayList<float[]>> paths = new HashMap<>();
106 private Map<Integer, Map<String, Integer>> pathsDetails = new HashMap<>();
107 private Map<Integer, ArrayList<float[]>> areas = new HashMap<>();
108 private ArrayList<float[]> walls = new ArrayList<>();
109 private ArrayList<float[]> zones = new ArrayList<>();
110 private Map<Integer, ArrayList<int[]>> obstacles = new HashMap<>();
111 private byte[] blocks = new byte[0];
112 private int[] carpetMap = {};
113 private int[] mopPath = {};
115 private final Logger logger = LoggerFactory.getLogger(RRMapFileParser.class);
117 public RRMapFileParser(byte[] raw) {
118 boolean printBlockDetails = false;
119 int mapHeaderLength = getUInt16(raw, 0x02);
120 int mapDataLength = getUInt32LE(raw, 0x04);
121 this.majorVersion = getUInt16(raw, 0x08);
122 this.minorVersion = getUInt16(raw, 0x0A);
123 this.mapIndex = getUInt32LE(raw, 0x0C);
124 this.mapSequence = getUInt32LE(raw, 0x10);
126 int blockStartPos = getUInt16(raw, 0x02); // main header length
127 while (blockStartPos < raw.length) {
128 int blockHeaderLength = getUInt16(raw, blockStartPos + 0x02);
129 byte[] header = getBytes(raw, blockStartPos, blockHeaderLength);
130 int blocktype = getUInt16(header, 0x00);
131 int blockDataLength = getUInt32LE(header, 0x04);
132 int blockDataStart = blockStartPos + blockHeaderLength;
133 byte[] data = getBytes(raw, blockDataStart, blockDataLength);
137 this.chargerX = getUInt32LE(raw, blockStartPos + 0x08);
138 this.chargerY = getUInt32LE(raw, blockStartPos + 0x0C);
141 this.imageSize = blockDataLength;// (getUInt32LE(raw, blockStartPos + 0x04));
142 if (blockHeaderLength > 0x1C) {
143 logger.debug("block 2 unknown value @pos 8: {}", getUInt32LE(header, 0x08));
145 this.top = getUInt32LE(header, blockHeaderLength - 16);
146 this.left = getUInt32LE(header, blockHeaderLength - 12);
147 this.imgHeight = (getUInt32LE(header, blockHeaderLength - 8));
148 this.imgWidth = getUInt32LE(header, blockHeaderLength - 4);
152 this.roboX = getUInt32LE(data, 0x00);
153 this.roboY = getUInt32LE(data, 0x04);
154 if (blockDataLength > 8) { // model S6
155 this.roboA = getUInt32LE(data, 0x08);
160 case GOTO_PREDICTED_PATH:
161 ArrayList<float[]> path = new ArrayList<float[]>();
162 Map<String, Integer> detail = new HashMap<String, Integer>();
163 int pairs = getUInt32LE(header, 0x04) / 4;
164 detail.put(PATH_POINT_LENGTH, getUInt32LE(header, 0x08));
165 detail.put(PATH_POINT_SIZE, getUInt32LE(header, 0x0C));
166 detail.put(PATH_ANGLE, getUInt32LE(header, 0x10));
167 for (int pathpair = 0; pathpair < pairs; pathpair++) {
168 float x = (getUInt16(getBytes(raw, blockDataStart + pathpair * 4, 2)));
169 float y = getUInt16(getBytes(raw, blockDataStart + pathpair * 4 + 2, 2));
170 path.add(new float[] { x, y });
172 paths.put(blocktype, path);
173 pathsDetails.put(blocktype, detail);
175 case CURRENTLY_CLEANED_ZONES:
176 int zonePairs = getUInt16(header, 0x08);
177 for (int zonePair = 0; zonePair < zonePairs; zonePair++) {
178 float x0 = (getUInt16(raw, blockDataStart + zonePair * 8));
179 float y0 = getUInt16(raw, blockDataStart + zonePair * 8 + 2);
180 float x1 = (getUInt16(raw, blockDataStart + zonePair * 8 + 4));
181 float y1 = getUInt16(raw, blockDataStart + zonePair * 8 + 6);
182 zones.add(new float[] { x0, y0, x1, y1 });
186 this.gotoX = getUInt16(data, 0x00);
187 this.gotoY = getUInt16(data, 0x02);
190 isValid = Arrays.equals(data, sha1Hash(getBytes(raw, 0, mapHeaderLength + mapDataLength - 20)));
193 int wallPairs = getUInt16(header, 0x08);
194 for (int wallPair = 0; wallPair < wallPairs; wallPair++) {
195 float x0 = (getUInt16(raw, blockDataStart + wallPair * 8));
196 float y0 = getUInt16(raw, blockDataStart + wallPair * 8 + 2);
197 float x1 = (getUInt16(raw, blockDataStart + wallPair * 8 + 4));
198 float y1 = getUInt16(raw, blockDataStart + wallPair * 8 + 6);
199 walls.add(new float[] { x0, y0, x1, y1 });
203 case MOB_FORBIDDEN_AREA:
204 case CARPET_FORBIDDEN_AREA:
205 int areaPairs = getUInt16(header, 0x08);
206 ArrayList<float[]> area = new ArrayList<float[]>();
207 for (int areaPair = 0; areaPair < areaPairs; areaPair++) {
208 float x0 = (getUInt16(raw, blockDataStart + areaPair * 16));
209 float y0 = getUInt16(raw, blockDataStart + areaPair * 16 + 2);
210 float x1 = (getUInt16(raw, blockDataStart + areaPair * 16 + 4));
211 float y1 = getUInt16(raw, blockDataStart + areaPair * 16 + 6);
212 float x2 = (getUInt16(raw, blockDataStart + areaPair * 16 + 8));
213 float y2 = getUInt16(raw, blockDataStart + areaPair * 16 + 10);
214 float x3 = (getUInt16(raw, blockDataStart + areaPair * 16 + 12));
215 float y3 = getUInt16(raw, blockDataStart + areaPair * 16 + 14);
216 area.add(new float[] { x0, y0, x1, y1, x2, y2, x3, y3 });
218 areas.put(Integer.valueOf(blocktype & 0xFF), area);
221 case IGNORED_OBSTACLES:
222 int obstaclePairs = getUInt16(header, 0x08);
223 ArrayList<int[]> obstacle = new ArrayList<>();
224 for (int obstaclePair = 0; obstaclePair < obstaclePairs; obstaclePair++) {
225 int x0 = getUInt16(data, obstaclePair * 5 + 0);
226 int y0 = getUInt16(data, obstaclePair * 5 + 2);
227 int u = data[obstaclePair * 5 + 0] & 0xFF;
228 obstacle.add(new int[] { x0, y0, u });
230 obstacles.put(Integer.valueOf(blocktype & 0xFF), obstacle);
233 int obstacle2Pairs = getUInt16(header, 0x08);
234 if (obstacle2Pairs == 0) {
237 int obstacleDataLenght = blockDataLength / obstacle2Pairs;
238 logger.trace("block 15 records lenght: {}", obstacleDataLenght);
239 ArrayList<int[]> obstacle2 = new ArrayList<>();
240 for (int obstaclePair = 0; obstaclePair < obstacle2Pairs; obstaclePair++) {
241 int x0 = getUInt16(data, obstaclePair * obstacleDataLenght + 0);
242 int y0 = getUInt16(data, obstaclePair * obstacleDataLenght + 2);
243 int u0 = getUInt16(data, obstaclePair * obstacleDataLenght + 4);
244 int u1 = getUInt16(data, obstaclePair * obstacleDataLenght + 6);
245 int u2 = getUInt32LE(data, obstaclePair * obstacleDataLenght + 8);
246 if (obstacleDataLenght == 28) {
247 if ((data[obstaclePair * obstacleDataLenght + 12] & 0xFF) == 0) {
248 logger.trace("obstacle with photo: No text");
250 byte[] txt = getBytes(data, obstaclePair * obstacleDataLenght + 12, 16);
251 logger.trace("obstacle with photo: {}", new String(txt));
253 obstacle2.add(new int[] { x0, y0, u0, u1, u2 });
255 int u3 = getUInt32LE(data, obstaclePair * obstacleDataLenght + 12);
256 obstacle2.add(new int[] { x0, y0, u0, u1, u2, u3 });
257 logger.trace("obstacle without photo.");
260 obstacles.put(Integer.valueOf(blocktype & 0xFF), obstacle2);
262 case IGNORED_OBSTACLES2:
263 int ignoredObstaclePairs = getUInt16(header, 0x08);
264 ArrayList<int[]> ignoredObstacle = new ArrayList<>();
265 for (int obstaclePair = 0; obstaclePair < ignoredObstaclePairs; obstaclePair++) {
266 int x0 = getUInt16(data, obstaclePair * 6 + 0);
267 int y0 = getUInt16(data, obstaclePair * 6 + 2);
268 int u = getUInt16(data, obstaclePair * 6 + 4);
269 ignoredObstacle.add(new int[] { x0, y0, u });
271 obstacles.put(Integer.valueOf(blocktype & 0xFF), ignoredObstacle);
274 carpetMap = new int[blockDataLength];
275 for (int carpetNode = 0; carpetNode < blockDataLength; carpetNode++) {
276 carpetMap[carpetNode] = data[carpetNode] & 0xFF;
280 mopPath = new int[blockDataLength];
281 for (int mopNode = 0; mopNode < blockDataLength; mopNode++) {
282 mopPath[mopNode] = data[mopNode] & 0xFF;
286 int blocksPairs = getUInt16(header, 0x08);
287 blocks = getBytes(data, 0, blocksPairs);
289 case SMART_ZONES_PATH_TYPE:
292 case CL_FORBIDDEN_ZONES:
304 // new blocktypes not yet decoded
307 logger.info("Unknown blocktype {} (pls report to author)", blocktype);
308 printBlockDetails = true;
310 if (logger.isTraceEnabled() || printBlockDetails) {
311 logger.debug("Blocktype: {}", Integer.toString(blocktype));
312 logger.debug("Header len: {} data len: {} ", Integer.toString(blockHeaderLength),
313 Integer.toString(blockDataLength));
314 logger.debug("H: {}", Utils.getSpacedHex(header));
315 if (blockDataLength > 0) {
316 logger.debug("D: {}", (blockDataLength < 60 ? Utils.getSpacedHex(data)
317 : Utils.getSpacedHex(getBytes(data, 0, 60))));
319 printBlockDetails = false;
321 blockStartPos = blockStartPos + blockDataLength + (header[2] & 0xFF);
325 public static byte[] readRRMapFile(File file) throws IOException {
326 return readRRMapFile(new FileInputStream(file));
329 public static byte[] readRRMapFile(InputStream is) throws IOException {
330 ByteArrayOutputStream baos = new ByteArrayOutputStream();
331 try (GZIPInputStream in = new GZIPInputStream(is)) {
333 byte[] buf = new byte[bufsize];
335 readbytes = in.read(buf);
336 while (readbytes != -1) {
337 baos.write(buf, 0, readbytes);
338 readbytes = in.read(buf);
341 return baos.toByteArray();
345 private byte[] getBytes(byte[] raw, int pos, int len) {
346 return java.util.Arrays.copyOfRange(raw, pos, pos + len);
349 private int getUInt32LE(byte[] bytes, int pos) {
350 int value = bytes[0 + pos] & 0xFF;
351 value |= (bytes[1 + pos] << 8) & 0xFFFF;
352 value |= (bytes[2 + pos] << 16) & 0xFFFFFF;
353 value |= (bytes[3 + pos] << 24) & 0xFFFFFFFF;
357 private int getUInt16(byte[] bytes) {
358 return getUInt16(bytes, 0);
361 private int getUInt16(byte[] bytes, int pos) {
362 int value = bytes[0 + pos] & 0xFF;
363 value |= (bytes[1 + pos] << 8) & 0xFFFF;
368 public String toString() {
369 StringWriter sw = new StringWriter();
370 PrintWriter pw = new PrintWriter(sw);
371 pw.printf("RR Map:\tMajor Version: %d Minor version: %d Map Index: %d Map Sequence: %d\r\n", majorVersion,
372 minorVersion, mapIndex, mapSequence);
373 pw.printf("Image:\tsize: %9d\ttop: %9d\tleft: %9d height: %9d width: %9d\r\n", imageSize, top, left, imgHeight,
375 pw.printf("Charger pos:\tX: %.0f\tY: %.0f\r\n", getChargerX(), getChargerY());
376 pw.printf("Robo pos:\tX: %.0f\tY: %.0f\tAngle: %d\r\n", getRoboX(), getRoboY(), getRoboA());
377 pw.printf("Goto:\tX: %.0f\tY: %.0f\r\n", getGotoX(), getGotoY());
378 for (Entry<Integer, ArrayList<float[]>> area : areas.entrySet()) {
379 switch (area.getKey()) {
381 pw.print("Regular No Go zones:\t");
383 case MOB_FORBIDDEN_AREA:
384 pw.print("Mop No Go zones:\t");
386 case CARPET_FORBIDDEN_AREA:
387 pw.print("Carpet No Go zones:\t");
390 pw.print("Unknown type zones:\t");
392 pw.printf("%d\r\n", area.getValue().size());
393 printAreaDetails(area.getValue(), pw);
395 pw.printf("Walls:\t%d\r\n", walls.size());
396 printAreaDetails(walls, pw);
397 pw.printf("Zones:\t%d\r\n", zones.size());
398 printAreaDetails(zones, pw);
399 for (Entry<Integer, ArrayList<int[]>> obstacleType : obstacles.entrySet()) {
400 pw.printf("Obstacles Type (%d):\t%d\r\n", obstacleType.getKey(), obstacleType.getValue().size());
401 printObstacleDetails(obstacleType.getValue(), pw);
403 pw.printf("Blocks:\t%d\r\n", blocks.length);
405 for (Entry<Integer, Map<String, Integer>> pathDetail : pathsDetails.entrySet()) {
406 pw.printf("\r\nPath type:\t%d", pathDetail.getKey());
407 for (String detail : pathDetail.getValue().keySet()) {
408 pw.printf(" %s: %d", detail, pathDetail.getValue().get(detail));
412 pw.printf("Carpet Map:\t%d\r\n", carpetMap.length);
413 pw.printf("Mop Path:\t%d\r\n", mopPath.length);
415 return sw.toString();
418 private void printAreaDetails(@Nullable ArrayList<float[]> areas, PrintWriter pw) {
423 areas.forEach(area -> {
424 pw.print("\tArea coordinates:");
425 for (int i = 0; i < area.length; i++) {
426 pw.printf("\t%.0f", area[i]);
432 private void printObstacleDetails(@Nullable ArrayList<int[]> obstacle, PrintWriter pw) {
433 if (obstacle == null) {
437 obstacle.forEach(area -> {
438 pw.print("\tObstacle coordinates:");
439 for (int i = 0; i < area.length; i++) {
440 pw.printf("\t%d", area[i]);
447 * Compute SHA-1 hash value for the byte array
449 * @param inBytes ByteArray to be hashed
452 public static byte[] sha1Hash(byte[] inBytes) {
454 MessageDigest md = MessageDigest.getInstance("SHA-1");
455 return md.digest(inBytes);
456 } catch (NoSuchAlgorithmException e) {
457 return new byte[] { 0x00 };
461 public int getMajorVersion() {
465 public int getMinorVersion() {
469 public int getMapIndex() {
473 public int getMapSequence() {
477 public boolean isValid() {
481 public byte[] getImage() {
485 public int getImageSize() {
489 public int getImgHeight() {
493 public int getImgWidth() {
497 public int getTop() {
501 public int getLeft() {
505 public ArrayList<float[]> getZones() {
509 public float getRoboX() {
513 public float getRoboY() {
517 public float getChargerX() {
521 public float getChargerY() {
525 public float getGotoX() {
529 public float getGotoY() {
533 public int getRoboA() {
537 public Map<Integer, ArrayList<float[]>> getPaths() {
541 public Map<Integer, Map<String, Integer>> getPathsDetails() {
545 public ArrayList<float[]> getWalls() {
549 public Map<Integer, ArrayList<float[]>> getAreas() {
553 public Map<Integer, ArrayList<int[]>> getObstacles() {
557 public byte[] getBlocks() {
561 public final int[] getCarpetMap() {
565 public final int[] getMopPath() {