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