]> git.basschouten.com Git - openhab-addons.git/blob
06fbdd916b287ee433dc30f6793e786daf660a3f
[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.rotel.internal.communication;
14
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.InterruptedIOException;
18 import java.io.OutputStream;
19 import java.nio.charset.StandardCharsets;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.regex.PatternSyntaxException;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.rotel.internal.RotelException;
29 import org.openhab.binding.rotel.internal.RotelModel;
30 import org.openhab.core.util.HexUtils;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 /**
35  * Abstract class for communicating with the Rotel device
36  *
37  * @author Laurent Garnier - Initial contribution
38  */
39 @NonNullByDefault
40 public abstract class RotelConnector {
41
42     private final Logger logger = LoggerFactory.getLogger(RotelConnector.class);
43
44     public static final byte[] READ_ERROR = "read_error".getBytes(StandardCharsets.US_ASCII);
45
46     protected static final byte START = (byte) 0xFE;
47
48     // Message types
49     public static final byte PRIMARY_CMD = (byte) 0x10;
50     public static final byte MAIN_ZONE_CMD = (byte) 0x14;
51     public static final byte RECORD_SRC_CMD = (byte) 0x15;
52     public static final byte ZONE2_CMD = (byte) 0x16;
53     public static final byte ZONE3_CMD = (byte) 0x17;
54     public static final byte ZONE4_CMD = (byte) 0x18;
55     public static final byte VOLUME_CMD = (byte) 0x30;
56     public static final byte ZONE2_VOLUME_CMD = (byte) 0x32;
57     public static final byte ZONE3_VOLUME_CMD = (byte) 0x33;
58     public static final byte ZONE4_VOLUME_CMD = (byte) 0x34;
59     private static final byte TRIGGER_CMD = (byte) 0x40;
60     protected static final byte STANDARD_RESPONSE = (byte) 0x20;
61     private static final byte TRIGGER_STATUS = (byte) 0x21;
62     private static final byte SMART_DISPLAY_DATA_1 = (byte) 0x22;
63     private static final byte SMART_DISPLAY_DATA_2 = (byte) 0x23;
64
65     // Keys used by the HEX protocol
66     private static final String KEY1_HEX_VOLUME = "volume ";
67     private static final String KEY2_HEX_VOLUME = "vol ";
68     private static final String KEY_HEX_MUTE = "mute ";
69     private static final String KEY1_HEX_BASS = "bass ";
70     private static final String KEY2_HEX_BASS = "lf ";
71     private static final String KEY1_HEX_TREBLE = "treble ";
72     private static final String KEY2_HEX_TREBLE = "hf ";
73     private static final String KEY_HEX_MULTI_IN = "multi in ";
74     private static final String KEY_HEX_STEREO = "stereo";
75     private static final String KEY1_HEX_3CH = "3 stereo";
76     private static final String KEY2_HEX_3CH = "dolby 3 stereo";
77     private static final String KEY_HEX_5CH = "5ch stereo";
78     private static final String KEY_HEX_7CH = "7ch stereo";
79     private static final String KEY_HEX_MUSIC1 = "music 1";
80     private static final String KEY_HEX_MUSIC2 = "music 2";
81     private static final String KEY_HEX_MUSIC3 = "music 3";
82     private static final String KEY_HEX_MUSIC4 = "music 4";
83     private static final String KEY_HEX_DSP1 = "dsp 1";
84     private static final String KEY_HEX_DSP2 = "dsp 2";
85     private static final String KEY_HEX_DSP3 = "dsp 3";
86     private static final String KEY_HEX_DSP4 = "dsp 4";
87     private static final String KEY1_HEX_PROLOGIC = "prologic  emu";
88     private static final String KEY2_HEX_PROLOGIC = "dolby pro logic";
89     private static final String KEY1_HEX_PLII_CINEMA = "prologic  cin";
90     private static final String KEY2_HEX_PLII_CINEMA = "dolby pl  c";
91     private static final String KEY1_HEX_PLII_MUSIC = "prologic  mus";
92     private static final String KEY2_HEX_PLII_MUSIC = "dolby pl  m";
93     private static final String KEY1_HEX_PLII_GAME = "prologic  gam";
94     private static final String KEY2_HEX_PLII_GAME = "dolby pl  g";
95     private static final String KEY1_HEX_PLIIX_CINEMA = "pl x cinema";
96     private static final String KEY2_HEX_PLIIX_CINEMA = "dolby pl x c";
97     private static final String KEY1_HEX_PLIIX_MUSIC = "pl x music";
98     private static final String KEY2_HEX_PLIIX_MUSIC = "dolby pl x m";
99     private static final String KEY1_HEX_PLIIX_GAME = "pl x game";
100     private static final String KEY2_HEX_PLIIX_GAME = "dolby pl x g";
101     private static final String KEY_HEX_PLIIZ = "dolby pl z";
102     private static final String KEY1_HEX_DTS_NEO6_CINEMA = "neo 6 cinema";
103     private static final String KEY2_HEX_DTS_NEO6_CINEMA = "dts neo:6 c";
104     private static final String KEY1_HEX_DTS_NEO6_MUSIC = "neo 6 music";
105     private static final String KEY2_HEX_DTS_NEO6_MUSIC = "dts neo:6 m";
106     private static final String KEY_HEX_DTS = "dts";
107     private static final String KEY_HEX_DTS_ES = "dts-es";
108     private static final String KEY_HEX_DTS_96 = "dts 96";
109     private static final String KEY_HEX_DD = "dolby digital";
110     private static final String KEY_HEX_DD_EX = "dolby d ex";
111     private static final String KEY_HEX_PCM = "pcm";
112     private static final String KEY_HEX_LPCM = "lpcm";
113     private static final String KEY_HEX_MPEG = "mpeg";
114     private static final String KEY_HEX_BYPASS = "bypass";
115     private static final String KEY1_HEX_ZONE2 = "zone ";
116     private static final String KEY2_HEX_ZONE2 = "zone2 ";
117     private static final String KEY_HEX_ZONE3 = "zone3 ";
118     private static final String KEY_HEX_ZONE4 = "zone4 ";
119     private static final String KEY_HEX_RECORD = "rec ";
120
121     // Keys used by the ASCII protocol
122     public static final String KEY_UPDATE_MODE = "update_mode";
123     public static final String KEY_DISPLAY_UPDATE = "display_update";
124     public static final String KEY_POWER = "power";
125     public static final String KEY_VOLUME_MIN = "volume_min";
126     public static final String KEY_VOLUME_MAX = "volume_max";
127     public static final String KEY_VOLUME = "volume";
128     public static final String KEY_MUTE = "mute";
129     public static final String KEY_TONE_MAX = "tone_max";
130     public static final String KEY_BASS = "bass";
131     public static final String KEY_TREBLE = "treble";
132     public static final String KEY_SOURCE = "source";
133     public static final String KEY1_PLAY_STATUS = "play_status";
134     public static final String KEY2_PLAY_STATUS = "status";
135     public static final String KEY_TRACK = "track";
136     public static final String KEY_DSP_MODE = "dsp_mode";
137     public static final String KEY_DIMMER = "dimmer";
138     public static final String KEY_FREQ = "freq";
139     public static final String KEY_TONE = "tone";
140     public static final String KEY_TCBYPASS = "bypass";
141     public static final String KEY_BALANCE = "balance";
142     public static final String KEY_SPEAKER = "speaker";
143
144     // Special keys used by the binding
145     public static final String KEY_LINE1 = "line1";
146     public static final String KEY_LINE2 = "line2";
147     public static final String KEY_RECORD = "record";
148     public static final String KEY_RECORD_SEL = "record_sel";
149     public static final String KEY_ZONE = "zone";
150     public static final String KEY_POWER_ZONE2 = "power_zone2";
151     public static final String KEY_POWER_ZONE3 = "power_zone3";
152     public static final String KEY_POWER_ZONE4 = "power_zone4";
153     public static final String KEY_SOURCE_ZONE2 = "source_zone2";
154     public static final String KEY_SOURCE_ZONE3 = "source_zone3";
155     public static final String KEY_SOURCE_ZONE4 = "source_zone4";
156     public static final String KEY_VOLUME_ZONE2 = "volume_zone2";
157     public static final String KEY_VOLUME_ZONE3 = "volume_zone3";
158     public static final String KEY_VOLUME_ZONE4 = "volume_zone4";
159     public static final String KEY_MUTE_ZONE2 = "mute_zone2";
160     public static final String KEY_MUTE_ZONE3 = "mute_zone3";
161     public static final String KEY_MUTE_ZONE4 = "mute_zone4";
162     public static final String KEY_ERROR = "error";
163
164     public static final String MSG_VALUE_OFF = "off";
165     public static final String MSG_VALUE_ON = "on";
166     public static final String POWER_ON = "on";
167     public static final String STANDBY = "standby";
168     public static final String POWER_OFF_DELAYED = "off_delayed";
169     protected static final String AUTO = "auto";
170     protected static final String MANUAL = "manual";
171     public static final String MSG_VALUE_MIN = "min";
172     public static final String MSG_VALUE_MAX = "max";
173     public static final String MSG_VALUE_FIX = "fix";
174     public static final String PLAY = "play";
175     public static final String PAUSE = "pause";
176     public static final String STOP = "stop";
177     private static final String SOURCE = "source";
178     public static final String MSG_VALUE_SPEAKER_A = "a";
179     public static final String MSG_VALUE_SPEAKER_B = "b";
180     public static final String MSG_VALUE_SPEAKER_AB = "a_b";
181
182     private RotelModel model;
183     private RotelProtocol protocol;
184     protected Map<RotelSource, String> sourcesLabels;
185     private boolean simu;
186
187     /** The output stream */
188     protected @Nullable OutputStream dataOut;
189
190     /** The input stream */
191     protected @Nullable InputStream dataIn;
192
193     /** true if the connection is established, false if not */
194     private boolean connected;
195
196     protected String readerThreadName;
197     private @Nullable Thread readerThread;
198
199     private List<RotelMessageEventListener> listeners = new ArrayList<>();
200
201     /** Special characters that can be found in the feedback messages for several devices using the ASCII protocol */
202     public static final byte[][] SPECIAL_CHARACTERS = { { (byte) 0xEE, (byte) 0x82, (byte) 0x85 },
203             { (byte) 0xEE, (byte) 0x82, (byte) 0x84 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x92 },
204             { (byte) 0xEE, (byte) 0x82, (byte) 0x87 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x8E },
205             { (byte) 0xEE, (byte) 0x82, (byte) 0x89 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x93 },
206             { (byte) 0xEE, (byte) 0x82, (byte) 0x8C }, { (byte) 0xEE, (byte) 0x82, (byte) 0x8F },
207             { (byte) 0xEE, (byte) 0x82, (byte) 0x8A }, { (byte) 0xEE, (byte) 0x82, (byte) 0x8B },
208             { (byte) 0xEE, (byte) 0x82, (byte) 0x81 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x82 },
209             { (byte) 0xEE, (byte) 0x82, (byte) 0x83 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x94 },
210             { (byte) 0xEE, (byte) 0x82, (byte) 0x97 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x98 },
211             { (byte) 0xEE, (byte) 0x82, (byte) 0x80 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x99 },
212             { (byte) 0xEE, (byte) 0x82, (byte) 0x9A }, { (byte) 0xEE, (byte) 0x82, (byte) 0x88 },
213             { (byte) 0xEE, (byte) 0x82, (byte) 0x95 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x96 },
214             { (byte) 0xEE, (byte) 0x82, (byte) 0x90 }, { (byte) 0xEE, (byte) 0x82, (byte) 0x91 },
215             { (byte) 0xEE, (byte) 0x82, (byte) 0x8D }, { (byte) 0xEE, (byte) 0x80, (byte) 0x80, (byte) 0xEE,
216                     (byte) 0x80, (byte) 0x81, (byte) 0xEE, (byte) 0x80, (byte) 0x82 } };
217
218     /** Special characters that can be found in the feedback messages for the RCD-1572 */
219     public static final byte[][] SPECIAL_CHARACTERS_RCD1572 = { { (byte) 0xC2, (byte) 0x8C },
220             { (byte) 0xC2, (byte) 0x54 }, { (byte) 0xC2, (byte) 0x81 }, { (byte) 0xC2, (byte) 0x82 },
221             { (byte) 0xC2, (byte) 0x83 } };
222
223     /** Empty table of special characters */
224     public static final byte[][] NO_SPECIAL_CHARACTERS = {};
225
226     /**
227      * Constructor
228      *
229      * @param model the Rotel model in use
230      * @param protocol the protocol to be used
231      * @param simu whether the communication is simulated or real
232      * @param readerThreadName the name of thread to be created
233      */
234     public RotelConnector(RotelModel model, RotelProtocol protocol, Map<RotelSource, String> sourcesLabels,
235             boolean simu, String readerThreadName) {
236         this.model = model;
237         this.protocol = protocol;
238         this.sourcesLabels = sourcesLabels;
239         this.simu = simu;
240         this.readerThreadName = readerThreadName;
241     }
242
243     /**
244      * Get the Rotel model
245      *
246      * @return the model
247      */
248     public RotelModel getModel() {
249         return model;
250     }
251
252     /**
253      * Get the protocol to be used
254      *
255      * @return the protocol
256      */
257     public RotelProtocol getProtocol() {
258         return protocol;
259     }
260
261     /**
262      * Get whether the connection is established or not
263      *
264      * @return true if the connection is established
265      */
266     public boolean isConnected() {
267         return connected;
268     }
269
270     /**
271      * Set whether the connection is established or not
272      *
273      * @param connected true if the connection is established
274      */
275     protected void setConnected(boolean connected) {
276         this.connected = connected;
277     }
278
279     /**
280      * Set the thread that handles the feedback messages
281      *
282      * @param readerThread the thread
283      */
284     protected void setReaderThread(Thread readerThread) {
285         this.readerThread = readerThread;
286     }
287
288     /**
289      * Open the connection with the Rotel device
290      *
291      * @throws RotelException - In case of any problem
292      */
293     public abstract void open() throws RotelException;
294
295     /**
296      * Close the connection with the Rotel device
297      */
298     public abstract void close();
299
300     /**
301      * Stop the thread that handles the feedback messages and close the opened input and output streams
302      */
303     protected void cleanup() {
304         Thread readerThread = this.readerThread;
305         if (readerThread != null) {
306             readerThread.interrupt();
307             try {
308                 readerThread.join();
309             } catch (InterruptedException e) {
310             }
311             this.readerThread = null;
312         }
313         OutputStream dataOut = this.dataOut;
314         if (dataOut != null) {
315             try {
316                 dataOut.close();
317             } catch (IOException e) {
318             }
319             this.dataOut = null;
320         }
321         InputStream dataIn = this.dataIn;
322         if (dataIn != null) {
323             try {
324                 dataIn.close();
325             } catch (IOException e) {
326             }
327             this.dataIn = null;
328         }
329     }
330
331     /**
332      * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
333      * actually read is returned as an integer.
334      *
335      * @param dataBuffer the buffer into which the data is read.
336      *
337      * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
338      *         stream has been reached.
339      *
340      * @throws RotelException - If the input stream is null, if the first byte cannot be read for any reason
341      *             other than the end of the file, if the input stream has been closed, or if some other I/O error
342      *             occurs.
343      * @throws InterruptedIOException - if the thread was interrupted during the reading of the input stream
344      */
345     protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
346         if (simu) {
347             throw new RotelException("readInput failed: should not be called in simu mode");
348         }
349         InputStream dataIn = this.dataIn;
350         if (dataIn == null) {
351             throw new RotelException("readInput failed: input stream is null");
352         }
353         try {
354             return dataIn.read(dataBuffer);
355         } catch (IOException e) {
356             logger.debug("readInput failed: {}", e.getMessage());
357             throw new RotelException("readInput failed", e);
358         }
359     }
360
361     /**
362      * Request the Rotel device to execute a command
363      *
364      * @param cmd the command to execute
365      *
366      * @throws RotelException - In case of any problem
367      */
368     public void sendCommand(RotelCommand cmd) throws RotelException {
369         sendCommand(cmd, null);
370     }
371
372     /**
373      * Request the Rotel device to execute a command
374      *
375      * @param cmd the command to execute
376      * @param value the integer value to consider for volume, bass or treble adjustment
377      *
378      * @throws RotelException - In case of any problem
379      */
380     public void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
381         String messageStr;
382         byte[] message = new byte[0];
383         switch (protocol) {
384             case HEX:
385                 if (cmd.getHexType() == 0) {
386                     logger.debug("Send comman \"{}\" ignored: not available for HEX protocol", cmd.getName());
387                     return;
388                 } else {
389                     final int size = 6;
390                     message = new byte[size];
391                     int idx = 0;
392                     message[idx++] = START;
393                     message[idx++] = 3;
394                     message[idx++] = model.getDeviceId();
395                     message[idx++] = cmd.getHexType();
396                     message[idx++] = (value == null) ? cmd.getHexKey() : (byte) (value & 0x000000FF);
397                     final byte checksum = computeCheckSum(message, idx - 1);
398                     if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
399                         message = Arrays.copyOf(message, size + 1);
400                         message[idx++] = (byte) 0xFD;
401                         message[idx++] = ((checksum & 0x000000FF) == 0x000000FD) ? (byte) 0 : (byte) 1;
402                     } else {
403                         message[idx++] = checksum;
404                     }
405                     logger.debug("Send command \"{}\" => {}", cmd.getName(), HexUtils.bytesToHex(message));
406                 }
407                 break;
408             case ASCII_V1:
409                 messageStr = cmd.getAsciiCommandV1();
410                 if (messageStr == null) {
411                     logger.debug("Send comman \"{}\" ignored: not available for ASCII V1 protocol", cmd.getName());
412                     return;
413                 } else {
414                     if (value != null) {
415                         switch (cmd) {
416                             case VOLUME_SET:
417                                 messageStr += String.format("%d", value);
418                                 break;
419                             case BASS_SET:
420                             case TREBLE_SET:
421                                 if (value == 0) {
422                                     messageStr += "000";
423                                 } else if (value > 0) {
424                                     messageStr += String.format("+%02d", value);
425                                 } else {
426                                     messageStr += String.format("-%02d", -value);
427                                 }
428                                 break;
429                             case BALANCE_SET:
430                                 if (value == 0) {
431                                     messageStr += "000";
432                                 } else if (value > 0) {
433                                     messageStr += String.format("R%02d", value);
434                                 } else {
435                                     messageStr += String.format("L%02d", -value);
436                                 }
437                                 break;
438                             case DIMMER_LEVEL_SET:
439                                 if (value > 0 && model.getDimmerLevelMin() < 0) {
440                                     messageStr += String.format("+%d", value);
441                                 } else {
442                                     messageStr += String.format("%d", value);
443                                 }
444                                 break;
445                             default:
446                                 break;
447                         }
448                     }
449                     if (!messageStr.endsWith("?")) {
450                         messageStr += "!";
451                     }
452                     message = messageStr.getBytes(StandardCharsets.US_ASCII);
453                     logger.debug("Send command \"{}\" => {}", cmd.getName(), messageStr);
454                 }
455                 break;
456             case ASCII_V2:
457                 messageStr = cmd.getAsciiCommandV2();
458                 if (messageStr == null) {
459                     logger.debug("Send comman \"{}\" ignored: not available for ASCII V2 protocol", cmd.getName());
460                     return;
461                 } else {
462                     if (value != null) {
463                         switch (cmd) {
464                             case VOLUME_SET:
465                                 messageStr += String.format("%02d", value);
466                                 break;
467                             case BASS_SET:
468                             case TREBLE_SET:
469                                 if (value == 0) {
470                                     messageStr += "000";
471                                 } else if (value > 0) {
472                                     messageStr += String.format("+%02d", value);
473                                 } else {
474                                     messageStr += String.format("-%02d", -value);
475                                 }
476                                 break;
477                             case BALANCE_SET:
478                                 if (value == 0) {
479                                     messageStr += "000";
480                                 } else if (value > 0) {
481                                     messageStr += String.format("r%02d", value);
482                                 } else {
483                                     messageStr += String.format("l%02d", -value);
484                                 }
485                                 break;
486                             case DIMMER_LEVEL_SET:
487                                 if (value > 0 && model.getDimmerLevelMin() < 0) {
488                                     messageStr += String.format("+%d", value);
489                                 } else {
490                                     messageStr += String.format("%d", value);
491                                 }
492                                 break;
493                             default:
494                                 break;
495                         }
496                     }
497                     if (!messageStr.endsWith("?")) {
498                         messageStr += "!";
499                     }
500                     message = messageStr.getBytes(StandardCharsets.US_ASCII);
501                     logger.debug("Send command \"{}\" => {}", cmd.getName(), messageStr);
502                 }
503                 break;
504         }
505         if (simu) {
506             return;
507         }
508         OutputStream dataOut = this.dataOut;
509         if (dataOut == null) {
510             throw new RotelException("Send command \"" + cmd.getName() + "\" failed: output stream is null");
511         }
512         try {
513             dataOut.write(message);
514             dataOut.flush();
515         } catch (IOException e) {
516             logger.debug("Send command \"{}\" failed: {}", cmd.getName(), e.getMessage());
517             throw new RotelException("Send command \"" + cmd.getName() + "\" failed", e);
518         }
519         logger.debug("Send command \"{}\" succeeded", cmd.getName());
520     }
521
522     /**
523      * Validate the content of a feedback message
524      *
525      * @param responseMessage the buffer containing the feedback message
526      *
527      * @throws RotelException - If the message has unexpected content
528      */
529     private void validateResponse(byte[] responseMessage) throws RotelException {
530         if (protocol == RotelProtocol.HEX) {
531             // Check minimum message length
532             if (responseMessage.length < 6) {
533                 logger.debug("Unexpected message length: {}", responseMessage.length);
534                 throw new RotelException("Unexpected message length");
535             }
536
537             // Check START
538             if (responseMessage[0] != START) {
539                 logger.debug("Unexpected START in response: {} rather than {}",
540                         Integer.toHexString(responseMessage[0] & 0x000000FF), Integer.toHexString(START & 0x000000FF));
541                 throw new RotelException("Unexpected START in response");
542             }
543
544             // Check ID
545             if (responseMessage[2] != model.getDeviceId()) {
546                 logger.debug("Unexpected ID in response: {} rather than {}",
547                         Integer.toHexString(responseMessage[2] & 0x000000FF),
548                         Integer.toHexString(model.getDeviceId() & 0x000000FF));
549                 throw new RotelException("Unexpected ID in response");
550             }
551
552             // Check TYPE
553             if (responseMessage[3] != STANDARD_RESPONSE && responseMessage[3] != TRIGGER_STATUS
554                     && responseMessage[3] != SMART_DISPLAY_DATA_1 && responseMessage[3] != SMART_DISPLAY_DATA_2
555                     && responseMessage[3] != PRIMARY_CMD && responseMessage[3] != MAIN_ZONE_CMD
556                     && responseMessage[3] != RECORD_SRC_CMD && responseMessage[3] != ZONE2_CMD
557                     && responseMessage[3] != ZONE3_CMD && responseMessage[3] != ZONE4_CMD
558                     && responseMessage[3] != VOLUME_CMD && responseMessage[3] != ZONE2_VOLUME_CMD
559                     && responseMessage[3] != ZONE3_VOLUME_CMD && responseMessage[3] != ZONE4_VOLUME_CMD
560                     && responseMessage[3] != TRIGGER_CMD) {
561                 logger.debug("Unexpected TYPE in response: {}", Integer.toHexString(responseMessage[3] & 0x000000FF));
562                 throw new RotelException("Unexpected TYPE in response");
563             }
564
565             int expectedLen = (responseMessage[3] == STANDARD_RESPONSE)
566                     ? (5 + model.getRespNbChars() + model.getRespNbFlags())
567                     : responseMessage.length;
568
569             // Check COUNT
570             if (responseMessage[1] != (expectedLen - 3)) {
571                 logger.debug("Unexpected COUNT in response: {} rather than {}",
572                         Integer.toHexString(responseMessage[1] & 0x000000FF),
573                         Integer.toHexString((expectedLen - 3) & 0x000000FF));
574                 throw new RotelException("Unexpected COUNT in response");
575             }
576
577             final byte checksum = computeCheckSum(responseMessage, expectedLen - 2);
578             if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
579                 expectedLen++;
580             }
581
582             // Check message length
583             if (responseMessage.length != expectedLen) {
584                 logger.debug("Unexpected message length: {} rather than {}", responseMessage.length, expectedLen);
585                 throw new RotelException("Unexpected message length");
586             }
587
588             // Check sum
589             if ((checksum & 0x000000FF) == 0x000000FD) {
590                 if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
591                         || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 0) {
592                     logger.debug("Invalid check sum in response: {} rather than FD00", HexUtils.bytesToHex(
593                             Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
594                     throw new RotelException("Invalid check sum in response");
595                 }
596             } else if ((checksum & 0x000000FF) == 0x000000FE) {
597                 if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
598                         || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 1) {
599                     logger.debug("Invalid check sum in response: {} rather than FD01", HexUtils.bytesToHex(
600                             Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
601                     throw new RotelException("Invalid check sum in response");
602                 }
603             } else if ((checksum & 0x000000FF) != (responseMessage[responseMessage.length - 1] & 0x000000FF)) {
604                 logger.debug("Invalid check sum in response: {} rather than {}",
605                         Integer.toHexString(responseMessage[responseMessage.length - 1] & 0x000000FF),
606                         Integer.toHexString(checksum & 0x000000FF));
607                 throw new RotelException("Invalid check sum in response");
608             }
609         } else {
610             // Check minimum message length
611             if (responseMessage.length < 1) {
612                 logger.debug("Unexpected message length: {}", responseMessage.length);
613                 throw new RotelException("Unexpected message length");
614             }
615
616             if (responseMessage[responseMessage.length - 1] != '!'
617                     && responseMessage[responseMessage.length - 1] != '$') {
618                 logger.debug("Unexpected ending character in response: {}",
619                         Integer.toHexString(responseMessage[responseMessage.length - 1] & 0x000000FF));
620                 throw new RotelException("Unexpected ending character in response");
621             }
622         }
623     }
624
625     /**
626      * Compute the checksum of a message
627      *
628      * @param message the buffer containing the message
629      * @param maxIdx the position in the buffer at which the sum has to be stopped
630      *
631      * @return the checksum as a byte
632      */
633     protected byte computeCheckSum(byte[] message, int maxIdx) {
634         int result = 0;
635         for (int i = 1; i <= maxIdx; i++) {
636             result += (message[i] & 0x000000FF);
637         }
638         return (byte) (result & 0x000000FF);
639     }
640
641     /**
642      * Add a listener to the list of listeners to be notified with events
643      *
644      * @param listener the listener
645      */
646     public void addEventListener(RotelMessageEventListener listener) {
647         listeners.add(listener);
648     }
649
650     /**
651      * Remove a listener from the list of listeners to be notified with events
652      *
653      * @param listener the listener
654      */
655     public void removeEventListener(RotelMessageEventListener listener) {
656         listeners.remove(listener);
657     }
658
659     /**
660      * Analyze an incoming message and dispatch corresponding (key, value) to the event listeners
661      *
662      * @param incomingMessage the received message
663      */
664     public void handleIncomingMessage(byte[] incomingMessage) {
665         logger.debug("handleIncomingMessage: bytes {}", HexUtils.bytesToHex(incomingMessage));
666
667         if (READ_ERROR.equals(incomingMessage)) {
668             dispatchKeyValue(KEY_ERROR, MSG_VALUE_ON);
669             return;
670         }
671
672         try {
673             validateResponse(incomingMessage);
674         } catch (RotelException e) {
675             return;
676         }
677
678         if (protocol == RotelProtocol.HEX) {
679             handleValidHexMessage(incomingMessage);
680         } else {
681             handleValidAsciiMessage(incomingMessage);
682         }
683     }
684
685     /**
686      * Analyze a valid HEX message and dispatch corresponding (key, value) to the event listeners
687      *
688      * @param incomingMessage the received message
689      */
690     private void handleValidHexMessage(byte[] incomingMessage) {
691         if (incomingMessage[3] != STANDARD_RESPONSE) {
692             return;
693         }
694
695         final int idxChars = model.isCharsBeforeFlags() ? 4 : (4 + model.getRespNbFlags());
696
697         // Replace characters with code < 32 by a space before converting to a string
698         for (int i = idxChars; i < (idxChars + model.getRespNbChars()); i++) {
699             if (incomingMessage[i] < 0x20) {
700                 incomingMessage[i] = 0x20;
701             }
702         }
703
704         String value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
705         logger.debug("handleValidHexMessage: chars *{}*", value);
706
707         final int idxFlags = model.isCharsBeforeFlags() ? (4 + model.getRespNbChars()) : 4;
708         final byte[] flags = Arrays.copyOfRange(incomingMessage, idxFlags, idxFlags + model.getRespNbFlags());
709         if (logger.isTraceEnabled()) {
710             for (int i = 1; i <= flags.length; i++) {
711                 try {
712                     logger.trace("handleValidHexMessage: Flag {} = {} bits 7-0 = {} {} {} {} {} {} {} {}", i,
713                             Integer.toHexString(flags[i - 1] & 0x000000FF), RotelFlagsMapping.isBitFlagOn(flags, i, 7),
714                             RotelFlagsMapping.isBitFlagOn(flags, i, 6), RotelFlagsMapping.isBitFlagOn(flags, i, 5),
715                             RotelFlagsMapping.isBitFlagOn(flags, i, 4), RotelFlagsMapping.isBitFlagOn(flags, i, 3),
716                             RotelFlagsMapping.isBitFlagOn(flags, i, 2), RotelFlagsMapping.isBitFlagOn(flags, i, 1),
717                             RotelFlagsMapping.isBitFlagOn(flags, i, 0));
718                 } catch (RotelException e1) {
719                 }
720             }
721         }
722         try {
723             dispatchKeyValue(KEY_POWER_ZONE2, model.isZone2On(flags) ? POWER_ON : STANDBY);
724         } catch (RotelException e1) {
725         }
726         try {
727             dispatchKeyValue(KEY_POWER_ZONE3, model.isZone3On(flags) ? POWER_ON : STANDBY);
728         } catch (RotelException e1) {
729         }
730         try {
731             dispatchKeyValue(KEY_POWER_ZONE4, model.isZone4On(flags) ? POWER_ON : STANDBY);
732         } catch (RotelException e1) {
733         }
734         boolean checkMultiIn = false;
735         boolean checkSource = true;
736         try {
737             if (model.isMultiInputOn(flags)) {
738                 checkSource = false;
739                 try {
740                     RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
741                     RotelCommand cmd = source.getCommand();
742                     if (cmd != null) {
743                         String value2 = cmd.getAsciiCommandV2();
744                         if (value2 != null) {
745                             dispatchKeyValue(KEY_SOURCE, value2);
746                         }
747                     }
748                 } catch (RotelException e1) {
749                 }
750             }
751         } catch (RotelException e1) {
752             checkMultiIn = true;
753         }
754         boolean checkStereo = true;
755         try {
756             checkStereo = !model.isMoreThan2Channels(flags);
757         } catch (RotelException e1) {
758         }
759
760         String valueLowerCase = value.trim().toLowerCase();
761         if (!valueLowerCase.isEmpty() && !valueLowerCase.startsWith(KEY1_HEX_ZONE2)
762                 && !valueLowerCase.startsWith(KEY2_HEX_ZONE2) && !valueLowerCase.startsWith(KEY_HEX_ZONE3)
763                 && !valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
764             dispatchKeyValue(KEY_POWER, POWER_ON);
765         }
766
767         if (model.getRespNbChars() == 42) {
768             // 2 lines of 21 characters with a left part and a right part
769
770             // Line 1 left
771             value = new String(incomingMessage, idxChars, 14, StandardCharsets.US_ASCII);
772             logger.debug("handleValidHexMessage: line 1 left *{}*", value);
773             parseText(value, checkSource, checkMultiIn, false, false, false, false, false, true);
774
775             // Line 1 right
776             value = new String(incomingMessage, idxChars + 14, 7, StandardCharsets.US_ASCII);
777             logger.debug("handleValidHexMessage: line 1 right *{}*", value);
778             parseText(value, false, false, false, false, false, false, false, true);
779
780             // Full line 1
781             value = new String(incomingMessage, idxChars, 21, StandardCharsets.US_ASCII);
782             dispatchKeyValue(KEY_LINE1, value);
783
784             // Line 2 right
785             value = new String(incomingMessage, idxChars + 35, 7, StandardCharsets.US_ASCII);
786             logger.debug("handleValidHexMessage: line 2 right *{}*", value);
787             parseText(value, false, false, false, false, false, false, false, true);
788
789             // Full line 2
790             value = new String(incomingMessage, idxChars + 21, 21, StandardCharsets.US_ASCII);
791             logger.debug("handleValidHexMessage: line 2 *{}*", value);
792             parseText(value, false, false, true, true, false, true, true, true);
793             dispatchKeyValue(KEY_LINE2, value);
794         } else {
795             value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
796             parseText(value, checkSource, checkMultiIn, true, false, true, true, checkStereo, false);
797             dispatchKeyValue(KEY_LINE1, value);
798         }
799
800         if (valueLowerCase.isEmpty()) {
801             dispatchKeyValue(KEY_POWER, POWER_OFF_DELAYED);
802         }
803     }
804
805     /**
806      * Analyze a valid ASCII message and dispatch corresponding (key, value) to the event listeners
807      *
808      * @param incomingMessage the received message
809      */
810     public void handleValidAsciiMessage(byte[] incomingMessage) {
811         byte[] message = filterMessage(incomingMessage, model.getSpecialCharacters());
812
813         // Replace characters with code < 32 by a space before converting to a string
814         for (int i = 0; i < message.length; i++) {
815             if (message[i] < 0x20) {
816                 message[i] = 0x20;
817             }
818         }
819
820         String value = new String(message, 0, message.length - 1, StandardCharsets.US_ASCII);
821         logger.debug("handleValidAsciiMessage: chars *{}*", value);
822         value = value.trim();
823         if (value.isEmpty()) {
824             return;
825         }
826         try {
827             String[] splittedValue = value.split("=");
828             if (splittedValue.length != 2) {
829                 logger.debug("handleValidAsciiMessage: ignored message {}", value);
830             } else {
831                 dispatchKeyValue(splittedValue[0].trim().toLowerCase(), splittedValue[1]);
832             }
833         } catch (PatternSyntaxException e) {
834             logger.debug("handleValidAsciiMessage: ignored message {}", value);
835         }
836     }
837
838     /**
839      * Parse a text and dispatch appropriate (key, value) to the event listeners for found information
840      *
841      * @param text the text to be parsed
842      * @param searchSource true if a source has to be searched in the text
843      * @param searchMultiIn true if MULTI IN indication has to be searched in the text
844      * @param searchZone true if a zone information has to be searched in the text
845      * @param searchRecord true if a record source has to be searched in the text
846      * @param searchRecordAfterSource true if a record source has to be searched in the text after the a found source
847      * @param searchDsp true if a DSP mode has to be searched in the text
848      * @param searchStereo true if a STEREO has to be considered in the search
849      * @param multipleInfo true if source and volume/mute are provided separately
850      */
851     private void parseText(String text, boolean searchSource, boolean searchMultiIn, boolean searchZone,
852             boolean searchRecord, boolean searchRecordAfterSource, boolean searchDsp, boolean searchStereo,
853             boolean multipleInfo) {
854         String value = text.trim();
855         String valueLowerCase = value.toLowerCase();
856         if (searchRecord) {
857             dispatchKeyValue(KEY_RECORD_SEL, valueLowerCase.startsWith(KEY_HEX_RECORD) ? MSG_VALUE_ON : MSG_VALUE_OFF);
858         }
859         if (searchZone) {
860             if (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2)) {
861                 dispatchKeyValue(KEY_ZONE, "2");
862             } else if (valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
863                 dispatchKeyValue(KEY_ZONE, "3");
864             } else if (valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
865                 dispatchKeyValue(KEY_ZONE, "4");
866             } else {
867                 dispatchKeyValue(KEY_ZONE, "1");
868             }
869         }
870         if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
871             value = extractNumber(value,
872                     valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
873             dispatchKeyValue(KEY_VOLUME, value);
874             dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
875         } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
876             value = value.substring(KEY_HEX_MUTE.length()).trim();
877             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
878                 dispatchKeyValue(KEY_MUTE, MSG_VALUE_ON);
879             } else {
880                 logger.debug("Invalid value {} for zone mute", value);
881             }
882         } else if (valueLowerCase.startsWith(KEY1_HEX_BASS) || valueLowerCase.startsWith(KEY2_HEX_BASS)) {
883             value = extractNumber(value,
884                     valueLowerCase.startsWith(KEY1_HEX_BASS) ? KEY1_HEX_BASS.length() : KEY2_HEX_BASS.length());
885             dispatchKeyValue(KEY_BASS, value);
886         } else if (valueLowerCase.startsWith(KEY1_HEX_TREBLE) || valueLowerCase.startsWith(KEY2_HEX_TREBLE)) {
887             value = extractNumber(value,
888                     valueLowerCase.startsWith(KEY1_HEX_TREBLE) ? KEY1_HEX_TREBLE.length() : KEY2_HEX_TREBLE.length());
889             dispatchKeyValue(KEY_TREBLE, value);
890         } else if (searchMultiIn && valueLowerCase.startsWith(KEY_HEX_MULTI_IN)) {
891             value = value.substring(KEY_HEX_MULTI_IN.length()).trim();
892             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
893                 try {
894                     RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
895                     RotelCommand cmd = source.getCommand();
896                     if (cmd != null) {
897                         String value2 = cmd.getAsciiCommandV2();
898                         if (value2 != null) {
899                             dispatchKeyValue(KEY_SOURCE, value2);
900                         }
901                     }
902                 } catch (RotelException e1) {
903                 }
904             } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
905                 logger.debug("Invalid value {} for MULTI IN", value);
906             }
907         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_BYPASS)) {
908             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_BYPASS.getFeedback());
909         } else if (searchDsp && searchStereo && valueLowerCase.startsWith(KEY_HEX_STEREO)) {
910             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
911         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_3CH) || valueLowerCase.startsWith(KEY2_HEX_3CH))) {
912             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO3.getFeedback());
913         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_5CH)) {
914             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO5.getFeedback());
915         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_7CH)) {
916             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO7.getFeedback());
917         } else if (searchDsp
918                 && (valueLowerCase.startsWith(KEY_HEX_MUSIC1) || valueLowerCase.startsWith(KEY_HEX_DSP1))) {
919             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP1.getFeedback());
920         } else if (searchDsp
921                 && (valueLowerCase.startsWith(KEY_HEX_MUSIC2) || valueLowerCase.startsWith(KEY_HEX_DSP2))) {
922             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP2.getFeedback());
923         } else if (searchDsp
924                 && (valueLowerCase.startsWith(KEY_HEX_MUSIC3) || valueLowerCase.startsWith(KEY_HEX_DSP3))) {
925             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP3.getFeedback());
926         } else if (searchDsp
927                 && (valueLowerCase.startsWith(KEY_HEX_MUSIC4) || valueLowerCase.startsWith(KEY_HEX_DSP4))) {
928             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP4.getFeedback());
929         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_CINEMA)
930                 || valueLowerCase.startsWith(KEY2_HEX_PLII_CINEMA) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_CINEMA)
931                 || searchDsp && valueLowerCase.startsWith(KEY2_HEX_PLIIX_CINEMA))) {
932             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_CINEMA.getFeedback());
933         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_MUSIC)
934                 || valueLowerCase.startsWith(KEY2_HEX_PLII_MUSIC) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_MUSIC)
935                 || valueLowerCase.startsWith(KEY2_HEX_PLIIX_MUSIC))) {
936             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_MUSIC.getFeedback());
937         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_GAME)
938                 || valueLowerCase.startsWith(KEY2_HEX_PLII_GAME) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_GAME)
939                 || valueLowerCase.startsWith(KEY2_HEX_PLIIX_GAME))) {
940             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_GAME.getFeedback());
941         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PLIIZ)) {
942             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PLIIZ.getFeedback());
943         } else if (searchDsp
944                 && (valueLowerCase.startsWith(KEY1_HEX_PROLOGIC) || valueLowerCase.startsWith(KEY2_HEX_PROLOGIC))) {
945             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PROLOGIC.getFeedback());
946         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_CINEMA)
947                 || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_CINEMA))) {
948             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_CINEMA.getFeedback());
949         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_MUSIC)
950                 || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_MUSIC))) {
951             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_MUSIC.getFeedback());
952         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_ES)) {
953             logger.debug("DTS-ES");
954             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
955         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_96)) {
956             logger.debug("DTS 96");
957             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
958         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS)) {
959             logger.debug("DTS");
960             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
961         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD_EX)) {
962             logger.debug("DD-EX");
963             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
964         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD)) {
965             logger.debug("DD");
966             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
967         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_LPCM)) {
968             logger.debug("LPCM");
969             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
970         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PCM)) {
971             logger.debug("PCM");
972             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
973         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_MPEG)) {
974             logger.debug("MPEG");
975             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
976         } else if (searchZone
977                 && (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2))) {
978             value = value.substring(
979                     valueLowerCase.startsWith(KEY1_HEX_ZONE2) ? KEY1_HEX_ZONE2.length() : KEY2_HEX_ZONE2.length());
980             parseZone2(value, multipleInfo);
981         } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
982             parseZone3(value.substring(KEY_HEX_ZONE3.length()), multipleInfo);
983         } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
984             parseZone4(value.substring(KEY_HEX_ZONE4.length()), multipleInfo);
985         } else if (searchRecord && valueLowerCase.startsWith(KEY_HEX_RECORD)) {
986             parseRecord(value.substring(KEY_HEX_RECORD.length()));
987         } else if (searchSource || searchRecordAfterSource) {
988             parseSourceAndRecord(value, searchSource, searchRecordAfterSource, multipleInfo);
989         }
990     }
991
992     /**
993      * Parse a text to identify a source
994      *
995      * @param text the text to be parsed
996      * @param acceptFollowMain true if follow main has to be considered in the search
997      *
998      * @return the identified source or null if no source is identified in the text
999      */
1000     private @Nullable RotelSource parseSource(String text, boolean acceptFollowMain) {
1001         String value = text.trim();
1002         RotelSource source = null;
1003         if (!value.isEmpty()) {
1004             if (acceptFollowMain && SOURCE.equalsIgnoreCase(value)) {
1005                 try {
1006                     source = model.getSourceFromName(RotelSource.CAT1_FOLLOW_MAIN.getName());
1007                 } catch (RotelException e) {
1008                 }
1009             } else {
1010                 for (RotelSource src : sourcesLabels.keySet()) {
1011                     String label = sourcesLabels.get(src);
1012                     if (label != null && value.startsWith(label)) {
1013                         if (source == null || sourcesLabels.get(source).length() < label.length()) {
1014                             source = src;
1015                         }
1016                     }
1017                 }
1018             }
1019         }
1020         return source;
1021     }
1022
1023     private void parseSourceAndRecord(String text, boolean searchSource, boolean searchRecordAfterSource,
1024             boolean multipleInfo) {
1025         RotelSource source = parseSource(text, false);
1026         if (source != null) {
1027             if (searchSource) {
1028                 RotelCommand cmd = source.getCommand();
1029                 if (cmd != null) {
1030                     String value2 = cmd.getAsciiCommandV2();
1031                     if (value2 != null) {
1032                         dispatchKeyValue(KEY_SOURCE, value2);
1033                         if (!multipleInfo) {
1034                             dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
1035                         }
1036                     }
1037                 }
1038             }
1039
1040             if (searchRecordAfterSource) {
1041                 String value = text.substring(getSourceLabel(source).length()).trim();
1042                 source = parseSource(value, true);
1043                 if (source != null) {
1044                     RotelCommand cmd = source.getRecordCommand();
1045                     if (cmd != null) {
1046                         value = cmd.getAsciiCommandV2();
1047                         if (value != null) {
1048                             dispatchKeyValue(KEY_RECORD, value);
1049                         }
1050                     }
1051                 }
1052             }
1053         }
1054     }
1055
1056     private String getSourceLabel(RotelSource source) {
1057         String label = sourcesLabels.get(source);
1058         return (label == null) ? source.getLabel() : label;
1059     }
1060
1061     private void parseRecord(String text) {
1062         String value = text.trim();
1063         RotelSource source = parseSource(value, true);
1064         if (source != null) {
1065             RotelCommand cmd = source.getRecordCommand();
1066             if (cmd != null) {
1067                 value = cmd.getAsciiCommandV2();
1068                 if (value != null) {
1069                     dispatchKeyValue(KEY_RECORD, value);
1070                 }
1071             }
1072         } else {
1073             logger.debug("Invalid value {} for record source", value);
1074         }
1075     }
1076
1077     private void parseZone2(String text, boolean multipleInfo) {
1078         String value = text.trim();
1079         String valueLowerCase = value.toLowerCase();
1080         if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
1081             value = extractNumber(value,
1082                     valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
1083             dispatchKeyValue(KEY_VOLUME_ZONE2, value);
1084             dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
1085         } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
1086             value = value.substring(KEY_HEX_MUTE.length()).trim();
1087             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1088                 dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_ON);
1089             } else {
1090                 logger.debug("Invalid value {} for zone mute", value);
1091             }
1092         } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1093             RotelSource source = parseSource(value, true);
1094             if (source != null) {
1095                 RotelCommand cmd = source.getZone2Command();
1096                 if (cmd != null) {
1097                     value = cmd.getAsciiCommandV2();
1098                     if (value != null) {
1099                         dispatchKeyValue(KEY_SOURCE_ZONE2, value);
1100                         if (!multipleInfo) {
1101                             dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
1102                         }
1103                     }
1104                 }
1105             } else {
1106                 logger.debug("Invalid value {} for zone 2 source", value);
1107             }
1108         }
1109     }
1110
1111     private void parseZone3(String text, boolean multipleInfo) {
1112         String value = text.trim();
1113         String valueLowerCase = value.toLowerCase();
1114         if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
1115             value = extractNumber(value,
1116                     valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
1117             dispatchKeyValue(KEY_VOLUME_ZONE3, value);
1118             dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
1119         } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
1120             value = value.substring(KEY_HEX_MUTE.length()).trim();
1121             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1122                 dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_ON);
1123             } else {
1124                 logger.debug("Invalid value {} for zone mute", value);
1125             }
1126         } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1127             RotelSource source = parseSource(value, true);
1128             if (source != null) {
1129                 RotelCommand cmd = source.getZone3Command();
1130                 if (cmd != null) {
1131                     value = cmd.getAsciiCommandV2();
1132                     if (value != null) {
1133                         dispatchKeyValue(KEY_SOURCE_ZONE3, value);
1134                         if (!multipleInfo) {
1135                             dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
1136                         }
1137                     }
1138                 }
1139             } else {
1140                 logger.debug("Invalid value {} for zone 3 source", value);
1141             }
1142         }
1143     }
1144
1145     private void parseZone4(String text, boolean multipleInfo) {
1146         String value = text.trim();
1147         String valueLowerCase = value.toLowerCase();
1148         if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
1149             value = extractNumber(value,
1150                     valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
1151             dispatchKeyValue(KEY_VOLUME_ZONE4, value);
1152             dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
1153         } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
1154             value = value.substring(KEY_HEX_MUTE.length()).trim();
1155             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1156                 dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_ON);
1157             } else {
1158                 logger.debug("Invalid value {} for zone mute", value);
1159             }
1160         } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1161             RotelSource source = parseSource(value, true);
1162             if (source != null) {
1163                 RotelCommand cmd = source.getZone4Command();
1164                 if (cmd != null) {
1165                     value = cmd.getAsciiCommandV2();
1166                     if (value != null) {
1167                         dispatchKeyValue(KEY_SOURCE_ZONE4, value);
1168                         if (!multipleInfo) {
1169                             dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
1170                         }
1171                     }
1172                 }
1173             } else {
1174                 logger.debug("Invalid value {} for zone 4 source", value);
1175             }
1176         }
1177     }
1178
1179     /**
1180      * Extract from a string a number
1181      *
1182      * @param value the string
1183      * @param startIndex the index in the string at which the integer has to be extracted
1184      *
1185      * @return the number as a string with its sign and no blank between the sign and the digits
1186      */
1187     private String extractNumber(String value, int startIndex) {
1188         String result = value.substring(startIndex).trim();
1189         // Delete possible blank(s) between the sign and the number
1190         if (result.startsWith("+") || result.startsWith("-")) {
1191             result = result.substring(0, 1) + result.substring(1, result.length()).trim();
1192         }
1193         return result;
1194     }
1195
1196     /**
1197      * Suppress certain sequences of bytes from a message
1198      *
1199      * @param message the message as a table of bytes
1200      * @param bytesSequences the table containing the sequence of bytes to be ignored
1201      *
1202      * @return the message without the unexpected sequence of bytes
1203      */
1204     private byte[] filterMessage(byte[] message, byte[][] bytesSequences) {
1205         if (bytesSequences.length == 0) {
1206             return message;
1207         }
1208         byte[] filteredMsg = new byte[message.length];
1209         int srcIdx = 0;
1210         int dstIdx = 0;
1211         while (srcIdx < message.length) {
1212             int ignoredLength = 0;
1213             for (int i = 0; i < bytesSequences.length; i++) {
1214                 int size = bytesSequences[i].length;
1215                 if ((message.length - srcIdx) >= size) {
1216                     boolean match = true;
1217                     for (int j = 0; j < size; j++) {
1218                         if (message[srcIdx + j] != bytesSequences[i][j]) {
1219                             match = false;
1220                             break;
1221                         }
1222                     }
1223                     if (match) {
1224                         ignoredLength = size;
1225                         break;
1226                     }
1227                 }
1228             }
1229             if (ignoredLength > 0) {
1230                 srcIdx += ignoredLength;
1231             } else {
1232                 filteredMsg[dstIdx++] = message[srcIdx++];
1233             }
1234         }
1235         return Arrays.copyOf(filteredMsg, dstIdx);
1236     }
1237
1238     /**
1239      * Dispatch an event (key, value) to the event listeners
1240      *
1241      * @param key the key
1242      * @param value the value
1243      */
1244     private void dispatchKeyValue(String key, String value) {
1245         RotelMessageEvent event = new RotelMessageEvent(this, key, value);
1246         for (int i = 0; i < listeners.size(); i++) {
1247             listeners.get(i).onNewMessageEvent(event);
1248         }
1249     }
1250 }