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.InterruptedIOException;
16 import java.nio.charset.StandardCharsets;
17 import java.util.Arrays;
19 import java.util.Objects;
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.RotelPlayStatus;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
30 * Class for simulating the communication with the Rotel device
32 * @author Laurent Garnier - Initial contribution
35 public class RotelSimuConnector extends RotelConnector {
37 private final Logger logger = LoggerFactory.getLogger(RotelSimuConnector.class);
39 private static final int STEP_TONE_LEVEL = 1;
41 private Object lock = new Object();
43 private byte[] feedbackMsg = new byte[1];
44 private int idxInFeedbackMsg = feedbackMsg.length;
46 private boolean power;
47 private boolean powerZone2;
48 private boolean powerZone3;
49 private boolean powerZone4;
50 private RotelSource source = RotelSource.CAT0_CD;
51 private RotelSource recordSource = RotelSource.CAT1_CD;
52 private RotelSource sourceZone2 = RotelSource.CAT1_CD;
53 private RotelSource sourceZone3 = RotelSource.CAT1_CD;
54 private RotelSource sourceZone4 = RotelSource.CAT1_CD;
55 private boolean multiinput;
56 private RotelDsp dsp = RotelDsp.CAT4_NONE;
57 private int volume = 50;
59 private int volumeZone2 = 20;
60 private boolean muteZone2;
61 private int volumeZone3 = 30;
62 private boolean muteZone3;
63 private int volumeZone4 = 40;
64 private boolean muteZone4;
67 private boolean showTreble;
68 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
69 private int track = 1;
70 private boolean selectingRecord;
74 private int minVolume;
75 private int maxVolume;
76 private int minToneLevel;
77 private int maxToneLevel;
82 * @param model the projector model in use
83 * @param protocol the protocol to be used
84 * @param readerThreadName the name of thread to be created
86 public RotelSimuConnector(RotelModel model, RotelProtocol protocol, Map<RotelSource, String> sourcesLabels,
87 String readerThreadName) {
88 super(model, protocol, sourcesLabels, true, readerThreadName);
90 this.maxVolume = model.hasVolumeControl() ? model.getVolumeMax() : 0;
91 this.maxToneLevel = model.hasToneControl() ? model.getToneLevelMax() : 0;
92 this.minToneLevel = -this.maxToneLevel;
96 public synchronized void open() throws RotelException {
97 logger.debug("Opening simulated connection");
98 Thread thread = new RotelReaderThread(this, readerThreadName);
99 setReaderThread(thread);
102 logger.debug("Simulated connection opened");
106 public synchronized void close() {
107 logger.debug("Closing simulated connection");
110 logger.debug("Simulated connection closed");
114 protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
115 synchronized (lock) {
116 int len = feedbackMsg.length - idxInFeedbackMsg;
118 if (len > dataBuffer.length) {
119 len = dataBuffer.length;
121 System.arraycopy(feedbackMsg, idxInFeedbackMsg, dataBuffer, 0, len);
122 idxInFeedbackMsg += len;
126 // Give more chance to someone else than the reader thread to get the lock
129 } catch (InterruptedException e) {
130 Thread.currentThread().interrupt();
136 public void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
137 super.sendCommand(cmd, value);
138 if ((getProtocol() == RotelProtocol.HEX && cmd.getHexType() != 0)
139 || (getProtocol() == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
140 || (getProtocol() == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
141 buildFeedbackMessage(cmd, value);
146 * Built the simulated feedback message for a sent command
148 * @param cmd the sent command
149 * @param value the integer value considered in the sent command for volume, bass or treble adjustment
151 private void buildFeedbackMessage(RotelCommand cmd, @Nullable Integer value) {
152 String text = buildSourceLine1Response();
153 String textLine1Left = buildSourceLine1LeftResponse();
154 String textLine1Right = buildVolumeLine1RightResponse();
155 String textLine2 = "";
156 String textAscii = "";
157 boolean accepted = true;
158 boolean resetZone = true;
160 case DISPLAY_REFRESH:
163 case MAIN_ZONE_POWER_OFF:
165 text = buildSourceLine1Response();
166 textLine1Left = buildSourceLine1LeftResponse();
167 textLine1Right = buildVolumeLine1RightResponse();
168 textAscii = buildPowerAsciiResponse();
171 case MAIN_ZONE_POWER_ON:
173 text = buildSourceLine1Response();
174 textLine1Left = buildSourceLine1LeftResponse();
175 textLine1Right = buildVolumeLine1RightResponse();
176 textAscii = buildPowerAsciiResponse();
179 textAscii = buildPowerAsciiResponse();
181 case ZONE2_POWER_OFF:
183 text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
184 powerZone2, sourceZone2);
190 text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
191 powerZone2, sourceZone2);
195 case ZONE3_POWER_OFF:
197 text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
203 text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
207 case ZONE4_POWER_OFF:
209 text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
215 text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
219 case RECORD_FONCTION_SELECT:
220 if (getModel().getNbAdditionalZones() >= 1 && getModel().getZoneSelectCmd() == cmd) {
222 if (showZone > getModel().getNbAdditionalZones()) {
232 selectingRecord = power;
234 textLine2 = buildRecordResponse();
235 } else if (showZone == 2) {
236 selectingRecord = false;
237 text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
238 powerZone2, sourceZone2);
239 } else if (showZone == 3) {
240 selectingRecord = false;
241 text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
242 } else if (showZone == 4) {
243 selectingRecord = false;
244 text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
249 if (getModel().getNbAdditionalZones() == 0
250 || (getModel().getNbAdditionalZones() > 1 && getModel().getZoneSelectCmd() == cmd)
251 || (showZone == 1 && getModel().getZoneSelectCmd() != cmd)) {
254 if (getModel().getZoneSelectCmd() == cmd) {
255 if (!power && !powerZone2) {
258 } else if (showZone == 2) {
259 powerZone2 = !powerZone2;
265 powerZone2 = !powerZone2;
266 } else if (showZone == 3) {
267 powerZone3 = !powerZone3;
268 } else if (showZone == 4) {
269 powerZone4 = !powerZone4;
273 text = textLine2 = buildZonePowerResponse(
274 getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE", powerZone2, sourceZone2);
275 } else if (showZone == 3) {
276 text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
277 } else if (showZone == 4) {
278 text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
287 if (!accepted && powerZone2) {
290 case ZONE2_VOLUME_UP:
291 if (volumeZone2 < maxVolume) {
294 text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
295 muteZone2, volumeZone2);
297 case ZONE2_VOLUME_DOWN:
298 if (volumeZone2 > minVolume) {
301 text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
302 muteZone2, volumeZone2);
304 case ZONE2_VOLUME_SET:
308 text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
309 muteZone2, volumeZone2);
312 if (!getModel().hasZone2Commands() && getModel().getNbAdditionalZones() >= 1 && showZone == 2) {
313 if (volumeZone2 < maxVolume) {
316 text = textLine2 = buildZoneVolumeResponse(
317 getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE", muteZone2, volumeZone2);
324 if (!getModel().hasZone2Commands() && getModel().getNbAdditionalZones() >= 1 && showZone == 2) {
325 if (volumeZone2 > minVolume) {
328 text = textLine2 = buildZoneVolumeResponse(
329 getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE", muteZone2, volumeZone2);
336 if (!getModel().hasZone2Commands() && getModel().getNbAdditionalZones() >= 1 && showZone == 2) {
340 text = textLine2 = buildZoneVolumeResponse(
341 getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE", muteZone2, volumeZone2);
347 case ZONE2_MUTE_TOGGLE:
348 muteZone2 = !muteZone2;
349 text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
350 muteZone2, volumeZone2);
354 text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
355 muteZone2, volumeZone2);
359 text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
360 muteZone2, volumeZone2);
368 sourceZone2 = getModel().getZone2SourceFromCommand(cmd);
370 text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
371 powerZone2, sourceZone2);
376 } catch (RotelException e) {
379 if (!accepted && !getModel().hasZone2Commands() && getModel().getNbAdditionalZones() >= 1
382 sourceZone2 = getModel().getSourceFromCommand(cmd);
384 text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
385 powerZone2, sourceZone2);
389 } catch (RotelException e) {
393 if (!accepted && powerZone3) {
396 case ZONE3_VOLUME_UP:
397 if (volumeZone3 < maxVolume) {
400 text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
402 case ZONE3_VOLUME_DOWN:
403 if (volumeZone3 > minVolume) {
406 text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
408 case ZONE3_VOLUME_SET:
412 text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
414 case ZONE3_MUTE_TOGGLE:
415 muteZone3 = !muteZone3;
416 text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
420 text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
424 text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
432 sourceZone3 = getModel().getZone3SourceFromCommand(cmd);
434 text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
439 } catch (RotelException e) {
443 if (!accepted && powerZone4) {
446 case ZONE4_VOLUME_UP:
447 if (volumeZone4 < maxVolume) {
450 text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
452 case ZONE4_VOLUME_DOWN:
453 if (volumeZone4 > minVolume) {
456 text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
458 case ZONE4_VOLUME_SET:
462 text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
464 case ZONE4_MUTE_TOGGLE:
465 muteZone4 = !muteZone4;
466 text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
470 text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
474 text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
482 sourceZone4 = getModel().getZone4SourceFromCommand(cmd);
484 text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
489 } catch (RotelException e) {
493 if (!accepted && power) {
497 textAscii = buildAsciiResponse(
498 getProtocol() == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, "AUTO");
501 textAscii = buildAsciiResponse(
502 getProtocol() == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, "MANUAL");
505 textAscii = buildAsciiResponse(KEY_VOLUME_MIN, minVolume);
508 textAscii = buildAsciiResponse(KEY_VOLUME_MAX, maxVolume);
511 case MAIN_ZONE_VOLUME_UP:
512 if (volume < maxVolume) {
515 text = buildVolumeLine1Response();
516 textLine1Right = buildVolumeLine1RightResponse();
517 textAscii = buildVolumeAsciiResponse();
520 case MAIN_ZONE_VOLUME_DOWN:
521 if (volume > minVolume) {
524 text = buildVolumeLine1Response();
525 textLine1Right = buildVolumeLine1RightResponse();
526 textAscii = buildVolumeAsciiResponse();
532 text = buildVolumeLine1Response();
533 textLine1Right = buildVolumeLine1RightResponse();
534 textAscii = buildVolumeAsciiResponse();
537 textAscii = buildVolumeAsciiResponse();
540 case MAIN_ZONE_MUTE_TOGGLE:
542 text = buildSourceLine1Response();
543 textLine1Right = buildVolumeLine1RightResponse();
544 textAscii = buildMuteAsciiResponse();
547 case MAIN_ZONE_MUTE_ON:
549 text = buildSourceLine1Response();
550 textLine1Right = buildVolumeLine1RightResponse();
551 textAscii = buildMuteAsciiResponse();
554 case MAIN_ZONE_MUTE_OFF:
556 text = buildSourceLine1Response();
557 textLine1Right = buildVolumeLine1RightResponse();
558 textAscii = buildMuteAsciiResponse();
561 textAscii = buildMuteAsciiResponse();
564 textAscii = buildAsciiResponse(KEY_TONE_MAX, "%02d", maxToneLevel);
567 if (bass < maxToneLevel) {
568 bass += STEP_TONE_LEVEL;
570 text = buildBassLine1Response();
571 textLine1Right = buildBassLine1RightResponse();
572 textAscii = buildBassAsciiResponse();
575 if (bass > minToneLevel) {
576 bass -= STEP_TONE_LEVEL;
578 text = buildBassLine1Response();
579 textLine1Right = buildBassLine1RightResponse();
580 textAscii = buildBassAsciiResponse();
586 text = buildBassLine1Response();
587 textLine1Right = buildBassLine1RightResponse();
588 textAscii = buildBassAsciiResponse();
591 textAscii = buildBassAsciiResponse();
594 if (treble < maxToneLevel) {
595 treble += STEP_TONE_LEVEL;
597 text = buildTrebleLine1Response();
598 textLine1Right = buildTrebleLine1RightResponse();
599 textAscii = buildTrebleAsciiResponse();
602 if (treble > minToneLevel) {
603 treble -= STEP_TONE_LEVEL;
605 text = buildTrebleLine1Response();
606 textLine1Right = buildTrebleLine1RightResponse();
607 textAscii = buildTrebleAsciiResponse();
613 text = buildTrebleLine1Response();
614 textLine1Right = buildTrebleLine1RightResponse();
615 textAscii = buildTrebleAsciiResponse();
618 textAscii = buildTrebleAsciiResponse();
620 case TONE_CONTROL_SELECT:
621 showTreble = !showTreble;
623 text = buildTrebleLine1Response();
624 textLine1Right = buildTrebleLine1RightResponse();
626 text = buildBassLine1Response();
627 textLine1Right = buildBassLine1RightResponse();
631 playStatus = RotelPlayStatus.PLAYING;
632 textAscii = buildPlayStatusAsciiResponse();
635 playStatus = RotelPlayStatus.STOPPED;
636 textAscii = buildPlayStatusAsciiResponse();
639 switch (playStatus) {
641 playStatus = RotelPlayStatus.PAUSED;
645 playStatus = RotelPlayStatus.PLAYING;
648 textAscii = buildPlayStatusAsciiResponse();
652 textAscii = buildPlayStatusAsciiResponse();
656 textAscii = buildTrackAsciiResponse();
662 textAscii = buildTrackAsciiResponse();
665 textAscii = buildTrackAsciiResponse();
667 case SOURCE_MULTI_INPUT:
668 multiinput = !multiinput;
669 text = "MULTI IN " + (multiinput ? "ON" : "OFF");
671 source = getModel().getSourceFromCommand(cmd);
672 textLine1Left = buildSourceLine1LeftResponse();
673 textAscii = buildSourceAsciiResponse();
675 } catch (RotelException e) {
679 textAscii = buildSourceAsciiResponse();
682 dsp = RotelDsp.CAT4_NONE;
683 textLine2 = "STEREO";
684 textAscii = buildDspAsciiResponse();
687 dsp = RotelDsp.CAT4_STEREO3;
688 textLine2 = "DOLBY 3 STEREO";
689 textAscii = buildDspAsciiResponse();
692 dsp = RotelDsp.CAT4_STEREO5;
693 textLine2 = "5CH STEREO";
694 textAscii = buildDspAsciiResponse();
697 dsp = RotelDsp.CAT4_STEREO7;
698 textLine2 = "7CH STEREO";
699 textAscii = buildDspAsciiResponse();
702 dsp = RotelDsp.CAT5_STEREO9;
703 textAscii = buildDspAsciiResponse();
706 dsp = RotelDsp.CAT5_STEREO11;
707 textAscii = buildDspAsciiResponse();
710 dsp = RotelDsp.CAT4_DSP1;
712 textAscii = buildDspAsciiResponse();
715 dsp = RotelDsp.CAT4_DSP2;
717 textAscii = buildDspAsciiResponse();
720 dsp = RotelDsp.CAT4_DSP3;
722 textAscii = buildDspAsciiResponse();
725 dsp = RotelDsp.CAT4_DSP4;
727 textAscii = buildDspAsciiResponse();
730 dsp = RotelDsp.CAT4_PROLOGIC;
731 textLine2 = "DOLBY PRO LOGIC";
732 textAscii = buildDspAsciiResponse();
735 dsp = RotelDsp.CAT4_PLII_CINEMA;
736 textLine2 = "DOLBY PL C";
737 textAscii = buildDspAsciiResponse();
740 dsp = RotelDsp.CAT4_PLII_MUSIC;
741 textLine2 = "DOLBY PL M";
742 textAscii = buildDspAsciiResponse();
745 dsp = RotelDsp.CAT4_PLII_GAME;
746 textLine2 = "DOLBY PL G";
747 textAscii = buildDspAsciiResponse();
750 dsp = RotelDsp.CAT4_PLIIZ;
751 textLine2 = "DOLBY PL z";
752 textAscii = buildDspAsciiResponse();
755 dsp = RotelDsp.CAT4_NEO6_MUSIC;
756 textLine2 = "DTS Neo:6 M";
757 textAscii = buildDspAsciiResponse();
760 dsp = RotelDsp.CAT4_NEO6_CINEMA;
761 textLine2 = "DTS Neo:6 C";
762 textAscii = buildDspAsciiResponse();
765 dsp = RotelDsp.CAT5_ATMOS;
766 textAscii = buildDspAsciiResponse();
769 dsp = RotelDsp.CAT5_NEURAL_X;
770 textAscii = buildDspAsciiResponse();
773 dsp = RotelDsp.CAT4_BYPASS;
774 textLine2 = "BYPASS";
775 textAscii = buildDspAsciiResponse();
778 textAscii = buildDspAsciiResponse();
781 textAscii = buildAsciiResponse(KEY_FREQ, "44.1");
783 case DIMMER_LEVEL_SET:
787 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
789 case DIMMER_LEVEL_GET:
790 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
798 source = getModel().getMainZoneSourceFromCommand(cmd);
799 text = buildSourceLine1Response();
800 textLine1Left = buildSourceLine1LeftResponse();
801 textAscii = buildSourceAsciiResponse();
803 } catch (RotelException e) {
808 if (selectingRecord && !getModel().hasOtherThanPrimaryCommands()) {
809 recordSource = getModel().getSourceFromCommand(cmd);
811 source = getModel().getSourceFromCommand(cmd);
813 text = buildSourceLine1Response();
814 textLine1Left = buildSourceLine1LeftResponse();
815 textAscii = buildSourceAsciiResponse();
818 } catch (RotelException e) {
823 recordSource = getModel().getRecordSourceFromCommand(cmd);
824 text = buildSourceLine1Response();
825 textLine2 = buildRecordResponse();
827 } catch (RotelException e) {
836 if (cmd != RotelCommand.RECORD_FONCTION_SELECT) {
837 selectingRecord = false;
843 if (getModel().getRespNbChars() == 42) {
844 while (textLine1Left.length() < 14) {
845 textLine1Left += " ";
847 while (textLine1Right.length() < 7) {
848 textLine1Right += " ";
850 while (textLine2.length() < 21) {
853 text = textLine1Left + textLine1Right + textLine2;
856 if (getProtocol() == RotelProtocol.HEX) {
857 byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), getModel().getRespNbChars());
858 byte[] flags = new byte[getModel().getRespNbFlags()];
860 getModel().setMultiInput(flags, multiinput);
861 } catch (RotelException e) {
864 getModel().setZone2(flags, powerZone2);
865 } catch (RotelException e) {
868 getModel().setZone3(flags, powerZone3);
869 } catch (RotelException e) {
872 getModel().setZone4(flags, powerZone4);
873 } catch (RotelException e) {
875 int size = 6 + getModel().getRespNbChars() + getModel().getRespNbFlags();
876 byte[] dataBuffer = new byte[size];
878 dataBuffer[idx++] = START;
879 dataBuffer[idx++] = (byte) (size - 4);
880 dataBuffer[idx++] = getModel().getDeviceId();
881 dataBuffer[idx++] = STANDARD_RESPONSE;
882 if (getModel().isCharsBeforeFlags()) {
883 System.arraycopy(chars, 0, dataBuffer, idx, getModel().getRespNbChars());
884 idx += getModel().getRespNbChars();
885 System.arraycopy(flags, 0, dataBuffer, idx, getModel().getRespNbFlags());
886 idx += getModel().getRespNbFlags();
888 System.arraycopy(flags, 0, dataBuffer, idx, getModel().getRespNbFlags());
889 idx += getModel().getRespNbFlags();
890 System.arraycopy(chars, 0, dataBuffer, idx, getModel().getRespNbChars());
891 idx += getModel().getRespNbChars();
893 byte checksum = computeCheckSum(dataBuffer, idx - 1);
894 if ((checksum & 0x000000FF) == 0x000000FD) {
895 dataBuffer[idx++] = (byte) 0xFD;
896 dataBuffer[idx++] = 0;
897 } else if ((checksum & 0x000000FF) == 0x000000FE) {
898 dataBuffer[idx++] = (byte) 0xFD;
899 dataBuffer[idx++] = 1;
901 dataBuffer[idx++] = checksum;
903 synchronized (lock) {
904 feedbackMsg = Arrays.copyOf(dataBuffer, idx);
905 idxInFeedbackMsg = 0;
908 String command = textAscii + (getProtocol() == RotelProtocol.ASCII_V1 ? "!" : "$");
909 synchronized (lock) {
910 feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
911 idxInFeedbackMsg = 0;
916 private String buildAsciiResponse(String key, String value) {
917 return String.format("%s=%s", key, value);
920 private String buildAsciiResponse(String key, int value) {
921 return buildAsciiResponse(key, "%d", value);
924 private String buildAsciiResponse(String key, String format, int value) {
925 return String.format("%s=" + format, key, value);
928 private String buildAsciiResponse(String key, boolean value) {
929 return buildAsciiResponse(key, value ? MSG_VALUE_ON : MSG_VALUE_OFF);
932 private String buildPowerAsciiResponse() {
933 return buildAsciiResponse(KEY_POWER, power ? POWER_ON : STANDBY);
936 private String buildVolumeAsciiResponse() {
937 return buildAsciiResponse(KEY_VOLUME, "%02d", volume);
940 private String buildMuteAsciiResponse() {
941 return buildAsciiResponse(KEY_MUTE, mute);
944 private String buildBassAsciiResponse() {
947 result = buildAsciiResponse(KEY_BASS, "000");
948 } else if (bass > 0) {
949 result = buildAsciiResponse(KEY_BASS, "+%02d", bass);
951 result = buildAsciiResponse(KEY_BASS, "-%02d", -bass);
956 private String buildTrebleAsciiResponse() {
959 result = buildAsciiResponse(KEY_TREBLE, "000");
960 } else if (treble > 0) {
961 result = buildAsciiResponse(KEY_TREBLE, "+%02d", treble);
963 result = buildAsciiResponse(KEY_TREBLE, "-%02d", -treble);
968 private String buildPlayStatusAsciiResponse() {
970 switch (playStatus) {
981 return buildAsciiResponse(getProtocol() == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_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());