]> git.basschouten.com Git - openhab-addons.git/blob
fec2effc2719c0cb8bfaae67b8d3cf4aac5222be
[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 DIMMER_LEVEL_SET:
430                                 if (value > 0 && model.getDimmerLevelMin() < 0) {
431                                     messageStr += String.format("+%d", value);
432                                 } else {
433                                     messageStr += String.format("%d", value);
434                                 }
435                                 break;
436                             default:
437                                 break;
438                         }
439                     }
440                     if (!messageStr.endsWith("?")) {
441                         messageStr += "!";
442                     }
443                     message = messageStr.getBytes(StandardCharsets.US_ASCII);
444                     logger.debug("Send command \"{}\" => {}", cmd.getName(), messageStr);
445                 }
446                 break;
447             case ASCII_V2:
448                 messageStr = cmd.getAsciiCommandV2();
449                 if (messageStr == null) {
450                     logger.debug("Send comman \"{}\" ignored: not available for ASCII V2 protocol", cmd.getName());
451                     return;
452                 } else {
453                     if (value != null) {
454                         switch (cmd) {
455                             case VOLUME_SET:
456                                 messageStr += String.format("%02d", value);
457                                 break;
458                             case BASS_SET:
459                             case TREBLE_SET:
460                                 if (value == 0) {
461                                     messageStr += "000";
462                                 } else if (value > 0) {
463                                     messageStr += String.format("+%02d", value);
464                                 } else {
465                                     messageStr += String.format("-%02d", -value);
466                                 }
467                                 break;
468                             case BALANCE_SET:
469                                 if (value == 0) {
470                                     messageStr += "000";
471                                 } else if (value > 0) {
472                                     messageStr += String.format("R%02d", value);
473                                 } else {
474                                     messageStr += String.format("L%02d", -value);
475                                 }
476                                 break;
477                             case BALANCE_SET_FIX:
478                                 // Firmware for models A1x does not follow strictly the Rotel specification
479                                 // The firmware expects values like r05 or l04 while the specification mentions
480                                 // R05 and L04
481                                 if (value == 0) {
482                                     messageStr += "000";
483                                 } else if (value > 0) {
484                                     messageStr += String.format("r%02d", value);
485                                 } else {
486                                     messageStr += String.format("l%02d", -value);
487                                 }
488                                 break;
489                             case DIMMER_LEVEL_SET:
490                                 if (value > 0 && model.getDimmerLevelMin() < 0) {
491                                     messageStr += String.format("+%d", value);
492                                 } else {
493                                     messageStr += String.format("%d", value);
494                                 }
495                                 break;
496                             default:
497                                 break;
498                         }
499                     }
500                     if (!messageStr.endsWith("?")) {
501                         messageStr += "!";
502                     }
503                     message = messageStr.getBytes(StandardCharsets.US_ASCII);
504                     logger.debug("Send command \"{}\" => {}", cmd.getName(), messageStr);
505                 }
506                 break;
507         }
508         if (simu) {
509             return;
510         }
511         OutputStream dataOut = this.dataOut;
512         if (dataOut == null) {
513             throw new RotelException("Send command \"" + cmd.getName() + "\" failed: output stream is null");
514         }
515         try {
516             dataOut.write(message);
517             dataOut.flush();
518         } catch (IOException e) {
519             logger.debug("Send command \"{}\" failed: {}", cmd.getName(), e.getMessage());
520             throw new RotelException("Send command \"" + cmd.getName() + "\" failed", e);
521         }
522         logger.debug("Send command \"{}\" succeeded", cmd.getName());
523     }
524
525     /**
526      * Validate the content of a feedback message
527      *
528      * @param responseMessage the buffer containing the feedback message
529      *
530      * @throws RotelException - If the message has unexpected content
531      */
532     private void validateResponse(byte[] responseMessage) throws RotelException {
533         if (protocol == RotelProtocol.HEX) {
534             // Check minimum message length
535             if (responseMessage.length < 6) {
536                 logger.debug("Unexpected message length: {}", responseMessage.length);
537                 throw new RotelException("Unexpected message length");
538             }
539
540             // Check START
541             if (responseMessage[0] != START) {
542                 logger.debug("Unexpected START in response: {} rather than {}",
543                         Integer.toHexString(responseMessage[0] & 0x000000FF), Integer.toHexString(START & 0x000000FF));
544                 throw new RotelException("Unexpected START in response");
545             }
546
547             // Check ID
548             if (responseMessage[2] != model.getDeviceId()) {
549                 logger.debug("Unexpected ID in response: {} rather than {}",
550                         Integer.toHexString(responseMessage[2] & 0x000000FF),
551                         Integer.toHexString(model.getDeviceId() & 0x000000FF));
552                 throw new RotelException("Unexpected ID in response");
553             }
554
555             // Check TYPE
556             if (responseMessage[3] != STANDARD_RESPONSE && responseMessage[3] != TRIGGER_STATUS
557                     && responseMessage[3] != SMART_DISPLAY_DATA_1 && responseMessage[3] != SMART_DISPLAY_DATA_2
558                     && responseMessage[3] != PRIMARY_CMD && responseMessage[3] != MAIN_ZONE_CMD
559                     && responseMessage[3] != RECORD_SRC_CMD && responseMessage[3] != ZONE2_CMD
560                     && responseMessage[3] != ZONE3_CMD && responseMessage[3] != ZONE4_CMD
561                     && responseMessage[3] != VOLUME_CMD && responseMessage[3] != ZONE2_VOLUME_CMD
562                     && responseMessage[3] != ZONE3_VOLUME_CMD && responseMessage[3] != ZONE4_VOLUME_CMD
563                     && responseMessage[3] != TRIGGER_CMD) {
564                 logger.debug("Unexpected TYPE in response: {}", Integer.toHexString(responseMessage[3] & 0x000000FF));
565                 throw new RotelException("Unexpected TYPE in response");
566             }
567
568             int expectedLen = (responseMessage[3] == STANDARD_RESPONSE)
569                     ? (5 + model.getRespNbChars() + model.getRespNbFlags())
570                     : responseMessage.length;
571
572             // Check COUNT
573             if (responseMessage[1] != (expectedLen - 3)) {
574                 logger.debug("Unexpected COUNT in response: {} rather than {}",
575                         Integer.toHexString(responseMessage[1] & 0x000000FF),
576                         Integer.toHexString((expectedLen - 3) & 0x000000FF));
577                 throw new RotelException("Unexpected COUNT in response");
578             }
579
580             final byte checksum = computeCheckSum(responseMessage, expectedLen - 2);
581             if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
582                 expectedLen++;
583             }
584
585             // Check message length
586             if (responseMessage.length != expectedLen) {
587                 logger.debug("Unexpected message length: {} rather than {}", responseMessage.length, expectedLen);
588                 throw new RotelException("Unexpected message length");
589             }
590
591             // Check sum
592             if ((checksum & 0x000000FF) == 0x000000FD) {
593                 if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
594                         || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 0) {
595                     logger.debug("Invalid check sum in response: {} rather than FD00", HexUtils.bytesToHex(
596                             Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
597                     throw new RotelException("Invalid check sum in response");
598                 }
599             } else if ((checksum & 0x000000FF) == 0x000000FE) {
600                 if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
601                         || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 1) {
602                     logger.debug("Invalid check sum in response: {} rather than FD01", HexUtils.bytesToHex(
603                             Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
604                     throw new RotelException("Invalid check sum in response");
605                 }
606             } else if ((checksum & 0x000000FF) != (responseMessage[responseMessage.length - 1] & 0x000000FF)) {
607                 logger.debug("Invalid check sum in response: {} rather than {}",
608                         Integer.toHexString(responseMessage[responseMessage.length - 1] & 0x000000FF),
609                         Integer.toHexString(checksum & 0x000000FF));
610                 throw new RotelException("Invalid check sum in response");
611             }
612         } else {
613             // Check minimum message length
614             if (responseMessage.length < 1) {
615                 logger.debug("Unexpected message length: {}", responseMessage.length);
616                 throw new RotelException("Unexpected message length");
617             }
618
619             if (responseMessage[responseMessage.length - 1] != '!'
620                     && responseMessage[responseMessage.length - 1] != '$') {
621                 logger.debug("Unexpected ending character in response: {}",
622                         Integer.toHexString(responseMessage[responseMessage.length - 1] & 0x000000FF));
623                 throw new RotelException("Unexpected ending character in response");
624             }
625         }
626     }
627
628     /**
629      * Compute the checksum of a message
630      *
631      * @param message the buffer containing the message
632      * @param maxIdx the position in the buffer at which the sum has to be stopped
633      *
634      * @return the checksum as a byte
635      */
636     protected byte computeCheckSum(byte[] message, int maxIdx) {
637         int result = 0;
638         for (int i = 1; i <= maxIdx; i++) {
639             result += (message[i] & 0x000000FF);
640         }
641         return (byte) (result & 0x000000FF);
642     }
643
644     /**
645      * Add a listener to the list of listeners to be notified with events
646      *
647      * @param listener the listener
648      */
649     public void addEventListener(RotelMessageEventListener listener) {
650         listeners.add(listener);
651     }
652
653     /**
654      * Remove a listener from the list of listeners to be notified with events
655      *
656      * @param listener the listener
657      */
658     public void removeEventListener(RotelMessageEventListener listener) {
659         listeners.remove(listener);
660     }
661
662     /**
663      * Analyze an incoming message and dispatch corresponding (key, value) to the event listeners
664      *
665      * @param incomingMessage the received message
666      */
667     public void handleIncomingMessage(byte[] incomingMessage) {
668         logger.debug("handleIncomingMessage: bytes {}", HexUtils.bytesToHex(incomingMessage));
669
670         if (READ_ERROR.equals(incomingMessage)) {
671             dispatchKeyValue(KEY_ERROR, MSG_VALUE_ON);
672             return;
673         }
674
675         try {
676             validateResponse(incomingMessage);
677         } catch (RotelException e) {
678             return;
679         }
680
681         if (protocol == RotelProtocol.HEX) {
682             handleValidHexMessage(incomingMessage);
683         } else {
684             handleValidAsciiMessage(incomingMessage);
685         }
686     }
687
688     /**
689      * Analyze a valid HEX message and dispatch corresponding (key, value) to the event listeners
690      *
691      * @param incomingMessage the received message
692      */
693     private void handleValidHexMessage(byte[] incomingMessage) {
694         if (incomingMessage[3] != STANDARD_RESPONSE) {
695             return;
696         }
697
698         final int idxChars = model.isCharsBeforeFlags() ? 4 : (4 + model.getRespNbFlags());
699
700         // Replace characters with code < 32 by a space before converting to a string
701         for (int i = idxChars; i < (idxChars + model.getRespNbChars()); i++) {
702             if (incomingMessage[i] < 0x20) {
703                 incomingMessage[i] = 0x20;
704             }
705         }
706
707         String value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
708         logger.debug("handleValidHexMessage: chars *{}*", value);
709
710         final int idxFlags = model.isCharsBeforeFlags() ? (4 + model.getRespNbChars()) : 4;
711         final byte[] flags = Arrays.copyOfRange(incomingMessage, idxFlags, idxFlags + model.getRespNbFlags());
712         if (logger.isTraceEnabled()) {
713             for (int i = 1; i <= flags.length; i++) {
714                 try {
715                     logger.trace("handleValidHexMessage: Flag {} = {} bits 7-0 = {} {} {} {} {} {} {} {}", i,
716                             Integer.toHexString(flags[i - 1] & 0x000000FF), RotelFlagsMapping.isBitFlagOn(flags, i, 7),
717                             RotelFlagsMapping.isBitFlagOn(flags, i, 6), RotelFlagsMapping.isBitFlagOn(flags, i, 5),
718                             RotelFlagsMapping.isBitFlagOn(flags, i, 4), RotelFlagsMapping.isBitFlagOn(flags, i, 3),
719                             RotelFlagsMapping.isBitFlagOn(flags, i, 2), RotelFlagsMapping.isBitFlagOn(flags, i, 1),
720                             RotelFlagsMapping.isBitFlagOn(flags, i, 0));
721                 } catch (RotelException e1) {
722                 }
723             }
724         }
725         try {
726             dispatchKeyValue(KEY_POWER_ZONE2, model.isZone2On(flags) ? POWER_ON : STANDBY);
727         } catch (RotelException e1) {
728         }
729         try {
730             dispatchKeyValue(KEY_POWER_ZONE3, model.isZone3On(flags) ? POWER_ON : STANDBY);
731         } catch (RotelException e1) {
732         }
733         try {
734             dispatchKeyValue(KEY_POWER_ZONE4, model.isZone4On(flags) ? POWER_ON : STANDBY);
735         } catch (RotelException e1) {
736         }
737         boolean checkMultiIn = false;
738         boolean checkSource = true;
739         try {
740             if (model.isMultiInputOn(flags)) {
741                 checkSource = false;
742                 try {
743                     RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
744                     RotelCommand cmd = source.getCommand();
745                     if (cmd != null) {
746                         String value2 = cmd.getAsciiCommandV2();
747                         if (value2 != null) {
748                             dispatchKeyValue(KEY_SOURCE, value2);
749                         }
750                     }
751                 } catch (RotelException e1) {
752                 }
753             }
754         } catch (RotelException e1) {
755             checkMultiIn = true;
756         }
757         boolean checkStereo = true;
758         try {
759             checkStereo = !model.isMoreThan2Channels(flags);
760         } catch (RotelException e1) {
761         }
762
763         String valueLowerCase = value.trim().toLowerCase();
764         if (!valueLowerCase.isEmpty() && !valueLowerCase.startsWith(KEY1_HEX_ZONE2)
765                 && !valueLowerCase.startsWith(KEY2_HEX_ZONE2) && !valueLowerCase.startsWith(KEY_HEX_ZONE3)
766                 && !valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
767             dispatchKeyValue(KEY_POWER, POWER_ON);
768         }
769
770         if (model.getRespNbChars() == 42) {
771             // 2 lines of 21 characters with a left part and a right part
772
773             // Line 1 left
774             value = new String(incomingMessage, idxChars, 14, StandardCharsets.US_ASCII);
775             logger.debug("handleValidHexMessage: line 1 left *{}*", value);
776             parseText(value, checkSource, checkMultiIn, false, false, false, false, false, true);
777
778             // Line 1 right
779             value = new String(incomingMessage, idxChars + 14, 7, StandardCharsets.US_ASCII);
780             logger.debug("handleValidHexMessage: line 1 right *{}*", value);
781             parseText(value, false, false, false, false, false, false, false, true);
782
783             // Full line 1
784             value = new String(incomingMessage, idxChars, 21, StandardCharsets.US_ASCII);
785             dispatchKeyValue(KEY_LINE1, value);
786
787             // Line 2 right
788             value = new String(incomingMessage, idxChars + 35, 7, StandardCharsets.US_ASCII);
789             logger.debug("handleValidHexMessage: line 2 right *{}*", value);
790             parseText(value, false, false, false, false, false, false, false, true);
791
792             // Full line 2
793             value = new String(incomingMessage, idxChars + 21, 21, StandardCharsets.US_ASCII);
794             logger.debug("handleValidHexMessage: line 2 *{}*", value);
795             parseText(value, false, false, true, true, false, true, true, true);
796             dispatchKeyValue(KEY_LINE2, value);
797         } else {
798             value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
799             parseText(value, checkSource, checkMultiIn, true, false, true, true, checkStereo, false);
800             dispatchKeyValue(KEY_LINE1, value);
801         }
802
803         if (valueLowerCase.isEmpty()) {
804             dispatchKeyValue(KEY_POWER, POWER_OFF_DELAYED);
805         }
806     }
807
808     /**
809      * Analyze a valid ASCII message and dispatch corresponding (key, value) to the event listeners
810      *
811      * @param incomingMessage the received message
812      */
813     public void handleValidAsciiMessage(byte[] incomingMessage) {
814         byte[] message = filterMessage(incomingMessage, model.getSpecialCharacters());
815
816         // Replace characters with code < 32 by a space before converting to a string
817         for (int i = 0; i < message.length; i++) {
818             if (message[i] < 0x20) {
819                 message[i] = 0x20;
820             }
821         }
822
823         String value = new String(message, 0, message.length - 1, StandardCharsets.US_ASCII);
824         logger.debug("handleValidAsciiMessage: chars *{}*", value);
825         value = value.trim();
826         if (value.isEmpty()) {
827             return;
828         }
829         try {
830             String[] splittedValue = value.split("=");
831             if (splittedValue.length != 2) {
832                 logger.debug("handleValidAsciiMessage: ignored message {}", value);
833             } else {
834                 dispatchKeyValue(splittedValue[0].trim().toLowerCase(), splittedValue[1]);
835             }
836         } catch (PatternSyntaxException e) {
837             logger.debug("handleValidAsciiMessage: ignored message {}", value);
838         }
839     }
840
841     /**
842      * Parse a text and dispatch appropriate (key, value) to the event listeners for found information
843      *
844      * @param text the text to be parsed
845      * @param searchSource true if a source has to be searched in the text
846      * @param searchMultiIn true if MULTI IN indication has to be searched in the text
847      * @param searchZone true if a zone information has to be searched in the text
848      * @param searchRecord true if a record source has to be searched in the text
849      * @param searchRecordAfterSource true if a record source has to be searched in the text after the a found source
850      * @param searchDsp true if a DSP mode has to be searched in the text
851      * @param searchStereo true if a STEREO has to be considered in the search
852      * @param multipleInfo true if source and volume/mute are provided separately
853      */
854     private void parseText(String text, boolean searchSource, boolean searchMultiIn, boolean searchZone,
855             boolean searchRecord, boolean searchRecordAfterSource, boolean searchDsp, boolean searchStereo,
856             boolean multipleInfo) {
857         String value = text.trim();
858         String valueLowerCase = value.toLowerCase();
859         if (searchRecord) {
860             dispatchKeyValue(KEY_RECORD_SEL, valueLowerCase.startsWith(KEY_HEX_RECORD) ? MSG_VALUE_ON : MSG_VALUE_OFF);
861         }
862         if (searchZone) {
863             if (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2)) {
864                 dispatchKeyValue(KEY_ZONE, "2");
865             } else if (valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
866                 dispatchKeyValue(KEY_ZONE, "3");
867             } else if (valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
868                 dispatchKeyValue(KEY_ZONE, "4");
869             } else {
870                 dispatchKeyValue(KEY_ZONE, "1");
871             }
872         }
873         if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
874             value = extractNumber(value,
875                     valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
876             dispatchKeyValue(KEY_VOLUME, value);
877             dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
878         } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
879             value = value.substring(KEY_HEX_MUTE.length()).trim();
880             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
881                 dispatchKeyValue(KEY_MUTE, MSG_VALUE_ON);
882             } else {
883                 logger.debug("Invalid value {} for zone mute", value);
884             }
885         } else if (valueLowerCase.startsWith(KEY1_HEX_BASS) || valueLowerCase.startsWith(KEY2_HEX_BASS)) {
886             value = extractNumber(value,
887                     valueLowerCase.startsWith(KEY1_HEX_BASS) ? KEY1_HEX_BASS.length() : KEY2_HEX_BASS.length());
888             dispatchKeyValue(KEY_BASS, value);
889         } else if (valueLowerCase.startsWith(KEY1_HEX_TREBLE) || valueLowerCase.startsWith(KEY2_HEX_TREBLE)) {
890             value = extractNumber(value,
891                     valueLowerCase.startsWith(KEY1_HEX_TREBLE) ? KEY1_HEX_TREBLE.length() : KEY2_HEX_TREBLE.length());
892             dispatchKeyValue(KEY_TREBLE, value);
893         } else if (searchMultiIn && valueLowerCase.startsWith(KEY_HEX_MULTI_IN)) {
894             value = value.substring(KEY_HEX_MULTI_IN.length()).trim();
895             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
896                 try {
897                     RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
898                     RotelCommand cmd = source.getCommand();
899                     if (cmd != null) {
900                         String value2 = cmd.getAsciiCommandV2();
901                         if (value2 != null) {
902                             dispatchKeyValue(KEY_SOURCE, value2);
903                         }
904                     }
905                 } catch (RotelException e1) {
906                 }
907             } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
908                 logger.debug("Invalid value {} for MULTI IN", value);
909             }
910         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_BYPASS)) {
911             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_BYPASS.getFeedback());
912         } else if (searchDsp && searchStereo && valueLowerCase.startsWith(KEY_HEX_STEREO)) {
913             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
914         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_3CH) || valueLowerCase.startsWith(KEY2_HEX_3CH))) {
915             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO3.getFeedback());
916         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_5CH)) {
917             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO5.getFeedback());
918         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_7CH)) {
919             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO7.getFeedback());
920         } else if (searchDsp
921                 && (valueLowerCase.startsWith(KEY_HEX_MUSIC1) || valueLowerCase.startsWith(KEY_HEX_DSP1))) {
922             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP1.getFeedback());
923         } else if (searchDsp
924                 && (valueLowerCase.startsWith(KEY_HEX_MUSIC2) || valueLowerCase.startsWith(KEY_HEX_DSP2))) {
925             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP2.getFeedback());
926         } else if (searchDsp
927                 && (valueLowerCase.startsWith(KEY_HEX_MUSIC3) || valueLowerCase.startsWith(KEY_HEX_DSP3))) {
928             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP3.getFeedback());
929         } else if (searchDsp
930                 && (valueLowerCase.startsWith(KEY_HEX_MUSIC4) || valueLowerCase.startsWith(KEY_HEX_DSP4))) {
931             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP4.getFeedback());
932         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_CINEMA)
933                 || valueLowerCase.startsWith(KEY2_HEX_PLII_CINEMA) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_CINEMA)
934                 || searchDsp && valueLowerCase.startsWith(KEY2_HEX_PLIIX_CINEMA))) {
935             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_CINEMA.getFeedback());
936         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_MUSIC)
937                 || valueLowerCase.startsWith(KEY2_HEX_PLII_MUSIC) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_MUSIC)
938                 || valueLowerCase.startsWith(KEY2_HEX_PLIIX_MUSIC))) {
939             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_MUSIC.getFeedback());
940         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_GAME)
941                 || valueLowerCase.startsWith(KEY2_HEX_PLII_GAME) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_GAME)
942                 || valueLowerCase.startsWith(KEY2_HEX_PLIIX_GAME))) {
943             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_GAME.getFeedback());
944         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PLIIZ)) {
945             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PLIIZ.getFeedback());
946         } else if (searchDsp
947                 && (valueLowerCase.startsWith(KEY1_HEX_PROLOGIC) || valueLowerCase.startsWith(KEY2_HEX_PROLOGIC))) {
948             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PROLOGIC.getFeedback());
949         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_CINEMA)
950                 || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_CINEMA))) {
951             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_CINEMA.getFeedback());
952         } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_MUSIC)
953                 || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_MUSIC))) {
954             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_MUSIC.getFeedback());
955         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_ES)) {
956             logger.debug("DTS-ES");
957             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
958         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_96)) {
959             logger.debug("DTS 96");
960             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
961         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS)) {
962             logger.debug("DTS");
963             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
964         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD_EX)) {
965             logger.debug("DD-EX");
966             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
967         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD)) {
968             logger.debug("DD");
969             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
970         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_LPCM)) {
971             logger.debug("LPCM");
972             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
973         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PCM)) {
974             logger.debug("PCM");
975             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
976         } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_MPEG)) {
977             logger.debug("MPEG");
978             dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
979         } else if (searchZone
980                 && (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2))) {
981             value = value.substring(
982                     valueLowerCase.startsWith(KEY1_HEX_ZONE2) ? KEY1_HEX_ZONE2.length() : KEY2_HEX_ZONE2.length());
983             parseZone2(value, multipleInfo);
984         } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
985             parseZone3(value.substring(KEY_HEX_ZONE3.length()), multipleInfo);
986         } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
987             parseZone4(value.substring(KEY_HEX_ZONE4.length()), multipleInfo);
988         } else if (searchRecord && valueLowerCase.startsWith(KEY_HEX_RECORD)) {
989             parseRecord(value.substring(KEY_HEX_RECORD.length()));
990         } else if (searchSource || searchRecordAfterSource) {
991             parseSourceAndRecord(value, searchSource, searchRecordAfterSource, multipleInfo);
992         }
993     }
994
995     /**
996      * Parse a text to identify a source
997      *
998      * @param text the text to be parsed
999      * @param acceptFollowMain true if follow main has to be considered in the search
1000      *
1001      * @return the identified source or null if no source is identified in the text
1002      */
1003     private @Nullable RotelSource parseSource(String text, boolean acceptFollowMain) {
1004         String value = text.trim();
1005         RotelSource source = null;
1006         if (!value.isEmpty()) {
1007             if (acceptFollowMain && SOURCE.equalsIgnoreCase(value)) {
1008                 try {
1009                     source = model.getSourceFromName(RotelSource.CAT1_FOLLOW_MAIN.getName());
1010                 } catch (RotelException e) {
1011                 }
1012             } else {
1013                 for (RotelSource src : sourcesLabels.keySet()) {
1014                     String label = sourcesLabels.get(src);
1015                     if (label != null && value.startsWith(label)) {
1016                         if (source == null || sourcesLabels.get(source).length() < label.length()) {
1017                             source = src;
1018                         }
1019                     }
1020                 }
1021             }
1022         }
1023         return source;
1024     }
1025
1026     private void parseSourceAndRecord(String text, boolean searchSource, boolean searchRecordAfterSource,
1027             boolean multipleInfo) {
1028         RotelSource source = parseSource(text, false);
1029         if (source != null) {
1030             if (searchSource) {
1031                 RotelCommand cmd = source.getCommand();
1032                 if (cmd != null) {
1033                     String value2 = cmd.getAsciiCommandV2();
1034                     if (value2 != null) {
1035                         dispatchKeyValue(KEY_SOURCE, value2);
1036                         if (!multipleInfo) {
1037                             dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
1038                         }
1039                     }
1040                 }
1041             }
1042
1043             if (searchRecordAfterSource) {
1044                 String value = text.substring(getSourceLabel(source).length()).trim();
1045                 source = parseSource(value, true);
1046                 if (source != null) {
1047                     RotelCommand cmd = source.getRecordCommand();
1048                     if (cmd != null) {
1049                         value = cmd.getAsciiCommandV2();
1050                         if (value != null) {
1051                             dispatchKeyValue(KEY_RECORD, value);
1052                         }
1053                     }
1054                 }
1055             }
1056         }
1057     }
1058
1059     private String getSourceLabel(RotelSource source) {
1060         String label = sourcesLabels.get(source);
1061         return (label == null) ? source.getLabel() : label;
1062     }
1063
1064     private void parseRecord(String text) {
1065         String value = text.trim();
1066         RotelSource source = parseSource(value, true);
1067         if (source != null) {
1068             RotelCommand cmd = source.getRecordCommand();
1069             if (cmd != null) {
1070                 value = cmd.getAsciiCommandV2();
1071                 if (value != null) {
1072                     dispatchKeyValue(KEY_RECORD, value);
1073                 }
1074             }
1075         } else {
1076             logger.debug("Invalid value {} for record source", value);
1077         }
1078     }
1079
1080     private void parseZone2(String text, boolean multipleInfo) {
1081         String value = text.trim();
1082         String valueLowerCase = value.toLowerCase();
1083         if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
1084             value = extractNumber(value,
1085                     valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
1086             dispatchKeyValue(KEY_VOLUME_ZONE2, value);
1087             dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
1088         } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
1089             value = value.substring(KEY_HEX_MUTE.length()).trim();
1090             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1091                 dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_ON);
1092             } else {
1093                 logger.debug("Invalid value {} for zone mute", value);
1094             }
1095         } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1096             RotelSource source = parseSource(value, true);
1097             if (source != null) {
1098                 RotelCommand cmd = source.getZone2Command();
1099                 if (cmd != null) {
1100                     value = cmd.getAsciiCommandV2();
1101                     if (value != null) {
1102                         dispatchKeyValue(KEY_SOURCE_ZONE2, value);
1103                         if (!multipleInfo) {
1104                             dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
1105                         }
1106                     }
1107                 }
1108             } else {
1109                 logger.debug("Invalid value {} for zone 2 source", value);
1110             }
1111         }
1112     }
1113
1114     private void parseZone3(String text, boolean multipleInfo) {
1115         String value = text.trim();
1116         String valueLowerCase = value.toLowerCase();
1117         if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
1118             value = extractNumber(value,
1119                     valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
1120             dispatchKeyValue(KEY_VOLUME_ZONE3, value);
1121             dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
1122         } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
1123             value = value.substring(KEY_HEX_MUTE.length()).trim();
1124             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1125                 dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_ON);
1126             } else {
1127                 logger.debug("Invalid value {} for zone mute", value);
1128             }
1129         } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1130             RotelSource source = parseSource(value, true);
1131             if (source != null) {
1132                 RotelCommand cmd = source.getZone3Command();
1133                 if (cmd != null) {
1134                     value = cmd.getAsciiCommandV2();
1135                     if (value != null) {
1136                         dispatchKeyValue(KEY_SOURCE_ZONE3, value);
1137                         if (!multipleInfo) {
1138                             dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
1139                         }
1140                     }
1141                 }
1142             } else {
1143                 logger.debug("Invalid value {} for zone 3 source", value);
1144             }
1145         }
1146     }
1147
1148     private void parseZone4(String text, boolean multipleInfo) {
1149         String value = text.trim();
1150         String valueLowerCase = value.toLowerCase();
1151         if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
1152             value = extractNumber(value,
1153                     valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
1154             dispatchKeyValue(KEY_VOLUME_ZONE4, value);
1155             dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
1156         } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
1157             value = value.substring(KEY_HEX_MUTE.length()).trim();
1158             if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1159                 dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_ON);
1160             } else {
1161                 logger.debug("Invalid value {} for zone mute", value);
1162             }
1163         } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1164             RotelSource source = parseSource(value, true);
1165             if (source != null) {
1166                 RotelCommand cmd = source.getZone4Command();
1167                 if (cmd != null) {
1168                     value = cmd.getAsciiCommandV2();
1169                     if (value != null) {
1170                         dispatchKeyValue(KEY_SOURCE_ZONE4, value);
1171                         if (!multipleInfo) {
1172                             dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
1173                         }
1174                     }
1175                 }
1176             } else {
1177                 logger.debug("Invalid value {} for zone 4 source", value);
1178             }
1179         }
1180     }
1181
1182     /**
1183      * Extract from a string a number
1184      *
1185      * @param value the string
1186      * @param startIndex the index in the string at which the integer has to be extracted
1187      *
1188      * @return the number as a string with its sign and no blank between the sign and the digits
1189      */
1190     private String extractNumber(String value, int startIndex) {
1191         String result = value.substring(startIndex).trim();
1192         // Delete possible blank(s) between the sign and the number
1193         if (result.startsWith("+") || result.startsWith("-")) {
1194             result = result.substring(0, 1) + result.substring(1, result.length()).trim();
1195         }
1196         return result;
1197     }
1198
1199     /**
1200      * Suppress certain sequences of bytes from a message
1201      *
1202      * @param message the message as a table of bytes
1203      * @param bytesSequences the table containing the sequence of bytes to be ignored
1204      *
1205      * @return the message without the unexpected sequence of bytes
1206      */
1207     private byte[] filterMessage(byte[] message, byte[][] bytesSequences) {
1208         if (bytesSequences.length == 0) {
1209             return message;
1210         }
1211         byte[] filteredMsg = new byte[message.length];
1212         int srcIdx = 0;
1213         int dstIdx = 0;
1214         while (srcIdx < message.length) {
1215             int ignoredLength = 0;
1216             for (int i = 0; i < bytesSequences.length; i++) {
1217                 int size = bytesSequences[i].length;
1218                 if ((message.length - srcIdx) >= size) {
1219                     boolean match = true;
1220                     for (int j = 0; j < size; j++) {
1221                         if (message[srcIdx + j] != bytesSequences[i][j]) {
1222                             match = false;
1223                             break;
1224                         }
1225                     }
1226                     if (match) {
1227                         ignoredLength = size;
1228                         break;
1229                     }
1230                 }
1231             }
1232             if (ignoredLength > 0) {
1233                 srcIdx += ignoredLength;
1234             } else {
1235                 filteredMsg[dstIdx++] = message[srcIdx++];
1236             }
1237         }
1238         return Arrays.copyOf(filteredMsg, dstIdx);
1239     }
1240
1241     /**
1242      * Dispatch an event (key, value) to the event listeners
1243      *
1244      * @param key the key
1245      * @param value the value
1246      */
1247     private void dispatchKeyValue(String key, String value) {
1248         RotelMessageEvent event = new RotelMessageEvent(this, key, value);
1249         for (int i = 0; i < listeners.size(); i++) {
1250             listeners.get(i).onNewMessageEvent(event);
1251         }
1252     }
1253 }