2 * Copyright (c) 2010-2023 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.protocol.hex;
15 import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
17 import java.nio.charset.StandardCharsets;
18 import java.util.Arrays;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.rotel.internal.RotelException;
24 import org.openhab.binding.rotel.internal.RotelModel;
25 import org.openhab.binding.rotel.internal.communication.RotelCommand;
26 import org.openhab.binding.rotel.internal.communication.RotelDsp;
27 import org.openhab.binding.rotel.internal.communication.RotelFlagsMapping;
28 import org.openhab.binding.rotel.internal.communication.RotelSource;
29 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
30 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
31 import org.openhab.core.util.HexUtils;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * Class for handling the Rotel HEX protocol (build of command messages, decoding of incoming data)
38 * @author Laurent Garnier - Initial contribution
41 public class RotelHexProtocolHandler extends RotelAbstractProtocolHandler {
43 public static final byte START = (byte) 0xFE;
45 private static final String KEY1_HEX_VOLUME = "volume ";
46 private static final String KEY2_HEX_VOLUME = "vol ";
47 private static final String KEY_HEX_MUTE = "mute ";
48 private static final String KEY1_HEX_BASS = "bass ";
49 private static final String KEY2_HEX_BASS = "lf ";
50 private static final String KEY1_HEX_TREBLE = "treble ";
51 private static final String KEY2_HEX_TREBLE = "hf ";
52 private static final String KEY_HEX_MULTI_IN = "multi in ";
53 private static final String KEY_HEX_STEREO = "stereo";
54 private static final String KEY1_HEX_3CH = "3 stereo";
55 private static final String KEY2_HEX_3CH = "dolby 3 stereo";
56 private static final String KEY_HEX_5CH = "5ch stereo";
57 private static final String KEY_HEX_7CH = "7ch stereo";
58 private static final String KEY_HEX_MUSIC1 = "music 1";
59 private static final String KEY_HEX_MUSIC2 = "music 2";
60 private static final String KEY_HEX_MUSIC3 = "music 3";
61 private static final String KEY_HEX_MUSIC4 = "music 4";
62 private static final String KEY_HEX_DSP1 = "dsp 1";
63 private static final String KEY_HEX_DSP2 = "dsp 2";
64 private static final String KEY_HEX_DSP3 = "dsp 3";
65 private static final String KEY_HEX_DSP4 = "dsp 4";
66 private static final String KEY1_HEX_PROLOGIC = "prologic emu";
67 private static final String KEY2_HEX_PROLOGIC = "dolby pro logic";
68 private static final String KEY1_HEX_PLII_CINEMA = "prologic cin";
69 private static final String KEY2_HEX_PLII_CINEMA = "dolby pl c";
70 private static final String KEY1_HEX_PLII_MUSIC = "prologic mus";
71 private static final String KEY2_HEX_PLII_MUSIC = "dolby pl m";
72 private static final String KEY1_HEX_PLII_GAME = "prologic gam";
73 private static final String KEY2_HEX_PLII_GAME = "dolby pl g";
74 private static final String KEY1_HEX_PLIIX_CINEMA = "pl x cinema";
75 private static final String KEY2_HEX_PLIIX_CINEMA = "dolby pl x c";
76 private static final String KEY1_HEX_PLIIX_MUSIC = "pl x music";
77 private static final String KEY2_HEX_PLIIX_MUSIC = "dolby pl x m";
78 private static final String KEY1_HEX_PLIIX_GAME = "pl x game";
79 private static final String KEY2_HEX_PLIIX_GAME = "dolby pl x g";
80 private static final String KEY_HEX_PLIIZ = "dolby pl z";
81 private static final String KEY1_HEX_DTS_NEO6_CINEMA = "neo 6 cinema";
82 private static final String KEY2_HEX_DTS_NEO6_CINEMA = "dts neo:6 c";
83 private static final String KEY1_HEX_DTS_NEO6_MUSIC = "neo 6 music";
84 private static final String KEY2_HEX_DTS_NEO6_MUSIC = "dts neo:6 m";
85 private static final String KEY_HEX_DTS = "dts";
86 private static final String KEY_HEX_DTS_ES = "dts-es";
87 private static final String KEY_HEX_DTS_96 = "dts 96";
88 private static final String KEY_HEX_DD = "dolby digital";
89 private static final String KEY_HEX_DD_EX = "dolby d ex";
90 private static final String KEY_HEX_PCM = "pcm";
91 private static final String KEY_HEX_LPCM = "lpcm";
92 private static final String KEY_HEX_MPEG = "mpeg";
93 private static final String KEY_HEX_BYPASS = "bypass";
94 private static final String KEY1_HEX_ZONE2 = "zone ";
95 private static final String KEY2_HEX_ZONE2 = "zone2 ";
96 private static final String KEY_HEX_ZONE3 = "zone3 ";
97 private static final String KEY_HEX_ZONE4 = "zone4 ";
98 private static final String KEY_HEX_RECORD = "rec ";
99 private static final String SOURCE = "source";
101 private final Logger logger = LoggerFactory.getLogger(RotelHexProtocolHandler.class);
103 private final Map<RotelSource, String> sourcesLabels;
105 private final int size;
106 private final byte[] dataBuffer;
108 private boolean startCodeReached;
115 * @param model the Rotel model in use
116 * @param sourcesLabels the custom labels for sources
118 public RotelHexProtocolHandler(RotelModel model, Map<RotelSource, String> sourcesLabels) {
120 this.sourcesLabels = sourcesLabels;
121 this.size = (6 + model.getRespNbChars() + model.getRespNbFlags());
122 this.dataBuffer = new byte[size];
123 this.startCodeReached = false;
129 public RotelProtocol getProtocol() {
130 return RotelProtocol.HEX;
134 public byte[] buildCommandMessage(RotelCommand cmd, @Nullable Integer value) throws RotelException {
135 if (cmd.getHexType() == 0) {
136 throw new RotelException("Command \"" + cmd.getLabel() + "\" ignored: not available for HEX protocol");
139 byte[] message = new byte[size];
141 message[idx++] = START;
143 message[idx++] = model.getDeviceId();
144 message[idx++] = cmd.getHexType();
145 message[idx++] = (value == null) ? cmd.getHexKey() : (byte) (value & 0x000000FF);
146 final byte checksum = computeCheckSum(message, idx - 1);
147 if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
148 message = Arrays.copyOf(message, size + 1);
149 message[idx++] = (byte) 0xFD;
150 message[idx++] = ((checksum & 0x000000FF) == 0x000000FD) ? (byte) 0 : (byte) 1;
152 message[idx++] = checksum;
154 logger.debug("Command \"{}\" => {}", cmd, HexUtils.bytesToHex(message));
159 public void handleIncomingData(byte[] inDataBuffer, int length) {
160 for (int i = 0; i < length; i++) {
161 if (inDataBuffer[i] == RotelHexProtocolHandler.START) {
162 startCodeReached = true;
166 if (startCodeReached) {
168 dataBuffer[index++] = inDataBuffer[i];
171 count = inDataBuffer[i];
172 } else if ((count > 0) && (index == (count + 3))) {
173 if ((inDataBuffer[i] & 0x000000FF) == 0x000000FD) {
176 byte[] msg = Arrays.copyOf(dataBuffer, index);
177 handleIncomingMessage(msg);
178 startCodeReached = false;
186 * Validate the content of a feedback message
188 * @param responseMessage the buffer containing the feedback message
190 * @throws RotelException - If the message has unexpected content
193 protected void validateResponse(byte[] responseMessage) throws RotelException {
194 // Check minimum message length
195 if (responseMessage.length < 6) {
196 logger.debug("Unexpected message length: {}", responseMessage.length);
197 throw new RotelException("Unexpected message length");
201 if (responseMessage[0] != START) {
202 logger.debug("Unexpected START in response: {} rather than {}",
203 Integer.toHexString(responseMessage[0] & 0x000000FF), Integer.toHexString(START & 0x000000FF));
204 throw new RotelException("Unexpected START in response");
208 if (responseMessage[2] != model.getDeviceId()) {
209 logger.debug("Unexpected ID in response: {} rather than {}",
210 Integer.toHexString(responseMessage[2] & 0x000000FF),
211 Integer.toHexString(model.getDeviceId() & 0x000000FF));
212 throw new RotelException("Unexpected ID in response");
216 if (responseMessage[3] != STANDARD_RESPONSE && responseMessage[3] != TRIGGER_STATUS
217 && responseMessage[3] != SMART_DISPLAY_DATA_1 && responseMessage[3] != SMART_DISPLAY_DATA_2
218 && responseMessage[3] != PRIMARY_CMD && responseMessage[3] != MAIN_ZONE_CMD
219 && responseMessage[3] != RECORD_SRC_CMD && responseMessage[3] != ZONE2_CMD
220 && responseMessage[3] != ZONE3_CMD && responseMessage[3] != ZONE4_CMD
221 && responseMessage[3] != VOLUME_CMD && responseMessage[3] != ZONE2_VOLUME_CMD
222 && responseMessage[3] != ZONE3_VOLUME_CMD && responseMessage[3] != ZONE4_VOLUME_CMD
223 && responseMessage[3] != TRIGGER_CMD) {
224 logger.debug("Unexpected TYPE in response: {}", Integer.toHexString(responseMessage[3] & 0x000000FF));
225 throw new RotelException("Unexpected TYPE in response");
228 int expectedLen = (responseMessage[3] == STANDARD_RESPONSE)
229 ? (5 + model.getRespNbChars() + model.getRespNbFlags())
230 : responseMessage.length;
233 if (responseMessage[1] != (expectedLen - 3)) {
234 logger.debug("Unexpected COUNT in response: {} rather than {}",
235 Integer.toHexString(responseMessage[1] & 0x000000FF),
236 Integer.toHexString((expectedLen - 3) & 0x000000FF));
237 throw new RotelException("Unexpected COUNT in response");
240 final byte checksum = computeCheckSum(responseMessage, expectedLen - 2);
241 if ((checksum & 0x000000FF) == 0x000000FD || (checksum & 0x000000FF) == 0x000000FE) {
245 // Check message length
246 if (responseMessage.length != expectedLen) {
247 logger.debug("Unexpected message length: {} rather than {}", responseMessage.length, expectedLen);
248 throw new RotelException("Unexpected message length");
252 if ((checksum & 0x000000FF) == 0x000000FD) {
253 if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
254 || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 0) {
255 logger.debug("Invalid check sum in response: {} rather than FD00", HexUtils.bytesToHex(
256 Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
257 throw new RotelException("Invalid check sum in response");
259 } else if ((checksum & 0x000000FF) == 0x000000FE) {
260 if ((responseMessage[responseMessage.length - 2] & 0x000000FF) != 0x000000FD
261 || (responseMessage[responseMessage.length - 1] & 0x000000FF) != 1) {
262 logger.debug("Invalid check sum in response: {} rather than FD01", HexUtils.bytesToHex(
263 Arrays.copyOfRange(responseMessage, responseMessage.length - 2, responseMessage.length)));
264 throw new RotelException("Invalid check sum in response");
266 } else if ((checksum & 0x000000FF) != (responseMessage[responseMessage.length - 1] & 0x000000FF)) {
267 logger.debug("Invalid check sum in response: {} rather than {}",
268 Integer.toHexString(responseMessage[responseMessage.length - 1] & 0x000000FF),
269 Integer.toHexString(checksum & 0x000000FF));
270 throw new RotelException("Invalid check sum in response");
275 * Compute the checksum of a message
277 * @param message the buffer containing the message
278 * @param maxIdx the position in the buffer at which the sum has to be stopped
280 * @return the checksum as a byte
282 public static byte computeCheckSum(byte[] message, int maxIdx) {
284 for (int i = 1; i <= maxIdx; i++) {
285 result += (message[i] & 0x000000FF);
287 return (byte) (result & 0x000000FF);
291 * Analyze a valid HEX message and dispatch corresponding (key, value) to the event listeners
293 * @param incomingMessage the received message
296 protected void handleValidMessage(byte[] incomingMessage) {
297 if (incomingMessage[3] != STANDARD_RESPONSE) {
301 final int idxChars = model.isCharsBeforeFlags() ? 4 : (4 + model.getRespNbFlags());
303 // Replace characters with code < 32 by a space before converting to a string
304 for (int i = idxChars; i < (idxChars + model.getRespNbChars()); i++) {
305 if (incomingMessage[i] < 0x20) {
306 incomingMessage[i] = 0x20;
310 String value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
311 logger.debug("handleValidHexMessage: chars *{}*", value);
313 final int idxFlags = model.isCharsBeforeFlags() ? (4 + model.getRespNbChars()) : 4;
314 final byte[] flags = Arrays.copyOfRange(incomingMessage, idxFlags, idxFlags + model.getRespNbFlags());
315 if (logger.isTraceEnabled()) {
316 for (int i = 1; i <= flags.length; i++) {
318 logger.trace("handleValidHexMessage: Flag {} = {} bits 7-0 = {} {} {} {} {} {} {} {}", i,
319 Integer.toHexString(flags[i - 1] & 0x000000FF), RotelFlagsMapping.isBitFlagOn(flags, i, 7),
320 RotelFlagsMapping.isBitFlagOn(flags, i, 6), RotelFlagsMapping.isBitFlagOn(flags, i, 5),
321 RotelFlagsMapping.isBitFlagOn(flags, i, 4), RotelFlagsMapping.isBitFlagOn(flags, i, 3),
322 RotelFlagsMapping.isBitFlagOn(flags, i, 2), RotelFlagsMapping.isBitFlagOn(flags, i, 1),
323 RotelFlagsMapping.isBitFlagOn(flags, i, 0));
324 } catch (RotelException e1) {
329 dispatchKeyValue(KEY_POWER_ZONE2, model.isZone2On(flags) ? POWER_ON : STANDBY);
330 } catch (RotelException e1) {
331 // Can't get zone power information from flags data, so we just do not notify of this information that way
334 dispatchKeyValue(KEY_POWER_ZONE3, model.isZone3On(flags) ? POWER_ON : STANDBY);
335 } catch (RotelException e1) {
336 // Can't get zone power information from flags data, so we just do not notify of this information that way
339 dispatchKeyValue(KEY_POWER_ZONE4, model.isZone4On(flags) ? POWER_ON : STANDBY);
340 } catch (RotelException e1) {
341 // Can't get zone power information from flags data, so we just do not notify of this information that way
343 boolean checkMultiIn = false;
344 boolean checkSource = true;
346 if (model.isMultiInputOn(flags)) {
349 RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
350 RotelCommand cmd = source.getCommand();
352 String value2 = cmd.getAsciiCommandV2();
353 if (value2 != null) {
354 dispatchKeyValue(KEY_SOURCE, value2);
357 } catch (RotelException e1) {
358 // MULTI source not declared for the model (should not happen), we do not notify of this source
361 } catch (RotelException e1) {
362 // Can't get status of multiple input source from flags data, checkMultiIn is set to true to get this
363 // information in another way
366 boolean checkStereo = true;
368 checkStereo = !model.isMoreThan2Channels(flags);
369 } catch (RotelException e1) {
370 // Can't get stereo information from flags data, checkStereo is set to true to get this information in
374 String valueLowerCase = value.trim().toLowerCase();
375 if (!valueLowerCase.isEmpty() && !valueLowerCase.startsWith(KEY1_HEX_ZONE2)
376 && !valueLowerCase.startsWith(KEY2_HEX_ZONE2) && !valueLowerCase.startsWith(KEY_HEX_ZONE3)
377 && !valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
378 dispatchKeyValue(KEY_POWER, POWER_ON);
381 if (model.getRespNbChars() == 42) {
382 // 2 lines of 21 characters with a left part and a right part
385 value = new String(incomingMessage, idxChars, 14, StandardCharsets.US_ASCII);
386 logger.debug("handleValidHexMessage: line 1 left *{}*", value);
387 parseText(value, checkSource, checkMultiIn, false, false, false, false, false, true);
390 value = new String(incomingMessage, idxChars + 14, 7, StandardCharsets.US_ASCII);
391 logger.debug("handleValidHexMessage: line 1 right *{}*", value);
392 parseText(value, false, false, false, false, false, false, false, true);
395 value = new String(incomingMessage, idxChars, 21, StandardCharsets.US_ASCII);
396 dispatchKeyValue(KEY_LINE1, value);
399 value = new String(incomingMessage, idxChars + 35, 7, StandardCharsets.US_ASCII);
400 logger.debug("handleValidHexMessage: line 2 right *{}*", value);
401 parseText(value, false, false, false, false, false, false, false, true);
404 value = new String(incomingMessage, idxChars + 21, 21, StandardCharsets.US_ASCII);
405 logger.debug("handleValidHexMessage: line 2 *{}*", value);
406 parseText(value, false, false, true, true, false, true, true, true);
407 dispatchKeyValue(KEY_LINE2, value);
409 value = new String(incomingMessage, idxChars, model.getRespNbChars(), StandardCharsets.US_ASCII);
410 parseText(value, checkSource, checkMultiIn, true, false, true, true, checkStereo, false);
411 dispatchKeyValue(KEY_LINE1, value);
414 if (valueLowerCase.isEmpty()) {
415 dispatchKeyValue(KEY_POWER, POWER_OFF_DELAYED);
420 * Parse a text and dispatch appropriate (key, value) to the event listeners for found information
422 * @param text the text to be parsed
423 * @param searchSource true if a source has to be searched in the text
424 * @param searchMultiIn true if MULTI IN indication has to be searched in the text
425 * @param searchZone true if a zone information has to be searched in the text
426 * @param searchRecord true if a record source has to be searched in the text
427 * @param searchRecordAfterSource true if a record source has to be searched in the text after the a found source
428 * @param searchDsp true if a DSP mode has to be searched in the text
429 * @param searchStereo true if a STEREO has to be considered in the search
430 * @param multipleInfo true if source and volume/mute are provided separately
432 private void parseText(String text, boolean searchSource, boolean searchMultiIn, boolean searchZone,
433 boolean searchRecord, boolean searchRecordAfterSource, boolean searchDsp, boolean searchStereo,
434 boolean multipleInfo) {
435 String value = text.trim();
436 String valueLowerCase = value.toLowerCase();
438 dispatchKeyValue(KEY_RECORD_SEL, valueLowerCase.startsWith(KEY_HEX_RECORD) ? MSG_VALUE_ON : MSG_VALUE_OFF);
441 if (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2)) {
442 dispatchKeyValue(KEY_ZONE, "2");
443 } else if (valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
444 dispatchKeyValue(KEY_ZONE, "3");
445 } else if (valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
446 dispatchKeyValue(KEY_ZONE, "4");
448 dispatchKeyValue(KEY_ZONE, "1");
451 if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
452 value = extractNumber(value,
453 valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
454 dispatchKeyValue(KEY_VOLUME, value);
455 dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
456 } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
457 value = value.substring(KEY_HEX_MUTE.length()).trim();
458 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
459 dispatchKeyValue(KEY_MUTE, MSG_VALUE_ON);
461 logger.debug("Invalid value {} for zone mute", value);
463 } else if (valueLowerCase.startsWith(KEY1_HEX_BASS) || valueLowerCase.startsWith(KEY2_HEX_BASS)) {
464 value = extractNumber(value,
465 valueLowerCase.startsWith(KEY1_HEX_BASS) ? KEY1_HEX_BASS.length() : KEY2_HEX_BASS.length());
466 dispatchKeyValue(KEY_BASS, value);
467 } else if (valueLowerCase.startsWith(KEY1_HEX_TREBLE) || valueLowerCase.startsWith(KEY2_HEX_TREBLE)) {
468 value = extractNumber(value,
469 valueLowerCase.startsWith(KEY1_HEX_TREBLE) ? KEY1_HEX_TREBLE.length() : KEY2_HEX_TREBLE.length());
470 dispatchKeyValue(KEY_TREBLE, value);
471 } else if (searchMultiIn && valueLowerCase.startsWith(KEY_HEX_MULTI_IN)) {
472 value = value.substring(KEY_HEX_MULTI_IN.length()).trim();
473 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
475 RotelSource source = model.getSourceFromName(RotelSource.CAT1_MULTI.getName());
476 RotelCommand cmd = source.getCommand();
478 String value2 = cmd.getAsciiCommandV2();
479 if (value2 != null) {
480 dispatchKeyValue(KEY_SOURCE, value2);
483 } catch (RotelException e1) {
484 // MULTI source not declared for the model (should not happen), we do not notify of this source
486 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
487 logger.debug("Invalid value {} for MULTI IN", value);
489 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_BYPASS)) {
490 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
491 } else if (searchDsp && searchStereo && valueLowerCase.startsWith(KEY_HEX_STEREO)) {
492 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
493 } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_3CH) || valueLowerCase.startsWith(KEY2_HEX_3CH))) {
494 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO3.getFeedback());
495 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_5CH)) {
496 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO5.getFeedback());
497 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_7CH)) {
498 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_STEREO7.getFeedback());
500 && (valueLowerCase.startsWith(KEY_HEX_MUSIC1) || valueLowerCase.startsWith(KEY_HEX_DSP1))) {
501 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP1.getFeedback());
503 && (valueLowerCase.startsWith(KEY_HEX_MUSIC2) || valueLowerCase.startsWith(KEY_HEX_DSP2))) {
504 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP2.getFeedback());
506 && (valueLowerCase.startsWith(KEY_HEX_MUSIC3) || valueLowerCase.startsWith(KEY_HEX_DSP3))) {
507 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP3.getFeedback());
509 && (valueLowerCase.startsWith(KEY_HEX_MUSIC4) || valueLowerCase.startsWith(KEY_HEX_DSP4))) {
510 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_DSP4.getFeedback());
511 } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_CINEMA)
512 || valueLowerCase.startsWith(KEY2_HEX_PLII_CINEMA) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_CINEMA)
513 || searchDsp && valueLowerCase.startsWith(KEY2_HEX_PLIIX_CINEMA))) {
514 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_CINEMA.getFeedback());
515 } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_MUSIC)
516 || valueLowerCase.startsWith(KEY2_HEX_PLII_MUSIC) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_MUSIC)
517 || valueLowerCase.startsWith(KEY2_HEX_PLIIX_MUSIC))) {
518 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_MUSIC.getFeedback());
519 } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_PLII_GAME)
520 || valueLowerCase.startsWith(KEY2_HEX_PLII_GAME) || valueLowerCase.startsWith(KEY1_HEX_PLIIX_GAME)
521 || valueLowerCase.startsWith(KEY2_HEX_PLIIX_GAME))) {
522 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT2_PLII_GAME.getFeedback());
523 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PLIIZ)) {
524 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PLIIZ.getFeedback());
526 && (valueLowerCase.startsWith(KEY1_HEX_PROLOGIC) || valueLowerCase.startsWith(KEY2_HEX_PROLOGIC))) {
527 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_PROLOGIC.getFeedback());
528 } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_CINEMA)
529 || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_CINEMA))) {
530 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_CINEMA.getFeedback());
531 } else if (searchDsp && (valueLowerCase.startsWith(KEY1_HEX_DTS_NEO6_MUSIC)
532 || valueLowerCase.startsWith(KEY2_HEX_DTS_NEO6_MUSIC))) {
533 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NEO6_MUSIC.getFeedback());
534 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_ES)) {
535 logger.debug("DTS-ES");
536 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
537 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS_96)) {
538 logger.debug("DTS 96");
539 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
540 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DTS)) {
542 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
543 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD_EX)) {
544 logger.debug("DD-EX");
545 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
546 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_DD)) {
548 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
549 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_LPCM)) {
550 logger.debug("LPCM");
551 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
552 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_PCM)) {
554 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
555 } else if (searchDsp && valueLowerCase.startsWith(KEY_HEX_MPEG)) {
556 logger.debug("MPEG");
557 dispatchKeyValue(KEY_DSP_MODE, RotelDsp.CAT4_NONE.getFeedback());
558 } else if (searchZone
559 && (valueLowerCase.startsWith(KEY1_HEX_ZONE2) || valueLowerCase.startsWith(KEY2_HEX_ZONE2))) {
560 value = value.substring(
561 valueLowerCase.startsWith(KEY1_HEX_ZONE2) ? KEY1_HEX_ZONE2.length() : KEY2_HEX_ZONE2.length());
562 parseZone2(value, multipleInfo);
563 } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE3)) {
564 parseZone3(value.substring(KEY_HEX_ZONE3.length()), multipleInfo);
565 } else if (searchZone && valueLowerCase.startsWith(KEY_HEX_ZONE4)) {
566 parseZone4(value.substring(KEY_HEX_ZONE4.length()), multipleInfo);
567 } else if (searchRecord && valueLowerCase.startsWith(KEY_HEX_RECORD)) {
568 parseRecord(value.substring(KEY_HEX_RECORD.length()));
569 } else if (searchSource || searchRecordAfterSource) {
570 parseSourceAndRecord(value, searchSource, searchRecordAfterSource, multipleInfo);
575 * Parse a text to identify a source
577 * @param text the text to be parsed
578 * @param acceptFollowMain true if follow main has to be considered in the search
580 * @return the identified source or null if no source is identified in the text
582 private @Nullable RotelSource parseSource(String text, boolean acceptFollowMain) {
583 String value = text.trim();
584 RotelSource source = null;
585 if (!value.isEmpty()) {
586 if (acceptFollowMain && SOURCE.equalsIgnoreCase(value)) {
588 source = model.getSourceFromName(RotelSource.CAT1_FOLLOW_MAIN.getName());
589 } catch (RotelException e) {
590 // MAIN (follow main zone source) source not declared for the model, we return null
593 for (RotelSource src : sourcesLabels.keySet()) {
594 String label = sourcesLabels.get(src);
595 if (label != null && value.startsWith(label)) {
596 if (source == null || sourcesLabels.get(source).length() < label.length()) {
606 private void parseSourceAndRecord(String text, boolean searchSource, boolean searchRecordAfterSource,
607 boolean multipleInfo) {
608 RotelSource source = parseSource(text, false);
609 if (source != null) {
611 RotelCommand cmd = source.getCommand();
613 String value2 = cmd.getAsciiCommandV2();
614 if (value2 != null) {
615 dispatchKeyValue(KEY_SOURCE, value2);
617 dispatchKeyValue(KEY_MUTE, MSG_VALUE_OFF);
623 if (searchRecordAfterSource) {
624 String value = text.substring(getSourceLabel(source).length()).trim();
625 source = parseSource(value, true);
626 if (source != null) {
627 RotelCommand cmd = source.getRecordCommand();
629 value = cmd.getAsciiCommandV2();
631 dispatchKeyValue(KEY_RECORD, value);
639 private String getSourceLabel(RotelSource source) {
640 String label = sourcesLabels.get(source);
641 return (label == null) ? source.getLabel() : label;
644 private void parseRecord(String text) {
645 String value = text.trim();
646 RotelSource source = parseSource(value, true);
647 if (source != null) {
648 RotelCommand cmd = source.getRecordCommand();
650 value = cmd.getAsciiCommandV2();
652 dispatchKeyValue(KEY_RECORD, value);
656 logger.debug("Invalid value {} for record source", value);
660 private void parseZone2(String text, boolean multipleInfo) {
661 String value = text.trim();
662 String valueLowerCase = value.toLowerCase();
663 if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
664 value = extractNumber(value,
665 valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
666 dispatchKeyValue(KEY_VOLUME_ZONE2, value);
667 dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
668 } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
669 value = value.substring(KEY_HEX_MUTE.length()).trim();
670 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
671 dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_ON);
673 logger.debug("Invalid value {} for zone mute", value);
675 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
676 RotelSource source = parseSource(value, true);
677 if (source != null) {
678 RotelCommand cmd = source.getZoneCommand(2);
680 value = cmd.getAsciiCommandV2();
682 dispatchKeyValue(KEY_SOURCE_ZONE2, value);
684 dispatchKeyValue(KEY_MUTE_ZONE2, MSG_VALUE_OFF);
689 logger.debug("Invalid value {} for zone 2 source", value);
694 private void parseZone3(String text, boolean multipleInfo) {
695 String value = text.trim();
696 String valueLowerCase = value.toLowerCase();
697 if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
698 value = extractNumber(value,
699 valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
700 dispatchKeyValue(KEY_VOLUME_ZONE3, value);
701 dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
702 } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
703 value = value.substring(KEY_HEX_MUTE.length()).trim();
704 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
705 dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_ON);
707 logger.debug("Invalid value {} for zone mute", value);
709 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
710 RotelSource source = parseSource(value, true);
711 if (source != null) {
712 RotelCommand cmd = source.getZoneCommand(3);
714 value = cmd.getAsciiCommandV2();
716 dispatchKeyValue(KEY_SOURCE_ZONE3, value);
718 dispatchKeyValue(KEY_MUTE_ZONE3, MSG_VALUE_OFF);
723 logger.debug("Invalid value {} for zone 3 source", value);
728 private void parseZone4(String text, boolean multipleInfo) {
729 String value = text.trim();
730 String valueLowerCase = value.toLowerCase();
731 if (valueLowerCase.startsWith(KEY1_HEX_VOLUME) || valueLowerCase.startsWith(KEY2_HEX_VOLUME)) {
732 value = extractNumber(value,
733 valueLowerCase.startsWith(KEY1_HEX_VOLUME) ? KEY1_HEX_VOLUME.length() : KEY2_HEX_VOLUME.length());
734 dispatchKeyValue(KEY_VOLUME_ZONE4, value);
735 dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
736 } else if (valueLowerCase.startsWith(KEY_HEX_MUTE)) {
737 value = value.substring(KEY_HEX_MUTE.length()).trim();
738 if (MSG_VALUE_ON.equalsIgnoreCase(value)) {
739 dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_ON);
741 logger.debug("Invalid value {} for zone mute", value);
743 } else if (!MSG_VALUE_OFF.equalsIgnoreCase(value)) {
744 RotelSource source = parseSource(value, true);
745 if (source != null) {
746 RotelCommand cmd = source.getZoneCommand(4);
748 value = cmd.getAsciiCommandV2();
750 dispatchKeyValue(KEY_SOURCE_ZONE4, value);
752 dispatchKeyValue(KEY_MUTE_ZONE4, MSG_VALUE_OFF);
757 logger.debug("Invalid value {} for zone 4 source", value);
763 * Extract from a string a number
765 * @param value the string
766 * @param startIndex the index in the string at which the integer has to be extracted
768 * @return the number as a string with its sign and no blank between the sign and the digits
770 private String extractNumber(String value, int startIndex) {
771 String result = value.substring(startIndex).trim();
772 // Delete possible blank(s) between the sign and the number
773 if (result.startsWith("+") || result.startsWith("-")) {
774 result = result.substring(0, 1) + result.substring(1, result.length()).trim();