]> git.basschouten.com Git - openhab-addons.git/blob
c0c57eb85a7d043b3987e957b8e0b3e3dd1a0d62
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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     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;
80
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";
84
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;
91
92     private int imgHeight;
93     private int imgWidth;
94     private int imageSize;
95     private int top;
96     private int left;
97
98     private int chargerX;
99     private int chargerY;
100     private int roboX;
101     private int roboY;
102     private int roboA;
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 = {};
114
115     private final Logger logger = LoggerFactory.getLogger(RRMapFileParser.class);
116
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);
125
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);
134
135             switch (blocktype) {
136                 case CHARGER:
137                     this.chargerX = getUInt32LE(raw, blockStartPos + 0x08);
138                     this.chargerY = getUInt32LE(raw, blockStartPos + 0x0C);
139                     break;
140                 case IMAGE:
141                     this.imageSize = blockDataLength;// (getUInt32LE(raw, blockStartPos + 0x04));
142                     if (blockHeaderLength > 0x1C) {
143                         logger.debug("block 2 unknown value @pos 8: {}", getUInt32LE(header, 0x08));
144                     }
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);
149                     this.image = data;
150                     break;
151                 case ROBOT_POSITION:
152                     this.roboX = getUInt32LE(data, 0x00);
153                     this.roboY = getUInt32LE(data, 0x04);
154                     if (blockDataLength > 8) { // model S6
155                         this.roboA = getUInt32LE(data, 0x08);
156                     }
157                     break;
158                 case PATH:
159                 case GOTO_PATH:
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 });
171                     }
172                     paths.put(blocktype, path);
173                     pathsDetails.put(blocktype, detail);
174                     break;
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 });
183                     }
184                     break;
185                 case GOTO_TARGET:
186                     this.gotoX = getUInt16(data, 0x00);
187                     this.gotoY = getUInt16(data, 0x02);
188                     break;
189                 case DIGEST:
190                     isValid = Arrays.equals(data, sha1Hash(getBytes(raw, 0, mapHeaderLength + mapDataLength - 20)));
191                     break;
192                 case VIRTUAL_WALLS:
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 });
200                     }
201                     break;
202                 case NO_GO_AREAS:
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 });
217                     }
218                     areas.put(Integer.valueOf(blocktype & 0xFF), area);
219                     break;
220                 case OBSTACLES:
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 });
229                     }
230                     obstacles.put(Integer.valueOf(blocktype & 0xFF), obstacle);
231                     break;
232                 case OBSTACLES2:
233                     int obstacle2Pairs = getUInt16(header, 0x08);
234                     if (obstacle2Pairs == 0) {
235                         break;
236                     }
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");
249                             } else {
250                                 byte[] txt = getBytes(data, obstaclePair * obstacleDataLenght + 12, 16);
251                                 logger.trace("obstacle with photo: {}", new String(txt));
252                             }
253                             obstacle2.add(new int[] { x0, y0, u0, u1, u2 });
254                         } else {
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.");
258                         }
259                     }
260                     obstacles.put(Integer.valueOf(blocktype & 0xFF), obstacle2);
261                     break;
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 });
270                     }
271                     obstacles.put(Integer.valueOf(blocktype & 0xFF), ignoredObstacle);
272                     break;
273                 case CARPET_MAP:
274                     carpetMap = new int[blockDataLength];
275                     for (int carpetNode = 0; carpetNode < blockDataLength; carpetNode++) {
276                         carpetMap[carpetNode] = data[carpetNode] & 0xFF;
277                     }
278                     break;
279                 case MOP_PATH:
280                     mopPath = new int[blockDataLength];
281                     for (int mopNode = 0; mopNode < blockDataLength; mopNode++) {
282                         mopPath[mopNode] = data[mopNode] & 0xFF;
283                     }
284                     break;
285                 case BLOCKS:
286                     int blocksPairs = getUInt16(header, 0x08);
287                     blocks = getBytes(data, 0, blocksPairs);
288                     break;
289                 case SMART_ZONES_PATH_TYPE:
290                 case SMART_ZONES:
291                 case CUSTOM_CARPET:
292                 case CL_FORBIDDEN_ZONES:
293                 case FLOOR_MAP:
294                 case FURNITURES:
295                 case DOCK_TYPE:
296                 case ENEMIES:
297                 case DOOR_ZONES:
298                 case STUCK_POINTS:
299                 case CLIFF_ZONES:
300                 case SMARTDS:
301                 case FLDIREC:
302                 case MAP_DATE:
303                 case NONCE_DATA:
304                     // new blocktypes not yet decoded
305                     break;
306                 default:
307                     logger.info("Unknown blocktype {} (pls report to author)", blocktype);
308                     printBlockDetails = true;
309             }
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))));
318                 }
319                 printBlockDetails = false;
320             }
321             blockStartPos = blockStartPos + blockDataLength + (header[2] & 0xFF);
322         }
323     }
324
325     public static byte[] readRRMapFile(File file) throws IOException {
326         return readRRMapFile(new FileInputStream(file));
327     }
328
329     public static byte[] readRRMapFile(InputStream is) throws IOException {
330         ByteArrayOutputStream baos = new ByteArrayOutputStream();
331         try (GZIPInputStream in = new GZIPInputStream(is)) {
332             int bufsize = 1024;
333             byte[] buf = new byte[bufsize];
334             int readbytes = 0;
335             readbytes = in.read(buf);
336             while (readbytes != -1) {
337                 baos.write(buf, 0, readbytes);
338                 readbytes = in.read(buf);
339             }
340             baos.flush();
341             return baos.toByteArray();
342         }
343     }
344
345     private byte[] getBytes(byte[] raw, int pos, int len) {
346         return java.util.Arrays.copyOfRange(raw, pos, pos + len);
347     }
348
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;
354         return value;
355     }
356
357     private int getUInt16(byte[] bytes) {
358         return getUInt16(bytes, 0);
359     }
360
361     private int getUInt16(byte[] bytes, int pos) {
362         int value = bytes[0 + pos] & 0xFF;
363         value |= (bytes[1 + pos] << 8) & 0xFFFF;
364         return value;
365     }
366
367     @Override
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,
374                 imgWidth);
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()) {
380                 case NO_GO_AREAS:
381                     pw.print("Regular No Go zones:\t");
382                     break;
383                 case MOB_FORBIDDEN_AREA:
384                     pw.print("Mop No Go zones:\t");
385                     break;
386                 case CARPET_FORBIDDEN_AREA:
387                     pw.print("Carpet No Go zones:\t");
388                     break;
389                 default:
390                     pw.print("Unknown type zones:\t");
391             }
392             pw.printf("%d\r\n", area.getValue().size());
393             printAreaDetails(area.getValue(), pw);
394         }
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);
402         }
403         pw.printf("Blocks:\t%d\r\n", blocks.length);
404         pw.print("Paths:");
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));
409             }
410         }
411         pw.println();
412         pw.printf("Carpet Map:\t%d\r\n", carpetMap.length);
413         pw.printf("Mop Path:\t%d\r\n", mopPath.length);
414         pw.close();
415         return sw.toString();
416     }
417
418     private void printAreaDetails(@Nullable ArrayList<float[]> areas, PrintWriter pw) {
419         if (areas == null) {
420             pw.println("null");
421             return;
422         }
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]);
427             }
428             pw.println();
429         });
430     }
431
432     private void printObstacleDetails(@Nullable ArrayList<int[]> obstacle, PrintWriter pw) {
433         if (obstacle == null) {
434             pw.println("null");
435             return;
436         }
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]);
441             }
442             pw.println();
443         });
444     }
445
446     /**
447      * Compute SHA-1 hash value for the byte array
448      *
449      * @param inBytes ByteArray to be hashed
450      * @return hash value
451      */
452     public static byte[] sha1Hash(byte[] inBytes) {
453         try {
454             MessageDigest md = MessageDigest.getInstance("SHA-1");
455             return md.digest(inBytes);
456         } catch (NoSuchAlgorithmException e) {
457             return new byte[] { 0x00 };
458         }
459     }
460
461     public int getMajorVersion() {
462         return majorVersion;
463     }
464
465     public int getMinorVersion() {
466         return minorVersion;
467     }
468
469     public int getMapIndex() {
470         return mapIndex;
471     }
472
473     public int getMapSequence() {
474         return mapSequence;
475     }
476
477     public boolean isValid() {
478         return isValid;
479     }
480
481     public byte[] getImage() {
482         return image;
483     }
484
485     public int getImageSize() {
486         return imageSize;
487     }
488
489     public int getImgHeight() {
490         return imgHeight;
491     }
492
493     public int getImgWidth() {
494         return imgWidth;
495     }
496
497     public int getTop() {
498         return top;
499     }
500
501     public int getLeft() {
502         return left;
503     }
504
505     public ArrayList<float[]> getZones() {
506         return zones;
507     }
508
509     public float getRoboX() {
510         return roboX;
511     }
512
513     public float getRoboY() {
514         return roboY;
515     }
516
517     public float getChargerX() {
518         return chargerX;
519     }
520
521     public float getChargerY() {
522         return chargerY;
523     }
524
525     public float getGotoX() {
526         return gotoX;
527     }
528
529     public float getGotoY() {
530         return gotoY;
531     }
532
533     public int getRoboA() {
534         return roboA;
535     }
536
537     public Map<Integer, ArrayList<float[]>> getPaths() {
538         return paths;
539     }
540
541     public Map<Integer, Map<String, Integer>> getPathsDetails() {
542         return pathsDetails;
543     }
544
545     public ArrayList<float[]> getWalls() {
546         return walls;
547     }
548
549     public Map<Integer, ArrayList<float[]>> getAreas() {
550         return areas;
551     }
552
553     public Map<Integer, ArrayList<int[]>> getObstacles() {
554         return obstacles;
555     }
556
557     public byte[] getBlocks() {
558         return blocks;
559     }
560
561     public final int[] getCarpetMap() {
562         return carpetMap;
563     }
564
565     public final int[] getMopPath() {
566         return mopPath;
567     }
568 }