2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.rotel.internal.communication;
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;
24 import java.util.regex.PatternSyntaxException;
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;
35 * Abstract class for communicating with the Rotel device
37 * @author Laurent Garnier - Initial contribution
40 public abstract class RotelConnector {
42 private final Logger logger = LoggerFactory.getLogger(RotelConnector.class);
44 public static final byte[] READ_ERROR = "read_error".getBytes(StandardCharsets.US_ASCII);
46 protected static final byte START = (byte) 0xFE;
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;
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 ";
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";
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";
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";
182 private RotelModel model;
183 private RotelProtocol protocol;
184 protected Map<RotelSource, String> sourcesLabels;
185 private boolean simu;
187 /** The output stream */
188 protected @Nullable OutputStream dataOut;
190 /** The input stream */
191 protected @Nullable InputStream dataIn;
193 /** true if the connection is established, false if not */
194 private boolean connected;
196 protected String readerThreadName;
197 private @Nullable Thread readerThread;
199 private List<RotelMessageEventListener> listeners = new ArrayList<>();
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 } };
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 } };
223 /** Empty table of special characters */
224 public static final byte[][] NO_SPECIAL_CHARACTERS = {};
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
234 public RotelConnector(RotelModel model, RotelProtocol protocol, Map<RotelSource, String> sourcesLabels,
235 boolean simu, String readerThreadName) {
237 this.protocol = protocol;
238 this.sourcesLabels = sourcesLabels;
240 this.readerThreadName = readerThreadName;
244 * Get the Rotel model
248 public RotelModel getModel() {
253 * Get the protocol to be used
255 * @return the protocol
257 public RotelProtocol getProtocol() {
262 * Get whether the connection is established or not
264 * @return true if the connection is established
266 public boolean isConnected() {
271 * Set whether the connection is established or not
273 * @param connected true if the connection is established
275 protected void setConnected(boolean connected) {
276 this.connected = connected;
280 * Set the thread that handles the feedback messages
282 * @param readerThread the thread
284 protected void setReaderThread(Thread readerThread) {
285 this.readerThread = readerThread;
289 * Open the connection with the Rotel device
291 * @throws RotelException - In case of any problem
293 public abstract void open() throws RotelException;
296 * Close the connection with the Rotel device
298 public abstract void close();
301 * Stop the thread that handles the feedback messages and close the opened input and output streams
303 protected void cleanup() {
304 Thread readerThread = this.readerThread;
305 if (readerThread != null) {
306 readerThread.interrupt();
309 } catch (InterruptedException e) {
311 this.readerThread = null;
313 OutputStream dataOut = this.dataOut;
314 if (dataOut != null) {
317 } catch (IOException e) {
321 InputStream dataIn = this.dataIn;
322 if (dataIn != null) {
325 } catch (IOException e) {
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.
335 * @param dataBuffer the buffer into which the data is read.
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.
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
343 * @throws InterruptedIOException - if the thread was interrupted during the reading of the input stream
345 protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
347 throw new RotelException("readInput failed: should not be called in simu mode");
349 InputStream dataIn = this.dataIn;
350 if (dataIn == null) {
351 throw new RotelException("readInput failed: input stream is null");
354 return dataIn.read(dataBuffer);
355 } catch (IOException e) {
356 logger.debug("readInput failed: {}", e.getMessage());
357 throw new RotelException("readInput failed", e);
362 * Request the Rotel device to execute a command
364 * @param cmd the command to execute
366 * @throws RotelException - In case of any problem
368 public void sendCommand(RotelCommand cmd) throws RotelException {
369 sendCommand(cmd, null);
373 * Request the Rotel device to execute a command
375 * @param cmd the command to execute
376 * @param value the integer value to consider for volume, bass or treble adjustment
378 * @throws RotelException - In case of any problem
380 public void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
382 byte[] message = new byte[0];
385 if (cmd.getHexType() == 0) {
386 logger.debug("Send comman \"{}\" ignored: not available for HEX protocol", cmd.getName());
390 message = new byte[size];
392 message[idx++] = START;
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;
403 message[idx++] = checksum;
405 logger.debug("Send command \"{}\" => {}", cmd.getName(), HexUtils.bytesToHex(message));
409 messageStr = cmd.getAsciiCommandV1();
410 if (messageStr == null) {
411 logger.debug("Send comman \"{}\" ignored: not available for ASCII V1 protocol", cmd.getName());
417 messageStr += String.format("%d", value);
423 } else if (value > 0) {
424 messageStr += String.format("+%02d", value);
426 messageStr += String.format("-%02d", -value);
429 case DIMMER_LEVEL_SET:
430 if (value > 0 && model.getDimmerLevelMin() < 0) {
431 messageStr += String.format("+%d", value);
433 messageStr += String.format("%d", value);
440 if (!messageStr.endsWith("?")) {
443 message = messageStr.getBytes(StandardCharsets.US_ASCII);
444 logger.debug("Send command \"{}\" => {}", cmd.getName(), messageStr);
448 messageStr = cmd.getAsciiCommandV2();
449 if (messageStr == null) {
450 logger.debug("Send comman \"{}\" ignored: not available for ASCII V2 protocol", cmd.getName());
456 messageStr += String.format("%02d", value);
462 } else if (value > 0) {
463 messageStr += String.format("+%02d", value);
465 messageStr += String.format("-%02d", -value);
471 } else if (value > 0) {
472 messageStr += String.format("R%02d", value);
474 messageStr += String.format("L%02d", -value);
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
483 } else if (value > 0) {
484 messageStr += String.format("r%02d", value);
486 messageStr += String.format("l%02d", -value);
489 case DIMMER_LEVEL_SET:
490 if (value > 0 && model.getDimmerLevelMin() < 0) {
491 messageStr += String.format("+%d", value);
493 messageStr += String.format("%d", value);
500 if (!messageStr.endsWith("?")) {
503 message = messageStr.getBytes(StandardCharsets.US_ASCII);
504 logger.debug("Send command \"{}\" => {}", cmd.getName(), messageStr);
511 OutputStream dataOut = this.dataOut;
512 if (dataOut == null) {
513 throw new RotelException("Send command \"" + cmd.getName() + "\" failed: output stream is null");
516 dataOut.write(message);
518 } catch (IOException e) {
519 logger.debug("Send command \"{}\" failed: {}", cmd.getName(), e.getMessage());
520 throw new RotelException("Send command \"" + cmd.getName() + "\" failed", e);
522 logger.debug("Send command \"{}\" succeeded", cmd.getName());
526 * Validate the content of a feedback message
528 * @param responseMessage the buffer containing the feedback message
530 * @throws RotelException - If the message has unexpected content
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");
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");
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");
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");
568 int expectedLen = (responseMessage[3] == STANDARD_RESPONSE)
569 ? (5 + model.getRespNbChars() + model.getRespNbFlags())
570 : responseMessage.length;
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");
580 final byte checksum = computeCheckSum(responseMessage, expectedLen - 2);
581 if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
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");
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");
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");
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");
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");
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");
629 * Compute the checksum of a message
631 * @param message the buffer containing the message
632 * @param maxIdx the position in the buffer at which the sum has to be stopped
634 * @return the checksum as a byte
636 protected byte computeCheckSum(byte[] message, int maxIdx) {
638 for (int i = 1; i <= maxIdx; i++) {
639 result += (message[i] & 0x000000FF);
641 return (byte) (result & 0x000000FF);
645 * Add a listener to the list of listeners to be notified with events
647 * @param listener the listener
649 public void addEventListener(RotelMessageEventListener listener) {
650 listeners.add(listener);
654 * Remove a listener from the list of listeners to be notified with events
656 * @param listener the listener
658 public void removeEventListener(RotelMessageEventListener listener) {
659 listeners.remove(listener);
663 * Analyze an incoming message and dispatch corresponding (key, value) to the event listeners
665 * @param incomingMessage the received message
667 public void handleIncomingMessage(byte[] incomingMessage) {
668 logger.debug("handleIncomingMessage: bytes {}", HexUtils.bytesToHex(incomingMessage));
670 if (READ_ERROR.equals(incomingMessage)) {
671 dispatchKeyValue(KEY_ERROR, MSG_VALUE_ON);
676 validateResponse(incomingMessage);
677 } catch (RotelException e) {
681 if (protocol == RotelProtocol.HEX) {
682 handleValidHexMessage(incomingMessage);
684 handleValidAsciiMessage(incomingMessage);
689 * Analyze a valid HEX message and dispatch corresponding (key, value) to the event listeners
691 * @param incomingMessage the received message
693 private void handleValidHexMessage(byte[] incomingMessage) {
694 if (incomingMessage[3] != STANDARD_RESPONSE) {
698 final int idxChars = model.isCharsBeforeFlags() ? 4 : (4 + model.getRespNbFlags());
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;
707 String value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
708 logger.debug("handleValidHexMessage: chars *{}*", value);
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++) {
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) {
726 dispatchKeyValue(KEY_POWER_ZONE2, model.isZone2On(flags) ? POWER_ON : STANDBY);
727 } catch (RotelException e1) {
730 dispatchKeyValue(KEY_POWER_ZONE3, model.isZone3On(flags) ? POWER_ON : STANDBY);
731 } catch (RotelException e1) {
734 dispatchKeyValue(KEY_POWER_ZONE4, model.isZone4On(flags) ? POWER_ON : STANDBY);
735 } catch (RotelException e1) {
737 boolean checkMultiIn = false;
738 boolean checkSource = true;
740 if (model.isMultiInputOn(flags)) {
743 RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
744 RotelCommand cmd = source.getCommand();
746 String value2 = cmd.getAsciiCommandV2();
747 if (value2 != null) {
748 dispatchKeyValue(KEY_SOURCE, value2);
751 } catch (RotelException e1) {
754 } catch (RotelException e1) {
757 boolean checkStereo = true;
759 checkStereo = !model.isMoreThan2Channels(flags);
760 } catch (RotelException e1) {
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);
770 if (model.getRespNbChars() == 42) {
771 // 2 lines of 21 characters with a left part and a right part
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);
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);
784 value = new String(incomingMessage, idxChars, 21, StandardCharsets.US_ASCII);
785 dispatchKeyValue(KEY_LINE1, value);
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);
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);
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);
803 if (valueLowerCase.isEmpty()) {
804 dispatchKeyValue(KEY_POWER, POWER_OFF_DELAYED);
809 * Analyze a valid ASCII message and dispatch corresponding (key, value) to the event listeners
811 * @param incomingMessage the received message
813 public void handleValidAsciiMessage(byte[] incomingMessage) {
814 byte[] message = filterMessage(incomingMessage, model.getSpecialCharacters());
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) {
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()) {
830 String[] splittedValue = value.split("=");
831 if (splittedValue.length != 2) {
832 logger.debug("handleValidAsciiMessage: ignored message {}", value);
834 dispatchKeyValue(splittedValue[0].trim().toLowerCase(), splittedValue[1]);
836 } catch (PatternSyntaxException e) {
837 logger.debug("handleValidAsciiMessage: ignored message {}", value);
842 * Parse a text and dispatch appropriate (key, value) to the event listeners for found information
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
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();
860 dispatchKeyValue(KEY_RECORD_SEL, valueLowerCase.startsWith(KEY_HEX_RECORD) ? MSG_VALUE_ON : MSG_VALUE_OFF);
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");
870 dispatchKeyValue(KEY_ZONE, "1");
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);
883 logger.debug("Invalid value {} for zone mute", value);
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)) {
897 RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
898 RotelCommand cmd = source.getCommand();
900 String value2 = cmd.getAsciiCommandV2();
901 if (value2 != null) {
902 dispatchKeyValue(KEY_SOURCE, value2);
905 } catch (RotelException e1) {
907 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
908 logger.debug("Invalid value {} for MULTI IN", value);
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());
921 && (valueLowerCase.startsWith(KEY_HEX_MUSIC1) || valueLowerCase.startsWith(KEY_HEX_DSP1))) {
922 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP1.getFeedback());
924 && (valueLowerCase.startsWith(KEY_HEX_MUSIC2) || valueLowerCase.startsWith(KEY_HEX_DSP2))) {
925 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP2.getFeedback());
927 && (valueLowerCase.startsWith(KEY_HEX_MUSIC3) || valueLowerCase.startsWith(KEY_HEX_DSP3))) {
928 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP3.getFeedback());
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());
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)) {
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)) {
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)) {
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);
996 * Parse a text to identify a source
998 * @param text the text to be parsed
999 * @param acceptFollowMain true if follow main has to be considered in the search
1001 * @return the identified source or null if no source is identified in the text
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)) {
1009 source = model.getSourceFromName(RotelSource.CAT1_FOLLOW_MAIN.getName());
1010 } catch (RotelException e) {
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()) {
1026 private void parseSourceAndRecord(String text, boolean searchSource, boolean searchRecordAfterSource,
1027 boolean multipleInfo) {
1028 RotelSource source = parseSource(text, false);
1029 if (source != null) {
1031 RotelCommand cmd = source.getCommand();
1033 String value2 = cmd.getAsciiCommandV2();
1034 if (value2 != null) {
1035 dispatchKeyValue(KEY_SOURCE, value2);
1036 if (!multipleInfo) {
1037 dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
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();
1049 value = cmd.getAsciiCommandV2();
1050 if (value != null) {
1051 dispatchKeyValue(KEY_RECORD, value);
1059 private String getSourceLabel(RotelSource source) {
1060 String label = sourcesLabels.get(source);
1061 return (label == null) ? source.getLabel() : label;
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();
1070 value = cmd.getAsciiCommandV2();
1071 if (value != null) {
1072 dispatchKeyValue(KEY_RECORD, value);
1076 logger.debug("Invalid value {} for record source", value);
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);
1093 logger.debug("Invalid value {} for zone mute", value);
1095 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1096 RotelSource source = parseSource(value, true);
1097 if (source != null) {
1098 RotelCommand cmd = source.getZone2Command();
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);
1109 logger.debug("Invalid value {} for zone 2 source", value);
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);
1127 logger.debug("Invalid value {} for zone mute", value);
1129 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1130 RotelSource source = parseSource(value, true);
1131 if (source != null) {
1132 RotelCommand cmd = source.getZone3Command();
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);
1143 logger.debug("Invalid value {} for zone 3 source", value);
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);
1161 logger.debug("Invalid value {} for zone mute", value);
1163 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1164 RotelSource source = parseSource(value, true);
1165 if (source != null) {
1166 RotelCommand cmd = source.getZone4Command();
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);
1177 logger.debug("Invalid value {} for zone 4 source", value);
1183 * Extract from a string a number
1185 * @param value the string
1186 * @param startIndex the index in the string at which the integer has to be extracted
1188 * @return the number as a string with its sign and no blank between the sign and the digits
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();
1200 * Suppress certain sequences of bytes from a message
1202 * @param message the message as a table of bytes
1203 * @param bytesSequences the table containing the sequence of bytes to be ignored
1205 * @return the message without the unexpected sequence of bytes
1207 private byte[] filterMessage(byte[] message, byte[][] bytesSequences) {
1208 if (bytesSequences.length == 0) {
1211 byte[] filteredMsg = new byte[message.length];
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]) {
1227 ignoredLength = size;
1232 if (ignoredLength > 0) {
1233 srcIdx += ignoredLength;
1235 filteredMsg[dstIdx++] = message[srcIdx++];
1238 return Arrays.copyOf(filteredMsg, dstIdx);
1242 * Dispatch an event (key, value) to the event listeners
1244 * @param key the key
1245 * @param value the value
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);