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);
432 } else if (value > 0) {
433 messageStr += String.format("R%02d", value);
435 messageStr += String.format("L%02d", -value);
438 case DIMMER_LEVEL_SET:
439 if (value > 0 && model.getDimmerLevelMin() < 0) {
440 messageStr += String.format("+%d", value);
442 messageStr += String.format("%d", value);
449 if (!messageStr.endsWith("?")) {
452 message = messageStr.getBytes(StandardCharsets.US_ASCII);
453 logger.debug("Send command \"{}\" => {}", cmd.getName(), messageStr);
457 messageStr = cmd.getAsciiCommandV2();
458 if (messageStr == null) {
459 logger.debug("Send comman \"{}\" ignored: not available for ASCII V2 protocol", cmd.getName());
465 messageStr += String.format("%02d", value);
471 } else if (value > 0) {
472 messageStr += String.format("+%02d", value);
474 messageStr += String.format("-%02d", -value);
480 } else if (value > 0) {
481 messageStr += String.format("r%02d", value);
483 messageStr += String.format("l%02d", -value);
486 case DIMMER_LEVEL_SET:
487 if (value > 0 && model.getDimmerLevelMin() < 0) {
488 messageStr += String.format("+%d", value);
490 messageStr += String.format("%d", value);
497 if (!messageStr.endsWith("?")) {
500 message = messageStr.getBytes(StandardCharsets.US_ASCII);
501 logger.debug("Send command \"{}\" => {}", cmd.getName(), messageStr);
508 OutputStream dataOut = this.dataOut;
509 if (dataOut == null) {
510 throw new RotelException("Send command \"" + cmd.getName() + "\" failed: output stream is null");
513 dataOut.write(message);
515 } catch (IOException e) {
516 logger.debug("Send command \"{}\" failed: {}", cmd.getName(), e.getMessage());
517 throw new RotelException("Send command \"" + cmd.getName() + "\" failed", e);
519 logger.debug("Send command \"{}\" succeeded", cmd.getName());
523 * Validate the content of a feedback message
525 * @param responseMessage the buffer containing the feedback message
527 * @throws RotelException - If the message has unexpected content
529 private void validateResponse(byte[] responseMessage) throws RotelException {
530 if (protocol == RotelProtocol.HEX) {
531 // Check minimum message length
532 if (responseMessage.length < 6) {
533 logger.debug("Unexpected message length: {}", responseMessage.length);
534 throw new RotelException("Unexpected message length");
538 if (responseMessage[0] != START) {
539 logger.debug("Unexpected START in response: {} rather than {}",
540 Integer.toHexString(responseMessage[0] & 0x000000FF), Integer.toHexString(START & 0x000000FF));
541 throw new RotelException("Unexpected START in response");
545 if (responseMessage[2] != model.getDeviceId()) {
546 logger.debug("Unexpected ID in response: {} rather than {}",
547 Integer.toHexString(responseMessage[2] & 0x000000FF),
548 Integer.toHexString(model.getDeviceId() & 0x000000FF));
549 throw new RotelException("Unexpected ID in response");
553 if (responseMessage[3] != STANDARD_RESPONSE && responseMessage[3] != TRIGGER_STATUS
554 && responseMessage[3] != SMART_DISPLAY_DATA_1 && responseMessage[3] != SMART_DISPLAY_DATA_2
555 && responseMessage[3] != PRIMARY_CMD && responseMessage[3] != MAIN_ZONE_CMD
556 && responseMessage[3] != RECORD_SRC_CMD && responseMessage[3] != ZONE2_CMD
557 && responseMessage[3] != ZONE3_CMD && responseMessage[3] != ZONE4_CMD
558 && responseMessage[3] != VOLUME_CMD && responseMessage[3] != ZONE2_VOLUME_CMD
559 && responseMessage[3] != ZONE3_VOLUME_CMD && responseMessage[3] != ZONE4_VOLUME_CMD
560 && responseMessage[3] != TRIGGER_CMD) {
561 logger.debug("Unexpected TYPE in response: {}", Integer.toHexString(responseMessage[3] & 0x000000FF));
562 throw new RotelException("Unexpected TYPE in response");
565 int expectedLen = (responseMessage[3] == STANDARD_RESPONSE)
566 ? (5 + model.getRespNbChars() + model.getRespNbFlags())
567 : responseMessage.length;
570 if (responseMessage[1] != (expectedLen - 3)) {
571 logger.debug("Unexpected COUNT in response: {} rather than {}",
572 Integer.toHexString(responseMessage[1] & 0x000000FF),
573 Integer.toHexString((expectedLen - 3) & 0x000000FF));
574 throw new RotelException("Unexpected COUNT in response");
577 final byte checksum = computeCheckSum(responseMessage, expectedLen - 2);
578 if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
582 // Check message length
583 if (responseMessage.length != expectedLen) {
584 logger.debug("Unexpected message length: {} rather than {}", responseMessage.length, expectedLen);
585 throw new RotelException("Unexpected message length");
589 if ((checksum & 0x000000FF) == 0x000000FD) {
590 if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
591 || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 0) {
592 logger.debug("Invalid check sum in response: {} rather than FD00", HexUtils.bytesToHex(
593 Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
594 throw new RotelException("Invalid check sum in response");
596 } else if ((checksum & 0x000000FF) == 0x000000FE) {
597 if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
598 || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 1) {
599 logger.debug("Invalid check sum in response: {} rather than FD01", HexUtils.bytesToHex(
600 Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
601 throw new RotelException("Invalid check sum in response");
603 } else if ((checksum & 0x000000FF) != (responseMessage[responseMessage.length - 1] & 0x000000FF)) {
604 logger.debug("Invalid check sum in response: {} rather than {}",
605 Integer.toHexString(responseMessage[responseMessage.length - 1] & 0x000000FF),
606 Integer.toHexString(checksum & 0x000000FF));
607 throw new RotelException("Invalid check sum in response");
610 // Check minimum message length
611 if (responseMessage.length < 1) {
612 logger.debug("Unexpected message length: {}", responseMessage.length);
613 throw new RotelException("Unexpected message length");
616 if (responseMessage[responseMessage.length - 1] != '!'
617 && responseMessage[responseMessage.length - 1] != '$') {
618 logger.debug("Unexpected ending character in response: {}",
619 Integer.toHexString(responseMessage[responseMessage.length - 1] & 0x000000FF));
620 throw new RotelException("Unexpected ending character in response");
626 * Compute the checksum of a message
628 * @param message the buffer containing the message
629 * @param maxIdx the position in the buffer at which the sum has to be stopped
631 * @return the checksum as a byte
633 protected byte computeCheckSum(byte[] message, int maxIdx) {
635 for (int i = 1; i <= maxIdx; i++) {
636 result += (message[i] & 0x000000FF);
638 return (byte) (result & 0x000000FF);
642 * Add a listener to the list of listeners to be notified with events
644 * @param listener the listener
646 public void addEventListener(RotelMessageEventListener listener) {
647 listeners.add(listener);
651 * Remove a listener from the list of listeners to be notified with events
653 * @param listener the listener
655 public void removeEventListener(RotelMessageEventListener listener) {
656 listeners.remove(listener);
660 * Analyze an incoming message and dispatch corresponding (key, value) to the event listeners
662 * @param incomingMessage the received message
664 public void handleIncomingMessage(byte[] incomingMessage) {
665 logger.debug("handleIncomingMessage: bytes {}", HexUtils.bytesToHex(incomingMessage));
667 if (READ_ERROR.equals(incomingMessage)) {
668 dispatchKeyValue(KEY_ERROR, MSG_VALUE_ON);
673 validateResponse(incomingMessage);
674 } catch (RotelException e) {
678 if (protocol == RotelProtocol.HEX) {
679 handleValidHexMessage(incomingMessage);
681 handleValidAsciiMessage(incomingMessage);
686 * Analyze a valid HEX message and dispatch corresponding (key, value) to the event listeners
688 * @param incomingMessage the received message
690 private void handleValidHexMessage(byte[] incomingMessage) {
691 if (incomingMessage[3] != STANDARD_RESPONSE) {
695 final int idxChars = model.isCharsBeforeFlags() ? 4 : (4 + model.getRespNbFlags());
697 // Replace characters with code < 32 by a space before converting to a string
698 for (int i = idxChars; i < (idxChars + model.getRespNbChars()); i++) {
699 if (incomingMessage[i] < 0x20) {
700 incomingMessage[i] = 0x20;
704 String value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
705 logger.debug("handleValidHexMessage: chars *{}*", value);
707 final int idxFlags = model.isCharsBeforeFlags() ? (4 + model.getRespNbChars()) : 4;
708 final byte[] flags = Arrays.copyOfRange(incomingMessage, idxFlags, idxFlags + model.getRespNbFlags());
709 if (logger.isTraceEnabled()) {
710 for (int i = 1; i <= flags.length; i++) {
712 logger.trace("handleValidHexMessage: Flag {} = {} bits 7-0 = {} {} {} {} {} {} {} {}", i,
713 Integer.toHexString(flags[i - 1] & 0x000000FF), RotelFlagsMapping.isBitFlagOn(flags, i, 7),
714 RotelFlagsMapping.isBitFlagOn(flags, i, 6), RotelFlagsMapping.isBitFlagOn(flags, i, 5),
715 RotelFlagsMapping.isBitFlagOn(flags, i, 4), RotelFlagsMapping.isBitFlagOn(flags, i, 3),
716 RotelFlagsMapping.isBitFlagOn(flags, i, 2), RotelFlagsMapping.isBitFlagOn(flags, i, 1),
717 RotelFlagsMapping.isBitFlagOn(flags, i, 0));
718 } catch (RotelException e1) {
723 dispatchKeyValue(KEY_POWER_ZONE2, model.isZone2On(flags) ? POWER_ON : STANDBY);
724 } catch (RotelException e1) {
727 dispatchKeyValue(KEY_POWER_ZONE3, model.isZone3On(flags) ? POWER_ON : STANDBY);
728 } catch (RotelException e1) {
731 dispatchKeyValue(KEY_POWER_ZONE4, model.isZone4On(flags) ? POWER_ON : STANDBY);
732 } catch (RotelException e1) {
734 boolean checkMultiIn = false;
735 boolean checkSource = true;
737 if (model.isMultiInputOn(flags)) {
740 RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
741 RotelCommand cmd = source.getCommand();
743 String value2 = cmd.getAsciiCommandV2();
744 if (value2 != null) {
745 dispatchKeyValue(KEY_SOURCE, value2);
748 } catch (RotelException e1) {
751 } catch (RotelException e1) {
754 boolean checkStereo = true;
756 checkStereo = !model.isMoreThan2Channels(flags);
757 } catch (RotelException e1) {
760 String valueLowerCase = value.trim().toLowerCase();
761 if (!valueLowerCase.isEmpty() && !valueLowerCase.startsWith(KEY1_HEX_ZONE2)
762 && !valueLowerCase.startsWith(KEY2_HEX_ZONE2) && !valueLowerCase.startsWith(KEY_HEX_ZONE3)
763 && !valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
764 dispatchKeyValue(KEY_POWER, POWER_ON);
767 if (model.getRespNbChars() == 42) {
768 // 2 lines of 21 characters with a left part and a right part
771 value = new String(incomingMessage, idxChars, 14, StandardCharsets.US_ASCII);
772 logger.debug("handleValidHexMessage: line 1 left *{}*", value);
773 parseText(value, checkSource, checkMultiIn, false, false, false, false, false, true);
776 value = new String(incomingMessage, idxChars + 14, 7, StandardCharsets.US_ASCII);
777 logger.debug("handleValidHexMessage: line 1 right *{}*", value);
778 parseText(value, false, false, false, false, false, false, false, true);
781 value = new String(incomingMessage, idxChars, 21, StandardCharsets.US_ASCII);
782 dispatchKeyValue(KEY_LINE1, value);
785 value = new String(incomingMessage, idxChars + 35, 7, StandardCharsets.US_ASCII);
786 logger.debug("handleValidHexMessage: line 2 right *{}*", value);
787 parseText(value, false, false, false, false, false, false, false, true);
790 value = new String(incomingMessage, idxChars + 21, 21, StandardCharsets.US_ASCII);
791 logger.debug("handleValidHexMessage: line 2 *{}*", value);
792 parseText(value, false, false, true, true, false, true, true, true);
793 dispatchKeyValue(KEY_LINE2, value);
795 value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
796 parseText(value, checkSource, checkMultiIn, true, false, true, true, checkStereo, false);
797 dispatchKeyValue(KEY_LINE1, value);
800 if (valueLowerCase.isEmpty()) {
801 dispatchKeyValue(KEY_POWER, POWER_OFF_DELAYED);
806 * Analyze a valid ASCII message and dispatch corresponding (key, value) to the event listeners
808 * @param incomingMessage the received message
810 public void handleValidAsciiMessage(byte[] incomingMessage) {
811 byte[] message = filterMessage(incomingMessage, model.getSpecialCharacters());
813 // Replace characters with code < 32 by a space before converting to a string
814 for (int i = 0; i < message.length; i++) {
815 if (message[i] < 0x20) {
820 String value = new String(message, 0, message.length - 1, StandardCharsets.US_ASCII);
821 logger.debug("handleValidAsciiMessage: chars *{}*", value);
822 value = value.trim();
823 if (value.isEmpty()) {
827 String[] splittedValue = value.split("=");
828 if (splittedValue.length != 2) {
829 logger.debug("handleValidAsciiMessage: ignored message {}", value);
831 dispatchKeyValue(splittedValue[0].trim().toLowerCase(), splittedValue[1]);
833 } catch (PatternSyntaxException e) {
834 logger.debug("handleValidAsciiMessage: ignored message {}", value);
839 * Parse a text and dispatch appropriate (key, value) to the event listeners for found information
841 * @param text the text to be parsed
842 * @param searchSource true if a source has to be searched in the text
843 * @param searchMultiIn true if MULTI IN indication has to be searched in the text
844 * @param searchZone true if a zone information has to be searched in the text
845 * @param searchRecord true if a record source has to be searched in the text
846 * @param searchRecordAfterSource true if a record source has to be searched in the text after the a found source
847 * @param searchDsp true if a DSP mode has to be searched in the text
848 * @param searchStereo true if a STEREO has to be considered in the search
849 * @param multipleInfo true if source and volume/mute are provided separately
851 private void parseText(String text, boolean searchSource, boolean searchMultiIn, boolean searchZone,
852 boolean searchRecord, boolean searchRecordAfterSource, boolean searchDsp, boolean searchStereo,
853 boolean multipleInfo) {
854 String value = text.trim();
855 String valueLowerCase = value.toLowerCase();
857 dispatchKeyValue(KEY_RECORD_SEL, valueLowerCase.startsWith(KEY_HEX_RECORD) ? MSG_VALUE_ON : MSG_VALUE_OFF);
860 if (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2)) {
861 dispatchKeyValue(KEY_ZONE, "2");
862 } else if (valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
863 dispatchKeyValue(KEY_ZONE, "3");
864 } else if (valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
865 dispatchKeyValue(KEY_ZONE, "4");
867 dispatchKeyValue(KEY_ZONE, "1");
870 if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
871 value = extractNumber(value,
872 valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
873 dispatchKeyValue(KEY_VOLUME, value);
874 dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
875 } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
876 value = value.substring(KEY_HEX_MUTE.length()).trim();
877 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
878 dispatchKeyValue(KEY_MUTE, MSG_VALUE_ON);
880 logger.debug("Invalid value {} for zone mute", value);
882 } else if (valueLowerCase.startsWith(KEY1_HEX_BASS) || valueLowerCase.startsWith(KEY2_HEX_BASS)) {
883 value = extractNumber(value,
884 valueLowerCase.startsWith(KEY1_HEX_BASS) ? KEY1_HEX_BASS.length() : KEY2_HEX_BASS.length());
885 dispatchKeyValue(KEY_BASS, value);
886 } else if (valueLowerCase.startsWith(KEY1_HEX_TREBLE) || valueLowerCase.startsWith(KEY2_HEX_TREBLE)) {
887 value = extractNumber(value,
888 valueLowerCase.startsWith(KEY1_HEX_TREBLE) ? KEY1_HEX_TREBLE.length() : KEY2_HEX_TREBLE.length());
889 dispatchKeyValue(KEY_TREBLE, value);
890 } else if (searchMultiIn && valueLowerCase.startsWith(KEY_HEX_MULTI_IN)) {
891 value = value.substring(KEY_HEX_MULTI_IN.length()).trim();
892 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
894 RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
895 RotelCommand cmd = source.getCommand();
897 String value2 = cmd.getAsciiCommandV2();
898 if (value2 != null) {
899 dispatchKeyValue(KEY_SOURCE, value2);
902 } catch (RotelException e1) {
904 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
905 logger.debug("Invalid value {} for MULTI IN", value);
907 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_BYPASS)) {
908 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_BYPASS.getFeedback());
909 } else if (searchDsp && searchStereo && valueLowerCase.startsWith(KEY_HEX_STEREO)) {
910 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
911 } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_3CH) || valueLowerCase.startsWith(KEY2_HEX_3CH))) {
912 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO3.getFeedback());
913 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_5CH)) {
914 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO5.getFeedback());
915 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_7CH)) {
916 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO7.getFeedback());
918 && (valueLowerCase.startsWith(KEY_HEX_MUSIC1) || valueLowerCase.startsWith(KEY_HEX_DSP1))) {
919 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP1.getFeedback());
921 && (valueLowerCase.startsWith(KEY_HEX_MUSIC2) || valueLowerCase.startsWith(KEY_HEX_DSP2))) {
922 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP2.getFeedback());
924 && (valueLowerCase.startsWith(KEY_HEX_MUSIC3) || valueLowerCase.startsWith(KEY_HEX_DSP3))) {
925 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP3.getFeedback());
927 && (valueLowerCase.startsWith(KEY_HEX_MUSIC4) || valueLowerCase.startsWith(KEY_HEX_DSP4))) {
928 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP4.getFeedback());
929 } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_CINEMA)
930 || valueLowerCase.startsWith(KEY2_HEX_PLII_CINEMA) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_CINEMA)
931 || searchDsp && valueLowerCase.startsWith(KEY2_HEX_PLIIX_CINEMA))) {
932 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_CINEMA.getFeedback());
933 } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_MUSIC)
934 || valueLowerCase.startsWith(KEY2_HEX_PLII_MUSIC) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_MUSIC)
935 || valueLowerCase.startsWith(KEY2_HEX_PLIIX_MUSIC))) {
936 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_MUSIC.getFeedback());
937 } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_GAME)
938 || valueLowerCase.startsWith(KEY2_HEX_PLII_GAME) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_GAME)
939 || valueLowerCase.startsWith(KEY2_HEX_PLIIX_GAME))) {
940 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_GAME.getFeedback());
941 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PLIIZ)) {
942 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PLIIZ.getFeedback());
944 && (valueLowerCase.startsWith(KEY1_HEX_PROLOGIC) || valueLowerCase.startsWith(KEY2_HEX_PROLOGIC))) {
945 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PROLOGIC.getFeedback());
946 } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_CINEMA)
947 || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_CINEMA))) {
948 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_CINEMA.getFeedback());
949 } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_MUSIC)
950 || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_MUSIC))) {
951 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_MUSIC.getFeedback());
952 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_ES)) {
953 logger.debug("DTS-ES");
954 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
955 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_96)) {
956 logger.debug("DTS 96");
957 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
958 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS)) {
960 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
961 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD_EX)) {
962 logger.debug("DD-EX");
963 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
964 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD)) {
966 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
967 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_LPCM)) {
968 logger.debug("LPCM");
969 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
970 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PCM)) {
972 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
973 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_MPEG)) {
974 logger.debug("MPEG");
975 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
976 } else if (searchZone
977 && (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2))) {
978 value = value.substring(
979 valueLowerCase.startsWith(KEY1_HEX_ZONE2) ? KEY1_HEX_ZONE2.length() : KEY2_HEX_ZONE2.length());
980 parseZone2(value, multipleInfo);
981 } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
982 parseZone3(value.substring(KEY_HEX_ZONE3.length()), multipleInfo);
983 } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
984 parseZone4(value.substring(KEY_HEX_ZONE4.length()), multipleInfo);
985 } else if (searchRecord && valueLowerCase.startsWith(KEY_HEX_RECORD)) {
986 parseRecord(value.substring(KEY_HEX_RECORD.length()));
987 } else if (searchSource || searchRecordAfterSource) {
988 parseSourceAndRecord(value, searchSource, searchRecordAfterSource, multipleInfo);
993 * Parse a text to identify a source
995 * @param text the text to be parsed
996 * @param acceptFollowMain true if follow main has to be considered in the search
998 * @return the identified source or null if no source is identified in the text
1000 private @Nullable RotelSource parseSource(String text, boolean acceptFollowMain) {
1001 String value = text.trim();
1002 RotelSource source = null;
1003 if (!value.isEmpty()) {
1004 if (acceptFollowMain && SOURCE.equalsIgnoreCase(value)) {
1006 source = model.getSourceFromName(RotelSource.CAT1_FOLLOW_MAIN.getName());
1007 } catch (RotelException e) {
1010 for (RotelSource src : sourcesLabels.keySet()) {
1011 String label = sourcesLabels.get(src);
1012 if (label != null && value.startsWith(label)) {
1013 if (source == null || sourcesLabels.get(source).length() < label.length()) {
1023 private void parseSourceAndRecord(String text, boolean searchSource, boolean searchRecordAfterSource,
1024 boolean multipleInfo) {
1025 RotelSource source = parseSource(text, false);
1026 if (source != null) {
1028 RotelCommand cmd = source.getCommand();
1030 String value2 = cmd.getAsciiCommandV2();
1031 if (value2 != null) {
1032 dispatchKeyValue(KEY_SOURCE, value2);
1033 if (!multipleInfo) {
1034 dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
1040 if (searchRecordAfterSource) {
1041 String value = text.substring(getSourceLabel(source).length()).trim();
1042 source = parseSource(value, true);
1043 if (source != null) {
1044 RotelCommand cmd = source.getRecordCommand();
1046 value = cmd.getAsciiCommandV2();
1047 if (value != null) {
1048 dispatchKeyValue(KEY_RECORD, value);
1056 private String getSourceLabel(RotelSource source) {
1057 String label = sourcesLabels.get(source);
1058 return (label == null) ? source.getLabel() : label;
1061 private void parseRecord(String text) {
1062 String value = text.trim();
1063 RotelSource source = parseSource(value, true);
1064 if (source != null) {
1065 RotelCommand cmd = source.getRecordCommand();
1067 value = cmd.getAsciiCommandV2();
1068 if (value != null) {
1069 dispatchKeyValue(KEY_RECORD, value);
1073 logger.debug("Invalid value {} for record source", value);
1077 private void parseZone2(String text, boolean multipleInfo) {
1078 String value = text.trim();
1079 String valueLowerCase = value.toLowerCase();
1080 if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
1081 value = extractNumber(value,
1082 valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
1083 dispatchKeyValue(KEY_VOLUME_ZONE2, value);
1084 dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
1085 } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
1086 value = value.substring(KEY_HEX_MUTE.length()).trim();
1087 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1088 dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_ON);
1090 logger.debug("Invalid value {} for zone mute", value);
1092 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1093 RotelSource source = parseSource(value, true);
1094 if (source != null) {
1095 RotelCommand cmd = source.getZone2Command();
1097 value = cmd.getAsciiCommandV2();
1098 if (value != null) {
1099 dispatchKeyValue(KEY_SOURCE_ZONE2, value);
1100 if (!multipleInfo) {
1101 dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
1106 logger.debug("Invalid value {} for zone 2 source", value);
1111 private void parseZone3(String text, boolean multipleInfo) {
1112 String value = text.trim();
1113 String valueLowerCase = value.toLowerCase();
1114 if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
1115 value = extractNumber(value,
1116 valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
1117 dispatchKeyValue(KEY_VOLUME_ZONE3, value);
1118 dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
1119 } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
1120 value = value.substring(KEY_HEX_MUTE.length()).trim();
1121 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1122 dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_ON);
1124 logger.debug("Invalid value {} for zone mute", value);
1126 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1127 RotelSource source = parseSource(value, true);
1128 if (source != null) {
1129 RotelCommand cmd = source.getZone3Command();
1131 value = cmd.getAsciiCommandV2();
1132 if (value != null) {
1133 dispatchKeyValue(KEY_SOURCE_ZONE3, value);
1134 if (!multipleInfo) {
1135 dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
1140 logger.debug("Invalid value {} for zone 3 source", value);
1145 private void parseZone4(String text, boolean multipleInfo) {
1146 String value = text.trim();
1147 String valueLowerCase = value.toLowerCase();
1148 if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
1149 value = extractNumber(value,
1150 valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
1151 dispatchKeyValue(KEY_VOLUME_ZONE4, value);
1152 dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
1153 } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
1154 value = value.substring(KEY_HEX_MUTE.length()).trim();
1155 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
1156 dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_ON);
1158 logger.debug("Invalid value {} for zone mute", value);
1160 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1161 RotelSource source = parseSource(value, true);
1162 if (source != null) {
1163 RotelCommand cmd = source.getZone4Command();
1165 value = cmd.getAsciiCommandV2();
1166 if (value != null) {
1167 dispatchKeyValue(KEY_SOURCE_ZONE4, value);
1168 if (!multipleInfo) {
1169 dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
1174 logger.debug("Invalid value {} for zone 4 source", value);
1180 * Extract from a string a number
1182 * @param value the string
1183 * @param startIndex the index in the string at which the integer has to be extracted
1185 * @return the number as a string with its sign and no blank between the sign and the digits
1187 private String extractNumber(String value, int startIndex) {
1188 String result = value.substring(startIndex).trim();
1189 // Delete possible blank(s) between the sign and the number
1190 if (result.startsWith("+") || result.startsWith("-")) {
1191 result = result.substring(0, 1) + result.substring(1, result.length()).trim();
1197 * Suppress certain sequences of bytes from a message
1199 * @param message the message as a table of bytes
1200 * @param bytesSequences the table containing the sequence of bytes to be ignored
1202 * @return the message without the unexpected sequence of bytes
1204 private byte[] filterMessage(byte[] message, byte[][] bytesSequences) {
1205 if (bytesSequences.length == 0) {
1208 byte[] filteredMsg = new byte[message.length];
1211 while (srcIdx < message.length) {
1212 int ignoredLength = 0;
1213 for (int i = 0; i < bytesSequences.length; i++) {
1214 int size = bytesSequences[i].length;
1215 if ((message.length - srcIdx) >= size) {
1216 boolean match = true;
1217 for (int j = 0; j < size; j++) {
1218 if (message[srcIdx + j] != bytesSequences[i][j]) {
1224 ignoredLength = size;
1229 if (ignoredLength > 0) {
1230 srcIdx += ignoredLength;
1232 filteredMsg[dstIdx++] = message[srcIdx++];
1235 return Arrays.copyOf(filteredMsg, dstIdx);
1239 * Dispatch an event (key, value) to the event listeners
1241 * @param key the key
1242 * @param value the value
1244 private void dispatchKeyValue(String key, String value) {
1245 RotelMessageEvent event = new RotelMessageEvent(this, key, value);
1246 for (int i = 0; i < listeners.size(); i++) {
1247 listeners.get(i).onNewMessageEvent(event);