]> git.basschouten.com Git - openhab-addons.git/blob
4e8f834f8499e59c9ade6b193f31fe93a07c8afe
[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.insteon.internal.message;
14
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.util.Arrays;
18 import java.util.Comparator;
19 import java.util.HashMap;
20 import java.util.Map;
21 import java.util.TreeSet;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.insteon.internal.device.InsteonAddress;
26 import org.openhab.binding.insteon.internal.utils.Utils;
27 import org.openhab.binding.insteon.internal.utils.Utils.ParsingException;
28 import org.osgi.framework.FrameworkUtil;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31
32 /**
33  * Contains an Insteon Message consisting of the raw data, and the message definition.
34  * For more info, see the public Insteon Developer's Guide, 2nd edition,
35  * and the Insteon Modem Developer's Guide.
36  *
37  * @author Bernd Pfrommer - Initial contribution
38  * @author Daniel Pfrommer - openHAB 1 insteonplm binding
39  * @author Rob Nielsen - Port to openHAB 2 insteon binding
40  */
41 @NonNullByDefault
42 @SuppressWarnings("null")
43 public class Msg {
44     private static final Logger logger = LoggerFactory.getLogger(Msg.class);
45
46     /**
47      * Represents the direction of the message from the host's view.
48      * The host is the machine to which the modem is attached.
49      */
50     public enum Direction {
51         TO_MODEM("TO_MODEM"),
52         FROM_MODEM("FROM_MODEM");
53
54         private static HashMap<String, Direction> map = new HashMap<>();
55
56         private String directionString;
57
58         static {
59             map.put(TO_MODEM.getDirectionString(), TO_MODEM);
60             map.put(FROM_MODEM.getDirectionString(), FROM_MODEM);
61         }
62
63         Direction(String dirString) {
64             this.directionString = dirString;
65         }
66
67         public String getDirectionString() {
68             return directionString;
69         }
70
71         public static Direction getDirectionFromString(String dir) {
72             return map.get(dir);
73         }
74     }
75
76     // has the structure of all known messages
77     private static final Map<String, Msg> MSG_MAP = new HashMap<>();
78     // maps between command number and the length of the header
79     private static final Map<Integer, Integer> HEADER_MAP = new HashMap<>();
80     // has templates for all message from modem to host
81     private static final Map<Integer, Msg> REPLY_MAP = new HashMap<>();
82
83     private int headerLength = -1;
84     private byte @Nullable [] data = null;
85     private MsgDefinition definition = new MsgDefinition();
86     private Direction direction = Direction.TO_MODEM;
87     private long quietTime = 0;
88
89     /**
90      * Constructor
91      *
92      * @param headerLength length of message header (in bytes)
93      * @param data byte array with message
94      * @param dataLength length of byte array data (in bytes)
95      * @param dir direction of the message (from/to modem)
96      */
97     public Msg(int headerLength, byte[] data, int dataLength, Direction dir) {
98         this.headerLength = headerLength;
99         this.direction = dir;
100         initialize(data, 0, dataLength);
101     }
102
103     /**
104      * Copy constructor, needed to make a copy of the templates when
105      * generating messages from them.
106      *
107      * @param m the message to make a copy of
108      */
109     public Msg(Msg m) {
110         headerLength = m.headerLength;
111         data = m.data.clone();
112         // the message definition usually doesn't change, but just to be sure...
113         definition = new MsgDefinition(m.definition);
114         direction = m.direction;
115     }
116
117     static {
118         // Use xml msg loader to load configs
119         try {
120             InputStream stream = FrameworkUtil.getBundle(Msg.class).getResource("/msg_definitions.xml").openStream();
121             if (stream != null) {
122                 HashMap<String, Msg> msgs = XMLMessageReader.readMessageDefinitions(stream);
123                 MSG_MAP.putAll(msgs);
124             } else {
125                 logger.warn("could not get message definition resource!");
126             }
127         } catch (IOException e) {
128             logger.warn("i/o error parsing xml insteon message definitions", e);
129         } catch (ParsingException e) {
130             logger.warn("parse error parsing xml insteon message definitions", e);
131         } catch (FieldException e) {
132             logger.warn("got field exception while parsing xml insteon message definitions", e);
133         }
134         buildHeaderMap();
135         buildLengthMap();
136     }
137
138     //
139     // ------------------ simple getters and setters -----------------
140     //
141
142     /**
143      * Experience has shown that if Insteon messages are sent in close succession,
144      * only the first one will make it. The quiet time parameter says how long to
145      * wait after a message before the next one can be sent.
146      *
147      * @return the time (in milliseconds) to pause after message has been sent
148      */
149     public long getQuietTime() {
150         return quietTime;
151     }
152
153     public byte @Nullable [] getData() {
154         return data;
155     }
156
157     public int getLength() {
158         return data.length;
159     }
160
161     public int getHeaderLength() {
162         return headerLength;
163     }
164
165     public Direction getDirection() {
166         return direction;
167     }
168
169     public MsgDefinition getDefinition() {
170         return definition;
171     }
172
173     public byte getCommandNumber() {
174         return ((data == null || data.length < 2) ? -1 : data[1]);
175     }
176
177     public boolean isPureNack() {
178         return (data.length == 2 && data[1] == 0x15);
179     }
180
181     public boolean isExtended() {
182         if (data == null || getLength() < 2) {
183             return false;
184         }
185         if (!definition.containsField("messageFlags")) {
186             return (false);
187         }
188         try {
189             byte flags = getByte("messageFlags");
190             return ((flags & 0x10) == 0x10);
191         } catch (FieldException e) {
192             // do nothing
193         }
194         return false;
195     }
196
197     public boolean isUnsolicited() {
198         // if the message has an ACK/NACK, it is in response to our message,
199         // otherwise it is out-of-band, i.e. unsolicited
200         return !definition.containsField("ACK/NACK");
201     }
202
203     public boolean isEcho() {
204         return isPureNack() || !isUnsolicited();
205     }
206
207     public boolean isOfType(MsgType mt) {
208         try {
209             MsgType t = MsgType.fromValue(getByte("messageFlags"));
210             return (t == mt);
211         } catch (FieldException e) {
212             return false;
213         }
214     }
215
216     public boolean isBroadcast() {
217         return isOfType(MsgType.ALL_LINK_BROADCAST) || isOfType(MsgType.BROADCAST);
218     }
219
220     public boolean isCleanup() {
221         return isOfType(MsgType.ALL_LINK_CLEANUP);
222     }
223
224     public boolean isAllLink() {
225         return isOfType(MsgType.ALL_LINK_BROADCAST) || isOfType(MsgType.ALL_LINK_CLEANUP);
226     }
227
228     public boolean isAckOfDirect() {
229         return isOfType(MsgType.ACK_OF_DIRECT);
230     }
231
232     public boolean isAllLinkCleanupAckOrNack() {
233         return isOfType(MsgType.ALL_LINK_CLEANUP_ACK) || isOfType(MsgType.ALL_LINK_CLEANUP_NACK);
234     }
235
236     public boolean isX10() {
237         try {
238             int cmd = getByte("Cmd") & 0xff;
239             if (cmd == 0x63 || cmd == 0x52) {
240                 return true;
241             }
242         } catch (FieldException e) {
243         }
244         return false;
245     }
246
247     public void setDefinition(MsgDefinition d) {
248         definition = d;
249     }
250
251     public void setQuietTime(long t) {
252         quietTime = t;
253     }
254
255     public void addField(Field f) {
256         definition.addField(f);
257     }
258
259     public @Nullable InsteonAddress getAddr(String name) {
260         @Nullable
261         InsteonAddress a = null;
262         try {
263             a = definition.getField(name).getAddress(data);
264         } catch (FieldException e) {
265             // do nothing, we'll return null
266         }
267         return a;
268     }
269
270     public int getHopsLeft() throws FieldException {
271         int hops = (getByte("messageFlags") & 0x0c) >> 2;
272         return hops;
273     }
274
275     /**
276      * Will initialize the message with a byte[], an offset, and a length
277      *
278      * @param newData the src byte array
279      * @param offset the offset in the src byte array
280      * @param len the length to copy from the src byte array
281      */
282     private void initialize(byte[] newData, int offset, int len) {
283         byte[] data = new byte[len];
284         if (offset >= 0 && offset < newData.length) {
285             System.arraycopy(newData, offset, data, 0, len);
286         } else {
287             logger.warn("intialize(): Offset out of bounds!");
288         }
289         this.data = data;
290     }
291
292     /**
293      * Will put a byte at the specified key
294      *
295      * @param key the string key in the message definition
296      * @param value the byte to put
297      */
298     public void setByte(@Nullable String key, byte value) throws FieldException {
299         Field f = definition.getField(key);
300         f.setByte(data, value);
301     }
302
303     /**
304      * Will put an int at the specified field key
305      *
306      * @param key the name of the field
307      * @param value the int to put
308      */
309     public void setInt(String key, int value) throws FieldException {
310         Field f = definition.getField(key);
311         f.setInt(data, value);
312     }
313
314     /**
315      * Will put address bytes at the field
316      *
317      * @param key the name of the field
318      * @param adr the address to put
319      */
320     public void setAddress(String key, InsteonAddress adr) throws FieldException {
321         Field f = definition.getField(key);
322         f.setAddress(data, adr);
323     }
324
325     /**
326      * Will fetch a byte
327      *
328      * @param key the name of the field
329      * @return the byte
330      */
331     public byte getByte(String key) throws FieldException {
332         return (definition.getField(key).getByte(data));
333     }
334
335     /**
336      * Will fetch a byte array starting at a certain field
337      *
338      * @param key the name of the first field
339      * @param number of bytes to get
340      * @return the byte array
341      */
342     public byte[] getBytes(String key, int numBytes) throws FieldException {
343         int offset = definition.getField(key).getOffset();
344         if (offset < 0 || offset + numBytes > data.length) {
345             throw new FieldException("data index out of bounds!");
346         }
347         byte[] section = new byte[numBytes];
348         byte[] data = this.data;
349         if (data != null) {
350             System.arraycopy(data, offset, section, 0, numBytes);
351         }
352         return section;
353     }
354
355     /**
356      * Will fetch address from field
357      *
358      * @param field the filed name to fetch
359      * @return the address
360      */
361     public InsteonAddress getAddress(String field) throws FieldException {
362         return (definition.getField(field).getAddress(data));
363     }
364
365     /**
366      * Fetch 3-byte (24bit) from message
367      *
368      * @param key1 the key of the msb
369      * @param key2 the key of the second msb
370      * @param key3 the key of the lsb
371      * @return the integer
372      */
373     public int getInt24(String key1, String key2, String key3) throws FieldException {
374         int i = (definition.getField(key1).getByte(data) << 16) & (definition.getField(key2).getByte(data) << 8)
375                 & definition.getField(key3).getByte(data);
376         return i;
377     }
378
379     public String toHexString() {
380         if (data != null) {
381             return Utils.getHexString(data);
382         }
383         return super.toString();
384     }
385
386     /**
387      * Sets the userData fields from a byte array
388      *
389      * @param data
390      */
391     public void setUserData(byte[] arg) {
392         byte[] data = Arrays.copyOf(arg, 14); // appends zeros if short
393         try {
394             setByte("userData1", data[0]);
395             setByte("userData2", data[1]);
396             setByte("userData3", data[2]);
397             setByte("userData4", data[3]);
398             setByte("userData5", data[4]);
399             setByte("userData6", data[5]);
400             setByte("userData7", data[6]);
401             setByte("userData8", data[7]);
402             setByte("userData9", data[8]);
403             setByte("userData10", data[9]);
404             setByte("userData11", data[10]);
405             setByte("userData12", data[11]);
406             setByte("userData13", data[12]);
407             setByte("userData14", data[13]);
408         } catch (FieldException e) {
409             logger.warn("got field exception on msg {}:", e.getMessage());
410         }
411     }
412
413     /**
414      * Calculate and set the CRC with the older 1-byte method
415      *
416      * @return the calculated crc
417      */
418     public int setCRC() {
419         int crc;
420         try {
421             crc = getByte("command1") + getByte("command2");
422             byte[] bytes = getBytes("userData1", 13); // skip userData14!
423             for (byte b : bytes) {
424                 crc += b;
425             }
426             crc = ((~crc) + 1) & 0xFF;
427             setByte("userData14", (byte) (crc & 0xFF));
428         } catch (FieldException e) {
429             logger.warn("got field exception on msg {}:", this, e);
430             crc = 0;
431         }
432         return crc;
433     }
434
435     /**
436      * Calculate and set the CRC with the newer 2-byte method
437      *
438      * @return the calculated crc
439      */
440     public int setCRC2() {
441         int crc = 0;
442         try {
443             byte[] bytes = getBytes("command1", 14);
444             for (int loop = 0; loop < bytes.length; loop++) {
445                 int b = bytes[loop] & 0xFF;
446                 for (int bit = 0; bit < 8; bit++) {
447                     int fb = b & 0x01;
448                     if ((crc & 0x8000) == 0) {
449                         fb = fb ^ 0x01;
450                     }
451                     if ((crc & 0x4000) == 0) {
452                         fb = fb ^ 0x01;
453                     }
454                     if ((crc & 0x1000) == 0) {
455                         fb = fb ^ 0x01;
456                     }
457                     if ((crc & 0x0008) == 0) {
458                         fb = fb ^ 0x01;
459                     }
460                     crc = ((crc << 1) | fb) & 0xFFFF;
461                     b = b >> 1;
462                 }
463             }
464             setByte("userData13", (byte) ((crc >> 8) & 0xFF));
465             setByte("userData14", (byte) (crc & 0xFF));
466         } catch (FieldException e) {
467             logger.warn("got field exception on msg {}:", this, e);
468             crc = 0;
469         }
470         return crc;
471     }
472
473     @Override
474     public String toString() {
475         String s = (direction == Direction.TO_MODEM) ? "OUT:" : "IN:";
476         if (data == null) {
477             return toHexString();
478         }
479         // need to first sort the fields by offset
480         Comparator<@Nullable Field> cmp = new Comparator<@Nullable Field>() {
481             @Override
482             public int compare(@Nullable Field f1, @Nullable Field f2) {
483                 return f1.getOffset() - f2.getOffset();
484             }
485         };
486         TreeSet<@Nullable Field> fields = new TreeSet<>(cmp);
487         for (@Nullable
488         Field f : definition.getFields().values()) {
489             fields.add(f);
490         }
491         for (Field f : fields) {
492             if (f.getName().equals("messageFlags")) {
493                 byte b;
494                 try {
495                     b = f.getByte(data);
496                     MsgType t = MsgType.fromValue(b);
497                     s += f.toString(data) + "=" + t.toString() + ":" + (b & 0x03) + ":" + ((b & 0x0c) >> 2) + "|";
498                 } catch (FieldException e) {
499                     logger.warn("toString error: ", e);
500                 } catch (IllegalArgumentException e) {
501                     logger.warn("toString msg type error: ", e);
502                 }
503             } else {
504                 s += f.toString(data) + "|";
505             }
506         }
507         return s;
508     }
509
510     /**
511      * Factory method to create Msg from raw byte stream received from the
512      * serial port.
513      *
514      * @param buf the raw received bytes
515      * @param msgLen length of received buffer
516      * @param isExtended whether it is an extended message or not
517      * @return message, or null if the Msg cannot be created
518      */
519     public static @Nullable Msg createMessage(byte[] buf, int msgLen, boolean isExtended) {
520         if (buf == null || buf.length < 2) {
521             return null;
522         }
523         Msg template = REPLY_MAP.get(cmdToKey(buf[1], isExtended));
524         if (template == null) {
525             return null; // cannot find lookup map
526         }
527         if (msgLen != template.getLength()) {
528             logger.warn("expected msg {} len {}, got {}", template.getCommandNumber(), template.getLength(), msgLen);
529             return null;
530         }
531         Msg msg = new Msg(template.getHeaderLength(), buf, msgLen, Direction.FROM_MODEM);
532         msg.setDefinition(template.getDefinition());
533         return (msg);
534     }
535
536     /**
537      * Finds the header length from the insteon command in the received message
538      *
539      * @param cmd the insteon command received in the message
540      * @return the length of the header to expect
541      */
542     public static int getHeaderLength(byte cmd) {
543         Integer len = HEADER_MAP.get((int) cmd);
544         if (len == null) {
545             return (-1); // not found
546         }
547         return len;
548     }
549
550     /**
551      * Tries to determine the length of a received Insteon message.
552      *
553      * @param b Insteon message command received
554      * @param isExtended flag indicating if it is an extended message
555      * @return message length, or -1 if length cannot be determined
556      */
557     public static int getMessageLength(byte b, boolean isExtended) {
558         int key = cmdToKey(b, isExtended);
559         Msg msg = REPLY_MAP.get(key);
560         if (msg == null) {
561             return -1;
562         }
563         return msg.getLength();
564     }
565
566     /**
567      * From bytes received thus far, tries to determine if an Insteon
568      * message is extended or standard.
569      *
570      * @param buf the received bytes
571      * @param len the number of bytes received so far
572      * @param headerLength the known length of the header
573      * @return true if it is definitely extended, false if cannot be
574      *         determined or if it is a standard message
575      */
576     public static boolean isExtended(byte[] buf, int len, int headerLength) {
577         if (headerLength <= 2) {
578             return false;
579         } // extended messages are longer
580         if (len < headerLength) {
581             return false;
582         } // not enough data to tell if extended
583         byte flags = buf[headerLength - 1]; // last byte says flags
584         boolean isExtended = (flags & 0x10) == 0x10; // bit 4 is the message
585         return (isExtended);
586     }
587
588     /**
589      * Creates Insteon message (for sending) of a given type
590      *
591      * @param type the type of message to create, as defined in the xml file
592      * @return reference to message created
593      * @throws IOException if there is no such message type known
594      */
595     public static Msg makeMessage(String type) throws InvalidMessageTypeException {
596         Msg m = MSG_MAP.get(type);
597         if (m == null) {
598             throw new InvalidMessageTypeException("unknown message type: " + type);
599         }
600         return new Msg(m);
601     }
602
603     private static int cmdToKey(byte cmd, boolean isExtended) {
604         return (cmd + (isExtended ? 256 : 0));
605     }
606
607     private static void buildHeaderMap() {
608         for (Msg m : MSG_MAP.values()) {
609             if (m.getDirection() == Direction.FROM_MODEM) {
610                 HEADER_MAP.put((int) m.getCommandNumber(), m.getHeaderLength());
611             }
612         }
613     }
614
615     private static void buildLengthMap() {
616         for (Msg m : MSG_MAP.values()) {
617             if (m.getDirection() == Direction.FROM_MODEM) {
618                 int key = cmdToKey(m.getCommandNumber(), m.isExtended());
619                 REPLY_MAP.put(key, m);
620             }
621         }
622     }
623 }