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