2 * Copyright (c) 2010-2021 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";
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";
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";
175 private RotelModel model;
176 private RotelProtocol protocol;
177 protected Map<RotelSource, String> sourcesLabels;
178 private boolean simu;
180 /** The output stream */
181 protected @Nullable OutputStream dataOut;
183 /** The input stream */
184 protected @Nullable InputStream dataIn;
186 /** true if the connection is established, false if not */
187 private boolean connected;
189 protected String readerThreadName;
190 private @Nullable Thread readerThread;
192 private List<RotelMessageEventListener> listeners = new ArrayList<>();
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 } };
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 } };
216 /** Empty table of special characters */
217 public static final byte[][] NO_SPECIAL_CHARACTERS = {};
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
227 public RotelConnector(RotelModel model, RotelProtocol protocol, Map<RotelSource, String> sourcesLabels,
228 boolean simu, String readerThreadName) {
230 this.protocol = protocol;
231 this.sourcesLabels = sourcesLabels;
233 this.readerThreadName = readerThreadName;
237 * Get the Rotel model
241 public RotelModel getModel() {
246 * Get the protocol to be used
248 * @return the protocol
250 public RotelProtocol getProtocol() {
255 * Get whether the connection is established or not
257 * @return true if the connection is established
259 public boolean isConnected() {
264 * Set whether the connection is established or not
266 * @param connected true if the connection is established
268 protected void setConnected(boolean connected) {
269 this.connected = connected;
273 * Set the thread that handles the feedback messages
275 * @param readerThread the thread
277 protected void setReaderThread(Thread readerThread) {
278 this.readerThread = readerThread;
282 * Open the connection with the Rotel device
284 * @throws RotelException - In case of any problem
286 public abstract void open() throws RotelException;
289 * Close the connection with the Rotel device
291 public abstract void close();
294 * Stop the thread that handles the feedback messages and close the opened input and output streams
296 protected void cleanup() {
297 Thread readerThread = this.readerThread;
298 if (readerThread != null) {
299 readerThread.interrupt();
302 } catch (InterruptedException e) {
304 this.readerThread = null;
306 OutputStream dataOut = this.dataOut;
307 if (dataOut != null) {
310 } catch (IOException e) {
314 InputStream dataIn = this.dataIn;
315 if (dataIn != null) {
318 } catch (IOException e) {
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.
328 * @param dataBuffer the buffer into which the data is read.
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.
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
336 * @throws InterruptedIOException - if the thread was interrupted during the reading of the input stream
338 protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
340 throw new RotelException("readInput failed: should not be called in simu mode");
342 InputStream dataIn = this.dataIn;
343 if (dataIn == null) {
344 throw new RotelException("readInput failed: input stream is null");
347 return dataIn.read(dataBuffer);
348 } catch (IOException e) {
349 logger.debug("readInput failed: {}", e.getMessage());
350 throw new RotelException("readInput failed", e);
355 * Request the Rotel device to execute a command
357 * @param cmd the command to execute
359 * @throws RotelException - In case of any problem
361 public void sendCommand(RotelCommand cmd) throws RotelException {
362 sendCommand(cmd, null);
366 * Request the Rotel device to execute a command
368 * @param cmd the command to execute
369 * @param value the integer value to consider for volume, bass or treble adjustment
371 * @throws RotelException - In case of any problem
373 public void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
375 byte[] message = new byte[0];
378 if (cmd.getHexType() == 0) {
379 logger.debug("Send comman \"{}\" ignored: not available for HEX protocol", cmd.getName());
383 message = new byte[size];
385 message[idx++] = START;
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;
396 message[idx++] = checksum;
398 logger.debug("Send command \"{}\" => {}", cmd.getName(), HexUtils.bytesToHex(message));
402 messageStr = cmd.getAsciiCommandV1();
403 if (messageStr == null) {
404 logger.debug("Send comman \"{}\" ignored: not available for ASCII V1 protocol", cmd.getName());
410 messageStr += String.format("%d", value);
416 } else if (value > 0) {
417 messageStr += String.format("+%02d", value);
419 messageStr += String.format("-%02d", -value);
422 case DIMMER_LEVEL_SET:
423 if (value > 0 && model.getDimmerLevelMin() < 0) {
424 messageStr += String.format("+%d", value);
426 messageStr += String.format("%d", value);
433 if (!messageStr.endsWith("?")) {
436 message = messageStr.getBytes(StandardCharsets.US_ASCII);
437 logger.debug("Send command \"{}\" => {}", cmd.getName(), messageStr);
441 messageStr = cmd.getAsciiCommandV2();
442 if (messageStr == null) {
443 logger.debug("Send comman \"{}\" ignored: not available for ASCII V2 protocol", cmd.getName());
449 messageStr += String.format("%02d", value);
455 } else if (value > 0) {
456 messageStr += String.format("+%02d", value);
458 messageStr += String.format("-%02d", -value);
461 case DIMMER_LEVEL_SET:
462 if (value > 0 && model.getDimmerLevelMin() < 0) {
463 messageStr += String.format("+%d", value);
465 messageStr += String.format("%d", value);
472 if (!messageStr.endsWith("?")) {
475 message = messageStr.getBytes(StandardCharsets.US_ASCII);
476 logger.debug("Send command \"{}\" => {}", cmd.getName(), messageStr);
483 OutputStream dataOut = this.dataOut;
484 if (dataOut == null) {
485 throw new RotelException("Send command \"" + cmd.getName() + "\" failed: output stream is null");
488 dataOut.write(message);
490 } catch (IOException e) {
491 logger.debug("Send command \"{}\" failed: {}", cmd.getName(), e.getMessage());
492 throw new RotelException("Send command \"" + cmd.getName() + "\" failed", e);
494 logger.debug("Send command \"{}\" succeeded", cmd.getName());
498 * Validate the content of a feedback message
500 * @param responseMessage the buffer containing the feedback message
502 * @throws RotelException - If the message has unexpected content
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");
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");
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");
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");
540 int expectedLen = (responseMessage[3] == STANDARD_RESPONSE)
541 ? (5 + model.getRespNbChars() + model.getRespNbFlags())
542 : responseMessage.length;
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");
552 final byte checksum = computeCheckSum(responseMessage, expectedLen - 2);
553 if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
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");
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");
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");
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");
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");
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");
601 * Compute the checksum of a message
603 * @param message the buffer containing the message
604 * @param maxIdx the position in the buffer at which the sum has to be stopped
606 * @return the checksum as a byte
608 protected byte computeCheckSum(byte[] message, int maxIdx) {
610 for (int i = 1; i <= maxIdx; i++) {
611 result += (message[i] & 0x000000FF);
613 return (byte) (result & 0x000000FF);
617 * Add a listener to the list of listeners to be notified with events
619 * @param listener the listener
621 public void addEventListener(RotelMessageEventListener listener) {
622 listeners.add(listener);
626 * Remove a listener from the list of listeners to be notified with events
628 * @param listener the listener
630 public void removeEventListener(RotelMessageEventListener listener) {
631 listeners.remove(listener);
635 * Analyze an incoming message and dispatch corresponding (key, value) to the event listeners
637 * @param incomingMessage the received message
639 public void handleIncomingMessage(byte[] incomingMessage) {
640 logger.debug("handleIncomingMessage: bytes {}", HexUtils.bytesToHex(incomingMessage));
642 if (READ_ERROR.equals(incomingMessage)) {
643 dispatchKeyValue(KEY_ERROR, MSG_VALUE_ON);
648 validateResponse(incomingMessage);
649 } catch (RotelException e) {
653 if (protocol == RotelProtocol.HEX) {
654 handleValidHexMessage(incomingMessage);
656 handleValidAsciiMessage(incomingMessage);
661 * Analyze a valid HEX message and dispatch corresponding (key, value) to the event listeners
663 * @param incomingMessage the received message
665 private void handleValidHexMessage(byte[] incomingMessage) {
666 if (incomingMessage[3] != STANDARD_RESPONSE) {
670 final int idxChars = model.isCharsBeforeFlags() ? 4 : (4 + model.getRespNbFlags());
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;
679 String value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
680 logger.debug("handleValidHexMessage: chars *{}*", value);
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++) {
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) {
698 dispatchKeyValue(KEY_POWER_ZONE2, model.isZone2On(flags) ? POWER_ON : STANDBY);
699 } catch (RotelException e1) {
702 dispatchKeyValue(KEY_POWER_ZONE3, model.isZone3On(flags) ? POWER_ON : STANDBY);
703 } catch (RotelException e1) {
706 dispatchKeyValue(KEY_POWER_ZONE4, model.isZone4On(flags) ? POWER_ON : STANDBY);
707 } catch (RotelException e1) {
709 boolean checkMultiIn = false;
710 boolean checkSource = true;
712 if (model.isMultiInputOn(flags)) {
715 RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
716 RotelCommand cmd = source.getCommand();
718 String value2 = cmd.getAsciiCommandV2();
719 if (value2 != null) {
720 dispatchKeyValue(KEY_SOURCE, value2);
723 } catch (RotelException e1) {
726 } catch (RotelException e1) {
729 boolean checkStereo = true;
731 checkStereo = !model.isMoreThan2Channels(flags);
732 } catch (RotelException e1) {
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);
742 if (model.getRespNbChars() == 42) {
743 // 2 lines of 21 characters with a left part and a right part
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);
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);
756 value = new String(incomingMessage, idxChars, 21, StandardCharsets.US_ASCII);
757 dispatchKeyValue(KEY_LINE1, value);
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);
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);
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);
775 if (valueLowerCase.isEmpty()) {
776 dispatchKeyValue(KEY_POWER, POWER_OFF_DELAYED);
781 * Analyze a valid ASCII message and dispatch corresponding (key, value) to the event listeners
783 * @param incomingMessage the received message
785 public void handleValidAsciiMessage(byte[] incomingMessage) {
786 byte[] message = filterMessage(incomingMessage, model.getSpecialCharacters());
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) {
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()) {
802 String[] splittedValue = value.split("=");
803 if (splittedValue.length != 2) {
804 logger.debug("handleValidAsciiMessage: ignored message {}", value);
806 dispatchKeyValue(splittedValue[0].trim().toLowerCase(), splittedValue[1]);
808 } catch (PatternSyntaxException e) {
809 logger.debug("handleValidAsciiMessage: ignored message {}", value);
814 * Parse a text and dispatch appropriate (key, value) to the event listeners for found information
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
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();
832 dispatchKeyValue(KEY_RECORD_SEL, valueLowerCase.startsWith(KEY_HEX_RECORD) ? MSG_VALUE_ON : MSG_VALUE_OFF);
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");
842 dispatchKeyValue(KEY_ZONE, "1");
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);
855 logger.debug("Invalid value {} for zone mute", value);
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)) {
869 RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
870 RotelCommand cmd = source.getCommand();
872 String value2 = cmd.getAsciiCommandV2();
873 if (value2 != null) {
874 dispatchKeyValue(KEY_SOURCE, value2);
877 } catch (RotelException e1) {
879 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
880 logger.debug("Invalid value {} for MULTI IN", value);
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());
893 && (valueLowerCase.startsWith(KEY_HEX_MUSIC1) || valueLowerCase.startsWith(KEY_HEX_DSP1))) {
894 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP1.getFeedback());
896 && (valueLowerCase.startsWith(KEY_HEX_MUSIC2) || valueLowerCase.startsWith(KEY_HEX_DSP2))) {
897 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP2.getFeedback());
899 && (valueLowerCase.startsWith(KEY_HEX_MUSIC3) || valueLowerCase.startsWith(KEY_HEX_DSP3))) {
900 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP3.getFeedback());
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());
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)) {
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)) {
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)) {
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);
968 * Parse a text to identify a source
970 * @param text the text to be parsed
971 * @param acceptFollowMain true if follow main has to be considered in the search
973 * @return the identified source or null if no source is identified in the text
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)) {
981 source = model.getSourceFromName(RotelSource.CAT1_FOLLOW_MAIN.getName());
982 } catch (RotelException e) {
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()) {
998 private void parseSourceAndRecord(String text, boolean searchSource, boolean searchRecordAfterSource,
999 boolean multipleInfo) {
1000 RotelSource source = parseSource(text, false);
1001 if (source != null) {
1003 RotelCommand cmd = source.getCommand();
1005 String value2 = cmd.getAsciiCommandV2();
1006 if (value2 != null) {
1007 dispatchKeyValue(KEY_SOURCE, value2);
1008 if (!multipleInfo) {
1009 dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
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();
1021 value = cmd.getAsciiCommandV2();
1022 if (value != null) {
1023 dispatchKeyValue(KEY_RECORD, value);
1031 private String getSourceLabel(RotelSource source) {
1032 String label = sourcesLabels.get(source);
1033 return (label == null) ? source.getLabel() : label;
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();
1042 value = cmd.getAsciiCommandV2();
1043 if (value != null) {
1044 dispatchKeyValue(KEY_RECORD, value);
1048 logger.debug("Invalid value {} for record source", value);
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);
1065 logger.debug("Invalid value {} for zone mute", value);
1067 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1068 RotelSource source = parseSource(value, true);
1069 if (source != null) {
1070 RotelCommand cmd = source.getZone2Command();
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);
1081 logger.debug("Invalid value {} for zone 2 source", value);
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);
1099 logger.debug("Invalid value {} for zone mute", value);
1101 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1102 RotelSource source = parseSource(value, true);
1103 if (source != null) {
1104 RotelCommand cmd = source.getZone3Command();
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);
1115 logger.debug("Invalid value {} for zone 3 source", value);
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);
1133 logger.debug("Invalid value {} for zone mute", value);
1135 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
1136 RotelSource source = parseSource(value, true);
1137 if (source != null) {
1138 RotelCommand cmd = source.getZone4Command();
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);
1149 logger.debug("Invalid value {} for zone 4 source", value);
1155 * Extract from a string a number
1157 * @param value the string
1158 * @param startIndex the index in the string at which the integer has to be extracted
1160 * @return the number as a string with its sign and no blank between the sign and the digits
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();
1172 * Suppress certain sequences of bytes from a message
1174 * @param message the message as a table of bytes
1175 * @param bytesSequences the table containing the sequence of bytes to be ignored
1177 * @return the message without the unexpected sequence of bytes
1179 private byte[] filterMessage(byte[] message, byte[][] bytesSequences) {
1180 if (bytesSequences.length == 0) {
1183 byte[] filteredMsg = new byte[message.length];
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]) {
1199 ignoredLength = size;
1204 if (ignoredLength > 0) {
1205 srcIdx += ignoredLength;
1207 filteredMsg[dstIdx++] = message[srcIdx++];
1210 return Arrays.copyOf(filteredMsg, dstIdx);
1214 * Dispatch an event (key, value) to the event listeners
1216 * @param key the key
1217 * @param value the value
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);