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 static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
16 import static org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler.START;
18 import java.io.InterruptedIOException;
19 import java.nio.charset.StandardCharsets;
20 import java.util.Arrays;
22 import java.util.Objects;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.rotel.internal.RotelException;
27 import org.openhab.binding.rotel.internal.RotelModel;
28 import org.openhab.binding.rotel.internal.RotelPlayStatus;
29 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
30 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
31 import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * Class for simulating the communication with the Rotel device
38 * @author Laurent Garnier - Initial contribution
41 public class RotelSimuConnector extends RotelConnector {
43 private static final int STEP_TONE_LEVEL = 1;
45 private final Logger logger = LoggerFactory.getLogger(RotelSimuConnector.class);
47 private final RotelModel model;
48 private final RotelProtocol protocol;
49 private final Map<RotelSource, String> sourcesLabels;
51 private Object lock = new Object();
53 private byte[] feedbackMsg = new byte[1];
54 private int idxInFeedbackMsg = feedbackMsg.length;
56 private boolean power;
57 private boolean powerZone2;
58 private boolean powerZone3;
59 private boolean powerZone4;
60 private RotelSource source = RotelSource.CAT0_CD;
61 private RotelSource recordSource = RotelSource.CAT1_CD;
62 private RotelSource sourceZone2 = RotelSource.CAT1_CD;
63 private RotelSource sourceZone3 = RotelSource.CAT1_CD;
64 private RotelSource sourceZone4 = RotelSource.CAT1_CD;
65 private boolean multiinput;
66 private RotelDsp dsp = RotelDsp.CAT4_NONE;
67 private int volume = 50;
69 private int volumeZone2 = 20;
70 private boolean muteZone2;
71 private int volumeZone3 = 30;
72 private boolean muteZone3;
73 private int volumeZone4 = 40;
74 private boolean muteZone4;
77 private boolean showTreble;
78 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
79 private int track = 1;
80 private boolean selectingRecord;
84 private int minVolume;
85 private int maxVolume;
86 private int minToneLevel;
87 private int maxToneLevel;
92 * @param model the projector model in use
93 * @param protocolHandler the protocol handler
94 * @param sourcesLabels the custom labels for sources
95 * @param readerThreadName the name of thread to be created
97 public RotelSimuConnector(RotelModel model, RotelAbstractProtocolHandler protocolHandler,
98 Map<RotelSource, String> sourcesLabels, String readerThreadName) {
99 super(protocolHandler, true, readerThreadName);
101 this.protocol = protocolHandler.getProtocol();
102 this.sourcesLabels = sourcesLabels;
104 this.maxVolume = model.hasVolumeControl() ? model.getVolumeMax() : 0;
105 this.maxToneLevel = model.hasToneControl() ? model.getToneLevelMax() : 0;
106 this.minToneLevel = -this.maxToneLevel;
110 public synchronized void open() throws RotelException {
111 logger.debug("Opening simulated connection");
112 readerThread.start();
114 logger.debug("Simulated connection opened");
118 public synchronized void close() {
119 logger.debug("Closing simulated connection");
122 logger.debug("Simulated connection closed");
126 protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
127 synchronized (lock) {
128 int len = feedbackMsg.length - idxInFeedbackMsg;
130 if (len > dataBuffer.length) {
131 len = dataBuffer.length;
133 System.arraycopy(feedbackMsg, idxInFeedbackMsg, dataBuffer, 0, len);
134 idxInFeedbackMsg += len;
138 // Give more chance to someone else than the reader thread to get the lock
141 } catch (InterruptedException e) {
142 Thread.currentThread().interrupt();
148 * Built the simulated feedback message for a sent command
150 * @param cmd the sent command
151 * @param value the integer value considered in the sent command for volume, bass or treble adjustment
153 public void buildFeedbackMessage(RotelCommand cmd, @Nullable Integer value) {
154 String text = buildSourceLine1Response();
155 String textLine1Left = buildSourceLine1LeftResponse();
156 String textLine1Right = buildVolumeLine1RightResponse();
157 String textLine2 = "";
158 String textAscii = "";
159 boolean accepted = true;
160 boolean resetZone = true;
162 case DISPLAY_REFRESH:
165 case MAIN_ZONE_POWER_OFF:
167 text = buildSourceLine1Response();
168 textLine1Left = buildSourceLine1LeftResponse();
169 textLine1Right = buildVolumeLine1RightResponse();
170 textAscii = buildPowerAsciiResponse();
173 case MAIN_ZONE_POWER_ON:
175 text = buildSourceLine1Response();
176 textLine1Left = buildSourceLine1LeftResponse();
177 textLine1Right = buildVolumeLine1RightResponse();
178 textAscii = buildPowerAsciiResponse();
181 textAscii = buildPowerAsciiResponse();
183 case ZONE2_POWER_OFF:
185 text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
186 powerZone2, sourceZone2);
192 text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
193 powerZone2, sourceZone2);
197 case ZONE3_POWER_OFF:
199 text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
205 text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
209 case ZONE4_POWER_OFF:
211 text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
217 text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
221 case RECORD_FONCTION_SELECT:
222 if (model.getNbAdditionalZones() >= 1 && model.getZoneSelectCmd() == cmd) {
224 if (showZone > model.getNbAdditionalZones()) {
234 selectingRecord = power;
236 textLine2 = buildRecordResponse();
237 } else if (showZone == 2) {
238 selectingRecord = false;
239 text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
240 powerZone2, sourceZone2);
241 } else if (showZone == 3) {
242 selectingRecord = false;
243 text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
244 } else if (showZone == 4) {
245 selectingRecord = false;
246 text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
251 if (model.getNbAdditionalZones() == 0
252 || (model.getNbAdditionalZones() > 1 && model.getZoneSelectCmd() == cmd)
253 || (showZone == 1 && model.getZoneSelectCmd() != cmd)) {
256 if (model.getZoneSelectCmd() == cmd) {
257 if (!power && !powerZone2) {
260 } else if (showZone == 2) {
261 powerZone2 = !powerZone2;
267 powerZone2 = !powerZone2;
268 } else if (showZone == 3) {
269 powerZone3 = !powerZone3;
270 } else if (showZone == 4) {
271 powerZone4 = !powerZone4;
275 text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
276 powerZone2, sourceZone2);
277 } else if (showZone == 3) {
278 text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
279 } else if (showZone == 4) {
280 text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
289 if (!accepted && powerZone2) {
292 case ZONE2_VOLUME_UP:
293 if (volumeZone2 < maxVolume) {
296 text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
297 muteZone2, volumeZone2);
299 case ZONE2_VOLUME_DOWN:
300 if (volumeZone2 > minVolume) {
303 text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
304 muteZone2, volumeZone2);
306 case ZONE2_VOLUME_SET:
310 text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
311 muteZone2, volumeZone2);
314 if (!model.hasZone2Commands() && model.getNbAdditionalZones() >= 1 && showZone == 2) {
315 if (volumeZone2 < maxVolume) {
318 text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
319 muteZone2, volumeZone2);
326 if (!model.hasZone2Commands() && model.getNbAdditionalZones() >= 1 && showZone == 2) {
327 if (volumeZone2 > minVolume) {
330 text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
331 muteZone2, volumeZone2);
338 if (!model.hasZone2Commands() && model.getNbAdditionalZones() >= 1 && showZone == 2) {
342 text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
343 muteZone2, volumeZone2);
349 case ZONE2_MUTE_TOGGLE:
350 muteZone2 = !muteZone2;
351 text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
352 muteZone2, volumeZone2);
356 text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
357 muteZone2, volumeZone2);
361 text = textLine2 = buildZoneVolumeResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
362 muteZone2, volumeZone2);
370 sourceZone2 = model.getZone2SourceFromCommand(cmd);
372 text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
373 powerZone2, sourceZone2);
378 } catch (RotelException e) {
381 if (!accepted && !model.hasZone2Commands() && model.getNbAdditionalZones() >= 1 && showZone == 2) {
383 sourceZone2 = model.getSourceFromCommand(cmd);
385 text = textLine2 = buildZonePowerResponse(model.getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
386 powerZone2, sourceZone2);
390 } catch (RotelException e) {
394 if (!accepted && powerZone3) {
397 case ZONE3_VOLUME_UP:
398 if (volumeZone3 < maxVolume) {
401 text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
403 case ZONE3_VOLUME_DOWN:
404 if (volumeZone3 > minVolume) {
407 text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
409 case ZONE3_VOLUME_SET:
413 text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
415 case ZONE3_MUTE_TOGGLE:
416 muteZone3 = !muteZone3;
417 text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
421 text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
425 text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
433 sourceZone3 = model.getZone3SourceFromCommand(cmd);
435 text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
440 } catch (RotelException e) {
444 if (!accepted && powerZone4) {
447 case ZONE4_VOLUME_UP:
448 if (volumeZone4 < maxVolume) {
451 text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
453 case ZONE4_VOLUME_DOWN:
454 if (volumeZone4 > minVolume) {
457 text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
459 case ZONE4_VOLUME_SET:
463 text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
465 case ZONE4_MUTE_TOGGLE:
466 muteZone4 = !muteZone4;
467 text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
471 text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
475 text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
483 sourceZone4 = model.getZone4SourceFromCommand(cmd);
485 text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
490 } catch (RotelException e) {
494 if (!accepted && power) {
498 textAscii = buildAsciiResponse(
499 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, AUTO);
502 textAscii = buildAsciiResponse(
503 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, MANUAL);
506 textAscii = buildAsciiResponse(KEY_VOLUME_MIN, minVolume);
509 textAscii = buildAsciiResponse(KEY_VOLUME_MAX, maxVolume);
512 case MAIN_ZONE_VOLUME_UP:
513 if (volume < maxVolume) {
516 text = buildVolumeLine1Response();
517 textLine1Right = buildVolumeLine1RightResponse();
518 textAscii = buildVolumeAsciiResponse();
521 case MAIN_ZONE_VOLUME_DOWN:
522 if (volume > minVolume) {
525 text = buildVolumeLine1Response();
526 textLine1Right = buildVolumeLine1RightResponse();
527 textAscii = buildVolumeAsciiResponse();
533 text = buildVolumeLine1Response();
534 textLine1Right = buildVolumeLine1RightResponse();
535 textAscii = buildVolumeAsciiResponse();
538 textAscii = buildVolumeAsciiResponse();
541 case MAIN_ZONE_MUTE_TOGGLE:
543 text = buildSourceLine1Response();
544 textLine1Right = buildVolumeLine1RightResponse();
545 textAscii = buildMuteAsciiResponse();
548 case MAIN_ZONE_MUTE_ON:
550 text = buildSourceLine1Response();
551 textLine1Right = buildVolumeLine1RightResponse();
552 textAscii = buildMuteAsciiResponse();
555 case MAIN_ZONE_MUTE_OFF:
557 text = buildSourceLine1Response();
558 textLine1Right = buildVolumeLine1RightResponse();
559 textAscii = buildMuteAsciiResponse();
562 textAscii = buildMuteAsciiResponse();
565 textAscii = buildAsciiResponse(KEY_TONE_MAX, "%02d", maxToneLevel);
568 if (bass < maxToneLevel) {
569 bass += STEP_TONE_LEVEL;
571 text = buildBassLine1Response();
572 textLine1Right = buildBassLine1RightResponse();
573 textAscii = buildBassAsciiResponse();
576 if (bass > minToneLevel) {
577 bass -= STEP_TONE_LEVEL;
579 text = buildBassLine1Response();
580 textLine1Right = buildBassLine1RightResponse();
581 textAscii = buildBassAsciiResponse();
587 text = buildBassLine1Response();
588 textLine1Right = buildBassLine1RightResponse();
589 textAscii = buildBassAsciiResponse();
592 textAscii = buildBassAsciiResponse();
595 if (treble < maxToneLevel) {
596 treble += STEP_TONE_LEVEL;
598 text = buildTrebleLine1Response();
599 textLine1Right = buildTrebleLine1RightResponse();
600 textAscii = buildTrebleAsciiResponse();
603 if (treble > minToneLevel) {
604 treble -= STEP_TONE_LEVEL;
606 text = buildTrebleLine1Response();
607 textLine1Right = buildTrebleLine1RightResponse();
608 textAscii = buildTrebleAsciiResponse();
614 text = buildTrebleLine1Response();
615 textLine1Right = buildTrebleLine1RightResponse();
616 textAscii = buildTrebleAsciiResponse();
619 textAscii = buildTrebleAsciiResponse();
621 case TONE_CONTROL_SELECT:
622 showTreble = !showTreble;
624 text = buildTrebleLine1Response();
625 textLine1Right = buildTrebleLine1RightResponse();
627 text = buildBassLine1Response();
628 textLine1Right = buildBassLine1RightResponse();
632 playStatus = RotelPlayStatus.PLAYING;
633 textAscii = buildPlayStatusAsciiResponse();
636 playStatus = RotelPlayStatus.STOPPED;
637 textAscii = buildPlayStatusAsciiResponse();
640 switch (playStatus) {
642 playStatus = RotelPlayStatus.PAUSED;
646 playStatus = RotelPlayStatus.PLAYING;
649 textAscii = buildPlayStatusAsciiResponse();
653 textAscii = buildPlayStatusAsciiResponse();
657 textAscii = buildTrackAsciiResponse();
663 textAscii = buildTrackAsciiResponse();
666 textAscii = buildTrackAsciiResponse();
668 case SOURCE_MULTI_INPUT:
669 multiinput = !multiinput;
670 text = "MULTI IN " + (multiinput ? "ON" : "OFF");
672 source = model.getSourceFromCommand(cmd);
673 textLine1Left = buildSourceLine1LeftResponse();
674 textAscii = buildSourceAsciiResponse();
676 } catch (RotelException e) {
680 textAscii = buildSourceAsciiResponse();
683 dsp = RotelDsp.CAT4_NONE;
684 textLine2 = "STEREO";
685 textAscii = buildDspAsciiResponse();
688 dsp = RotelDsp.CAT4_STEREO3;
689 textLine2 = "DOLBY 3 STEREO";
690 textAscii = buildDspAsciiResponse();
693 dsp = RotelDsp.CAT4_STEREO5;
694 textLine2 = "5CH STEREO";
695 textAscii = buildDspAsciiResponse();
698 dsp = RotelDsp.CAT4_STEREO7;
699 textLine2 = "7CH STEREO";
700 textAscii = buildDspAsciiResponse();
703 dsp = RotelDsp.CAT5_STEREO9;
704 textAscii = buildDspAsciiResponse();
707 dsp = RotelDsp.CAT5_STEREO11;
708 textAscii = buildDspAsciiResponse();
711 dsp = RotelDsp.CAT4_DSP1;
713 textAscii = buildDspAsciiResponse();
716 dsp = RotelDsp.CAT4_DSP2;
718 textAscii = buildDspAsciiResponse();
721 dsp = RotelDsp.CAT4_DSP3;
723 textAscii = buildDspAsciiResponse();
726 dsp = RotelDsp.CAT4_DSP4;
728 textAscii = buildDspAsciiResponse();
731 dsp = RotelDsp.CAT4_PROLOGIC;
732 textLine2 = "DOLBY PRO LOGIC";
733 textAscii = buildDspAsciiResponse();
736 dsp = RotelDsp.CAT4_PLII_CINEMA;
737 textLine2 = "DOLBY PL C";
738 textAscii = buildDspAsciiResponse();
741 dsp = RotelDsp.CAT4_PLII_MUSIC;
742 textLine2 = "DOLBY PL M";
743 textAscii = buildDspAsciiResponse();
746 dsp = RotelDsp.CAT4_PLII_GAME;
747 textLine2 = "DOLBY PL G";
748 textAscii = buildDspAsciiResponse();
751 dsp = RotelDsp.CAT4_PLIIZ;
752 textLine2 = "DOLBY PL z";
753 textAscii = buildDspAsciiResponse();
756 dsp = RotelDsp.CAT4_NEO6_MUSIC;
757 textLine2 = "DTS Neo:6 M";
758 textAscii = buildDspAsciiResponse();
761 dsp = RotelDsp.CAT4_NEO6_CINEMA;
762 textLine2 = "DTS Neo:6 C";
763 textAscii = buildDspAsciiResponse();
766 dsp = RotelDsp.CAT5_ATMOS;
767 textAscii = buildDspAsciiResponse();
770 dsp = RotelDsp.CAT5_NEURAL_X;
771 textAscii = buildDspAsciiResponse();
774 dsp = RotelDsp.CAT4_BYPASS;
775 textLine2 = "BYPASS";
776 textAscii = buildDspAsciiResponse();
779 textAscii = buildDspAsciiResponse();
782 textAscii = buildAsciiResponse(KEY_FREQ, "44.1");
784 case DIMMER_LEVEL_SET:
788 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
790 case DIMMER_LEVEL_GET:
791 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
799 source = model.getMainZoneSourceFromCommand(cmd);
800 text = buildSourceLine1Response();
801 textLine1Left = buildSourceLine1LeftResponse();
802 textAscii = buildSourceAsciiResponse();
804 } catch (RotelException e) {
809 if (selectingRecord && !model.hasOtherThanPrimaryCommands()) {
810 recordSource = model.getSourceFromCommand(cmd);
812 source = model.getSourceFromCommand(cmd);
814 text = buildSourceLine1Response();
815 textLine1Left = buildSourceLine1LeftResponse();
816 textAscii = buildSourceAsciiResponse();
819 } catch (RotelException e) {
824 recordSource = model.getRecordSourceFromCommand(cmd);
825 text = buildSourceLine1Response();
826 textLine2 = buildRecordResponse();
828 } catch (RotelException e) {
837 if (cmd != RotelCommand.RECORD_FONCTION_SELECT) {
838 selectingRecord = false;
844 if (model.getRespNbChars() == 42) {
845 while (textLine1Left.length() < 14) {
846 textLine1Left += " ";
848 while (textLine1Right.length() < 7) {
849 textLine1Right += " ";
851 while (textLine2.length() < 21) {
854 text = textLine1Left + textLine1Right + textLine2;
857 if (protocol == RotelProtocol.HEX) {
858 byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), model.getRespNbChars());
859 byte[] flags = new byte[model.getRespNbFlags()];
861 model.setMultiInput(flags, multiinput);
862 } catch (RotelException e) {
865 model.setZone2(flags, powerZone2);
866 } catch (RotelException e) {
869 model.setZone3(flags, powerZone3);
870 } catch (RotelException e) {
873 model.setZone4(flags, powerZone4);
874 } catch (RotelException e) {
876 int size = 6 + model.getRespNbChars() + model.getRespNbFlags();
877 byte[] dataBuffer = new byte[size];
879 dataBuffer[idx++] = START;
880 dataBuffer[idx++] = (byte) (size - 4);
881 dataBuffer[idx++] = model.getDeviceId();
882 dataBuffer[idx++] = STANDARD_RESPONSE;
883 if (model.isCharsBeforeFlags()) {
884 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
885 idx += model.getRespNbChars();
886 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
887 idx += model.getRespNbFlags();
889 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
890 idx += model.getRespNbFlags();
891 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
892 idx += model.getRespNbChars();
894 byte checksum = RotelHexProtocolHandler.computeCheckSum(dataBuffer, idx - 1);
895 if ((checksum & 0x000000FF) == 0x000000FD) {
896 dataBuffer[idx++] = (byte) 0xFD;
897 dataBuffer[idx++] = 0;
898 } else if ((checksum & 0x000000FF) == 0x000000FE) {
899 dataBuffer[idx++] = (byte) 0xFD;
900 dataBuffer[idx++] = 1;
902 dataBuffer[idx++] = checksum;
904 synchronized (lock) {
905 feedbackMsg = Arrays.copyOf(dataBuffer, idx);
906 idxInFeedbackMsg = 0;
909 String command = textAscii + (protocol == RotelProtocol.ASCII_V1 ? "!" : "$");
910 synchronized (lock) {
911 feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
912 idxInFeedbackMsg = 0;
917 private String buildAsciiResponse(String key, String value) {
918 return String.format("%s=%s", key, value);
921 private String buildAsciiResponse(String key, int value) {
922 return buildAsciiResponse(key, "%d", value);
925 private String buildAsciiResponse(String key, String format, int value) {
926 return String.format("%s=" + format, key, value);
929 private String buildAsciiResponse(String key, boolean value) {
930 return buildAsciiResponse(key, value ? MSG_VALUE_ON : MSG_VALUE_OFF);
933 private String buildPowerAsciiResponse() {
934 return buildAsciiResponse(KEY_POWER, power ? POWER_ON : STANDBY);
937 private String buildVolumeAsciiResponse() {
938 return buildAsciiResponse(KEY_VOLUME, "%02d", volume);
941 private String buildMuteAsciiResponse() {
942 return buildAsciiResponse(KEY_MUTE, mute);
945 private String buildBassAsciiResponse() {
948 result = buildAsciiResponse(KEY_BASS, "000");
949 } else if (bass > 0) {
950 result = buildAsciiResponse(KEY_BASS, "+%02d", bass);
952 result = buildAsciiResponse(KEY_BASS, "-%02d", -bass);
957 private String buildTrebleAsciiResponse() {
960 result = buildAsciiResponse(KEY_TREBLE, "000");
961 } else if (treble > 0) {
962 result = buildAsciiResponse(KEY_TREBLE, "+%02d", treble);
964 result = buildAsciiResponse(KEY_TREBLE, "-%02d", -treble);
969 private String buildPlayStatusAsciiResponse() {
971 switch (playStatus) {
982 return buildAsciiResponse(protocol == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_STATUS, status);
985 private String buildTrackAsciiResponse() {
986 return buildAsciiResponse(KEY_TRACK, "%03d", track);
989 private String buildSourceAsciiResponse() {
991 RotelCommand command = source.getCommand();
992 if (command != null) {
993 str = command.getAsciiCommandV2();
995 return buildAsciiResponse(KEY_SOURCE, (str == null) ? "" : str);
998 private String buildDspAsciiResponse() {
999 return buildAsciiResponse(KEY_DSP_MODE, dsp.getFeedback());
1002 private String buildSourceLine1Response() {
1009 text = getSourceLabel(source, false) + " " + getSourceLabel(recordSource, true);
1014 private String buildSourceLine1LeftResponse() {
1019 text = getSourceLabel(source, false);
1024 private String buildRecordResponse() {
1029 text = "REC " + getSourceLabel(recordSource, true);
1034 private String buildZonePowerResponse(String zone, boolean powerZone, RotelSource sourceZone) {
1035 String state = powerZone ? getSourceLabel(sourceZone, true) : "OFF";
1036 return zone + " " + state;
1039 private String buildVolumeLine1Response() {
1041 if (volume == minVolume) {
1042 text = " VOLUME MIN ";
1043 } else if (volume == maxVolume) {
1044 text = " VOLUME MAX ";
1046 text = String.format(" VOLUME %02d ", volume);
1051 private String buildVolumeLine1RightResponse() {
1057 } else if (volume == minVolume) {
1059 } else if (volume == maxVolume) {
1062 text = String.format("VOL %02d", volume);
1067 private String buildZoneVolumeResponse(String zone, boolean muted, int vol) {
1070 text = zone + " MUTE ON";
1071 } else if (vol == minVolume) {
1072 text = zone + " VOL MIN";
1073 } else if (vol == maxVolume) {
1074 text = zone + " VOL MAX";
1076 text = String.format("%s VOL %02d", zone, vol);
1081 private String buildBassLine1Response() {
1083 if (bass == minToneLevel) {
1084 text = " BASS MIN ";
1085 } else if (bass == maxToneLevel) {
1086 text = " BASS MAX ";
1087 } else if (bass == 0) {
1089 } else if (bass > 0) {
1090 text = String.format(" BASS +%02d ", bass);
1092 text = String.format(" BASS -%02d ", -bass);
1097 private String buildBassLine1RightResponse() {
1099 if (bass == minToneLevel) {
1101 } else if (bass == maxToneLevel) {
1103 } else if (bass == 0) {
1105 } else if (bass > 0) {
1106 text = String.format("LF + %02d", bass);
1108 text = String.format("LF - %02d", -bass);
1113 private String buildTrebleLine1Response() {
1115 if (treble == minToneLevel) {
1116 text = " TREBLE MIN ";
1117 } else if (treble == maxToneLevel) {
1118 text = " TREBLE MAX ";
1119 } else if (treble == 0) {
1120 text = " TREBLE 0 ";
1121 } else if (treble > 0) {
1122 text = String.format(" TREBLE +%02d ", treble);
1124 text = String.format(" TREBLE -%02d ", -treble);
1129 private String buildTrebleLine1RightResponse() {
1131 if (treble == minToneLevel) {
1133 } else if (treble == maxToneLevel) {
1135 } else if (treble == 0) {
1137 } else if (treble > 0) {
1138 text = String.format("HF + %02d", treble);
1140 text = String.format("HF - %02d", -treble);
1145 private String getSourceLabel(RotelSource source, boolean considerFollowMain) {
1147 if (considerFollowMain && source.getName().equals(RotelSource.CAT1_FOLLOW_MAIN.getName())) {
1150 label = Objects.requireNonNullElse(sourcesLabels.get(source), source.getLabel());