]> git.basschouten.com Git - openhab-addons.git/blob
095d06bb0d581beb4c8db19e857b44d43a70b2cd
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.miio.internal.robot;
14
15 import java.io.ByteArrayOutputStream;
16 import java.io.File;
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;
27 import java.util.Map;
28 import java.util.zip.GZIPInputStream;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.miio.internal.Utils;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 /**
37  * The {@link RRMapFileParser} is used to parse the RR map file format created by Xiaomi / RockRobo vacuum
38  *
39  * @author Marcel Verpaalen - Initial contribution
40  */
41 @NonNullByDefault
42 public class RRMapFileParser {
43     public static final int CHARGER = 1;
44     public static final int IMAGE = 2;
45     public static final int PATH = 3;
46     public static final int GOTO_PATH = 4;
47     public static final int GOTO_PREDICTED_PATH = 5;
48     public static final int CURRENTLY_CLEANED_ZONES = 6;
49     public static final int GOTO_TARGET = 7;
50     public static final int ROBOT_POSITION = 8;
51     public static final int NO_GO_AREAS = 9;
52     public static final int VIRTUAL_WALLS = 10;
53     public static final int BLOCKS = 11;
54     public static final int MFBZS_AREA = 12;
55     public static final int OBSTACLES = 13;
56     public static final int IGNORED_OBSTACLES = 14;
57     public static final int OBSTACLES2 = 15;
58     public static final int IGNORED_OBSTACLES2 = 16;
59     public static final int CARPET_MAP = 17;
60
61     public static final int DIGEST = 1024;
62     public static final int HEADER = 0x7272;
63
64     public static final String PATH_POINT_LENGTH = "pointLength";
65     public static final String PATH_POINT_SIZE = "pointSize";
66     public static final String PATH_ANGLE = "angle";
67
68     private byte[] image = new byte[] { 0 };
69     private final int majorVersion;
70     private final int minorVersion;
71     private final int mapIndex;
72     private final int mapSequence;
73     private boolean isValid;
74
75     private int imgHeight;
76     private int imgWidth;
77     private int imageSize;
78     private int top;
79     private int left;
80
81     private int chargerX;
82     private int chargerY;
83     private int roboX;
84     private int roboY;
85     private int roboA;
86     private float gotoX = 0;
87     private float gotoY = 0;
88     private Map<Integer, ArrayList<float[]>> paths = new HashMap<>();
89     private Map<Integer, Map<String, Integer>> pathsDetails = new HashMap<>();
90     private Map<Integer, ArrayList<float[]>> areas = new HashMap<>();
91     private ArrayList<float[]> walls = new ArrayList<>();
92     private ArrayList<float[]> zones = new ArrayList<>();
93     private Map<Integer, ArrayList<int[]>> obstacles = new HashMap<>();
94     private byte[] blocks = new byte[0];
95     private int[] carpetMap = {};
96
97     private final Logger logger = LoggerFactory.getLogger(RRMapFileParser.class);
98
99     public RRMapFileParser(byte[] raw) {
100         boolean printBlockDetails = false;
101         int mapHeaderLength = getUInt16(raw, 0x02);
102         int mapDataLength = getUInt32LE(raw, 0x04);
103         this.majorVersion = getUInt16(raw, 0x08);
104         this.minorVersion = getUInt16(raw, 0x0A);
105         this.mapIndex = getUInt32LE(raw, 0x0C);
106         this.mapSequence = getUInt32LE(raw, 0x10);
107
108         int blockStartPos = getUInt16(raw, 0x02); // main header length
109         while (blockStartPos < raw.length) {
110             int blockHeaderLength = getUInt16(raw, blockStartPos + 0x02);
111             byte[] header = getBytes(raw, blockStartPos, blockHeaderLength);
112             int blocktype = getUInt16(header, 0x00);
113             int blockDataLength = getUInt32LE(header, 0x04);
114             int blockDataStart = blockStartPos + blockHeaderLength;
115             byte[] data = getBytes(raw, blockDataStart, blockDataLength);
116
117             switch (blocktype) {
118                 case CHARGER:
119                     this.chargerX = getUInt32LE(raw, blockStartPos + 0x08);
120                     this.chargerY = getUInt32LE(raw, blockStartPos + 0x0C);
121                     break;
122                 case IMAGE:
123                     this.imageSize = blockDataLength;// (getUInt32LE(raw, blockStartPos + 0x04));
124                     if (blockHeaderLength > 0x1C) {
125                         logger.debug("block 2 unknown value @pos 8: {}", getUInt32LE(header, 0x08));
126                     }
127                     this.top = getUInt32LE(header, blockHeaderLength - 16);
128                     this.left = getUInt32LE(header, blockHeaderLength - 12);
129                     this.imgHeight = (getUInt32LE(header, blockHeaderLength - 8));
130                     this.imgWidth = getUInt32LE(header, blockHeaderLength - 4);
131                     this.image = data;
132                     break;
133                 case ROBOT_POSITION:
134                     this.roboX = getUInt32LE(data, 0x00);
135                     this.roboY = getUInt32LE(data, 0x04);
136                     if (blockDataLength > 8) { // model S6
137                         this.roboA = getUInt32LE(data, 0x08);
138                     }
139                     break;
140                 case PATH:
141                 case GOTO_PATH:
142                 case GOTO_PREDICTED_PATH:
143                     ArrayList<float[]> path = new ArrayList<float[]>();
144                     Map<String, Integer> detail = new HashMap<String, Integer>();
145                     int pairs = getUInt32LE(header, 0x04) / 4;
146                     detail.put(PATH_POINT_LENGTH, getUInt32LE(header, 0x08));
147                     detail.put(PATH_POINT_SIZE, getUInt32LE(header, 0x0C));
148                     detail.put(PATH_ANGLE, getUInt32LE(header, 0x10));
149                     for (int pathpair = 0; pathpair < pairs; pathpair++) {
150                         float x = (getUInt16(getBytes(raw, blockDataStart + pathpair * 4, 2)));
151                         float y = getUInt16(getBytes(raw, blockDataStart + pathpair * 4 + 2, 2));
152                         path.add(new float[] { x, y });
153                     }
154                     paths.put(blocktype, path);
155                     pathsDetails.put(blocktype, detail);
156                     break;
157                 case CURRENTLY_CLEANED_ZONES:
158                     int zonePairs = getUInt16(header, 0x08);
159                     for (int zonePair = 0; zonePair < zonePairs; zonePair++) {
160                         float x0 = (getUInt16(raw, blockDataStart + zonePair * 8));
161                         float y0 = getUInt16(raw, blockDataStart + zonePair * 8 + 2);
162                         float x1 = (getUInt16(raw, blockDataStart + zonePair * 8 + 4));
163                         float y1 = getUInt16(raw, blockDataStart + zonePair * 8 + 6);
164                         zones.add(new float[] { x0, y0, x1, y1 });
165                     }
166                     break;
167                 case GOTO_TARGET:
168                     this.gotoX = getUInt16(data, 0x00);
169                     this.gotoY = getUInt16(data, 0x02);
170                     break;
171                 case DIGEST:
172                     isValid = Arrays.equals(data, sha1Hash(getBytes(raw, 0, mapHeaderLength + mapDataLength - 20)));
173                     break;
174                 case VIRTUAL_WALLS:
175                     int wallPairs = getUInt16(header, 0x08);
176                     for (int wallPair = 0; wallPair < wallPairs; wallPair++) {
177                         float x0 = (getUInt16(raw, blockDataStart + wallPair * 8));
178                         float y0 = getUInt16(raw, blockDataStart + wallPair * 8 + 2);
179                         float x1 = (getUInt16(raw, blockDataStart + wallPair * 8 + 4));
180                         float y1 = getUInt16(raw, blockDataStart + wallPair * 8 + 6);
181                         walls.add(new float[] { x0, y0, x1, y1 });
182                     }
183                     break;
184                 case NO_GO_AREAS:
185                 case MFBZS_AREA:
186                     int areaPairs = getUInt16(header, 0x08);
187                     ArrayList<float[]> area = new ArrayList<float[]>();
188                     for (int areaPair = 0; areaPair < areaPairs; areaPair++) {
189                         float x0 = (getUInt16(raw, blockDataStart + areaPair * 16));
190                         float y0 = getUInt16(raw, blockDataStart + areaPair * 16 + 2);
191                         float x1 = (getUInt16(raw, blockDataStart + areaPair * 16 + 4));
192                         float y1 = getUInt16(raw, blockDataStart + areaPair * 16 + 6);
193                         float x2 = (getUInt16(raw, blockDataStart + areaPair * 16 + 8));
194                         float y2 = getUInt16(raw, blockDataStart + areaPair * 16 + 10);
195                         float x3 = (getUInt16(raw, blockDataStart + areaPair * 16 + 12));
196                         float y3 = getUInt16(raw, blockDataStart + areaPair * 16 + 14);
197                         area.add(new float[] { x0, y0, x1, y1, x2, y2, x3, y3 });
198                     }
199                     areas.put(Integer.valueOf(blocktype & 0xFF), area);
200                     break;
201                 case OBSTACLES:
202                 case IGNORED_OBSTACLES:
203                     int obstaclePairs = getUInt16(header, 0x08);
204                     ArrayList<int[]> obstacle = new ArrayList<>();
205                     for (int obstaclePair = 0; obstaclePair < obstaclePairs; obstaclePair++) {
206                         int x0 = getUInt16(data, obstaclePair * 5 + 0);
207                         int y0 = getUInt16(data, obstaclePair * 5 + 2);
208                         int u = data[obstaclePair * 5 + 0] & 0xFF;
209                         obstacle.add(new int[] { x0, y0, u });
210                     }
211                     obstacles.put(Integer.valueOf(blocktype & 0xFF), obstacle);
212                     break;
213                 case OBSTACLES2:
214                     int obstacle2Pairs = getUInt16(header, 0x08);
215                     if (obstacle2Pairs == 0) {
216                         break;
217                     }
218                     int obstacleDataLenght = blockDataLength / obstacle2Pairs;
219                     logger.trace("block 15 records lenght: {}", obstacleDataLenght);
220                     ArrayList<int[]> obstacle2 = new ArrayList<>();
221                     for (int obstaclePair = 0; obstaclePair < obstacle2Pairs; obstaclePair++) {
222                         int x0 = getUInt16(data, obstaclePair * obstacleDataLenght + 0);
223                         int y0 = getUInt16(data, obstaclePair * obstacleDataLenght + 2);
224                         int u0 = getUInt16(data, obstaclePair * obstacleDataLenght + 4);
225                         int u1 = getUInt16(data, obstaclePair * obstacleDataLenght + 6);
226                         int u2 = getUInt32LE(data, obstaclePair * obstacleDataLenght + 8);
227                         if (obstacleDataLenght == 28) {
228                             if ((data[obstaclePair * obstacleDataLenght + 12] & 0xFF) == 0) {
229                                 logger.trace("obstacle with photo: No text");
230                             } else {
231                                 byte[] txt = getBytes(data, obstaclePair * obstacleDataLenght + 12, 16);
232                                 logger.trace("obstacle with photo: {}", new String(txt));
233                             }
234                             obstacle2.add(new int[] { x0, y0, u0, u1, u2 });
235                         } else {
236                             int u3 = getUInt32LE(data, obstaclePair * obstacleDataLenght + 12);
237                             obstacle2.add(new int[] { x0, y0, u0, u1, u2, u3 });
238                             logger.trace("obstacle without photo.");
239                         }
240                     }
241                     obstacles.put(Integer.valueOf(blocktype & 0xFF), obstacle2);
242                     break;
243                 case IGNORED_OBSTACLES2:
244                     int ignoredObstaclePairs = getUInt16(header, 0x08);
245                     ArrayList<int[]> ignoredObstacle = new ArrayList<>();
246                     for (int obstaclePair = 0; obstaclePair < ignoredObstaclePairs; obstaclePair++) {
247                         int x0 = getUInt16(data, obstaclePair * 6 + 0);
248                         int y0 = getUInt16(data, obstaclePair * 6 + 2);
249                         int u = getUInt16(data, obstaclePair * 6 + 4);
250                         ignoredObstacle.add(new int[] { x0, y0, u });
251                     }
252                     obstacles.put(Integer.valueOf(blocktype & 0xFF), ignoredObstacle);
253                     break;
254                 case CARPET_MAP:
255                     carpetMap = new int[blockDataLength];
256                     for (int carpetNode = 0; carpetNode < blockDataLength; carpetNode++) {
257                         carpetMap[carpetNode] = data[carpetNode] & 0xFF;
258                     }
259                     break;
260                 case BLOCKS:
261                     int blocksPairs = getUInt16(header, 0x08);
262                     blocks = getBytes(data, 0, blocksPairs);
263                     break;
264                 default:
265                     logger.info("Unknown blocktype (pls report to author)");
266                     printBlockDetails = true;
267             }
268             if (logger.isTraceEnabled() || printBlockDetails) {
269                 logger.debug("Blocktype: {}", Integer.toString(blocktype));
270                 logger.debug("Header len: {}   data len: {} ", Integer.toString(blockHeaderLength),
271                         Integer.toString(blockDataLength));
272                 logger.debug("H: {}", Utils.getSpacedHex(header));
273                 if (blockDataLength > 0) {
274                     logger.debug("D: {}", (blockDataLength < 60 ? Utils.getSpacedHex(data)
275                             : Utils.getSpacedHex(getBytes(data, 0, 60))));
276                 }
277                 printBlockDetails = false;
278             }
279             blockStartPos = blockStartPos + blockDataLength + (header[2] & 0xFF);
280         }
281     }
282
283     public static byte[] readRRMapFile(File file) throws IOException {
284         return readRRMapFile(new FileInputStream(file));
285     }
286
287     public static byte[] readRRMapFile(InputStream is) throws IOException {
288         ByteArrayOutputStream baos = new ByteArrayOutputStream();
289         try (GZIPInputStream in = new GZIPInputStream(is)) {
290             int bufsize = 1024;
291             byte[] buf = new byte[bufsize];
292             int readbytes = 0;
293             readbytes = in.read(buf);
294             while (readbytes != -1) {
295                 baos.write(buf, 0, readbytes);
296                 readbytes = in.read(buf);
297             }
298             baos.flush();
299             return baos.toByteArray();
300         }
301     }
302
303     private byte[] getBytes(byte[] raw, int pos, int len) {
304         return java.util.Arrays.copyOfRange(raw, pos, pos + len);
305     }
306
307     private int getUInt32LE(byte[] bytes, int pos) {
308         int value = bytes[0 + pos] & 0xFF;
309         value |= (bytes[1 + pos] << 8) & 0xFFFF;
310         value |= (bytes[2 + pos] << 16) & 0xFFFFFF;
311         value |= (bytes[3 + pos] << 24) & 0xFFFFFFFF;
312         return value;
313     }
314
315     private int getUInt16(byte[] bytes) {
316         return getUInt16(bytes, 0);
317     }
318
319     private int getUInt16(byte[] bytes, int pos) {
320         int value = bytes[0 + pos] & 0xFF;
321         value |= (bytes[1 + pos] << 8) & 0xFFFF;
322         return value;
323     }
324
325     @Override
326     public String toString() {
327         StringWriter sw = new StringWriter();
328         PrintWriter pw = new PrintWriter(sw);
329         pw.printf("RR Map:\tMajor Version: %d Minor version: %d Map Index: %d Map Sequence: %d\r\n", majorVersion,
330                 minorVersion, mapIndex, mapSequence);
331         pw.printf("Image:\tsize: %9d\ttop: %9d\tleft: %9d height: %9d width: %9d\r\n", imageSize, top, left, imgHeight,
332                 imgWidth);
333         pw.printf("Charger pos:\tX: %.0f\tY: %.0f\r\n", getChargerX(), getChargerY());
334         pw.printf("Robo pos:\tX: %.0f\tY: %.0f\tAngle: %d\r\n", getRoboX(), getRoboY(), getRoboA());
335         pw.printf("Goto:\tX: %.0f\tY: %.0f\r\n", getGotoX(), getGotoY());
336         for (Integer area : areas.keySet()) {
337             pw.print(area == NO_GO_AREAS ? "No Go zones:\t" : "MFBZS zones:\t");
338             pw.printf("%d\r\n", areas.get(area).size());
339             printAreaDetails(areas.get(area), pw);
340         }
341         pw.printf("Walls:\t%d\r\n", walls.size());
342         printAreaDetails(walls, pw);
343         pw.printf("Zones:\t%d\r\n", zones.size());
344         printAreaDetails(zones, pw);
345         for (Integer obstacleType : obstacles.keySet()) {
346             pw.printf("Obstacles Type (%d):\t%d\r\n", obstacleType, obstacles.get(obstacleType).size());
347             printObstacleDetails(obstacles.get(obstacleType), pw);
348         }
349         pw.printf("Blocks:\t%d\r\n", blocks.length);
350         pw.print("Paths:");
351         for (Integer p : pathsDetails.keySet()) {
352             pw.printf("\r\nPath type:\t%d", p);
353             for (String detail : pathsDetails.get(p).keySet()) {
354                 pw.printf("   %s: %d", detail, pathsDetails.get(p).get(detail));
355             }
356         }
357         pw.println();
358         pw.close();
359         return sw.toString();
360     }
361
362     private void printAreaDetails(@Nullable ArrayList<float[]> areas, PrintWriter pw) {
363         if (areas == null) {
364             pw.println("null");
365             return;
366         }
367         areas.forEach(area -> {
368             pw.print("\tArea coordinates:");
369             for (int i = 0; i < area.length; i++) {
370                 pw.printf("\t%.0f", area[i]);
371             }
372             pw.println();
373         });
374     }
375
376     private void printObstacleDetails(@Nullable ArrayList<int[]> obstacle, PrintWriter pw) {
377         if (obstacle == null) {
378             pw.println("null");
379             return;
380         }
381         obstacle.forEach(area -> {
382             pw.print("\tObstacle coordinates:");
383             for (int i = 0; i < area.length; i++) {
384                 pw.printf("\t%d", area[i]);
385             }
386             pw.println();
387         });
388     }
389
390     /**
391      * Compute SHA-1 hash value for the byte array
392      *
393      * @param inBytes ByteArray to be hashed
394      * @return hash value
395      */
396     public static byte[] sha1Hash(byte[] inBytes) {
397         try {
398             MessageDigest md = MessageDigest.getInstance("SHA-1");
399             return md.digest(inBytes);
400         } catch (NoSuchAlgorithmException e) {
401             return new byte[] { 0x00 };
402         }
403     }
404
405     public int getMajorVersion() {
406         return majorVersion;
407     }
408
409     public int getMinorVersion() {
410         return minorVersion;
411     }
412
413     public int getMapIndex() {
414         return mapIndex;
415     }
416
417     public int getMapSequence() {
418         return mapSequence;
419     }
420
421     public boolean isValid() {
422         return isValid;
423     }
424
425     public byte[] getImage() {
426         return image;
427     }
428
429     public int getImageSize() {
430         return imageSize;
431     }
432
433     public int getImgHeight() {
434         return imgHeight;
435     }
436
437     public int getImgWidth() {
438         return imgWidth;
439     }
440
441     public int getTop() {
442         return top;
443     }
444
445     public int getLeft() {
446         return left;
447     }
448
449     public ArrayList<float[]> getZones() {
450         return zones;
451     }
452
453     public float getRoboX() {
454         return roboX;
455     }
456
457     public float getRoboY() {
458         return roboY;
459     }
460
461     public float getChargerX() {
462         return chargerX;
463     }
464
465     public float getChargerY() {
466         return chargerY;
467     }
468
469     public float getGotoX() {
470         return gotoX;
471     }
472
473     public float getGotoY() {
474         return gotoY;
475     }
476
477     public int getRoboA() {
478         return roboA;
479     }
480
481     public Map<Integer, ArrayList<float[]>> getPaths() {
482         return paths;
483     }
484
485     public Map<Integer, Map<String, Integer>> getPathsDetails() {
486         return pathsDetails;
487     }
488
489     public ArrayList<float[]> getWalls() {
490         return walls;
491     }
492
493     public Map<Integer, ArrayList<float[]>> getAreas() {
494         return areas;
495     }
496
497     public Map<Integer, ArrayList<int[]>> getObstacles() {
498         return obstacles;
499     }
500
501     public byte[] getBlocks() {
502         return blocks;
503     }
504
505     public final int[] getCarpetMap() {
506         return carpetMap;
507     }
508 }