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;
21 import java.util.List;
23 import java.util.Objects;
24 import java.util.StringJoiner;
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.binding.rotel.internal.RotelPlayStatus;
31 import org.openhab.binding.rotel.internal.RotelRepeatMode;
32 import org.openhab.binding.rotel.internal.protocol.RotelAbstractProtocolHandler;
33 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
34 import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
39 * Class for simulating the communication with the Rotel device
41 * @author Laurent Garnier - Initial contribution
44 public class RotelSimuConnector extends RotelConnector {
46 private static final int STEP_TONE_LEVEL = 1;
48 private final Logger logger = LoggerFactory.getLogger(RotelSimuConnector.class);
50 private final RotelModel model;
51 private final RotelProtocol protocol;
52 private final Map<RotelSource, String> sourcesLabels;
54 private Object lock = new Object();
56 private byte[] feedbackMsg = new byte[1];
57 private int idxInFeedbackMsg = feedbackMsg.length;
59 private boolean[] powers = { false, false, false, false, false };
60 private RotelSource[] sources;
61 private RotelSource recordSource;
62 private boolean multiinput;
63 private RotelDsp dsp = RotelDsp.CAT4_NONE;
64 private int[] volumes = { 50, 10, 20, 30, 40 };
65 private boolean[] mutes = { false, false, false, false, false };
66 private boolean tcbypass;
67 private int[] basses = { 0, 0, 0, 0, 0 };
68 private int[] trebles = { 0, 0, 0, 0, 0 };
69 private int[] balances = { 0, 0, 0, 0, 0 };
70 private boolean showTreble;
71 private boolean speakerA = true;
72 private boolean speakerB = false;
73 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
74 private int track = 1;
75 private boolean randomMode;
76 private RotelRepeatMode repeatMode = RotelRepeatMode.OFF;
77 private boolean selectingRecord;
81 private int minVolume;
82 private int maxVolume;
83 private int minToneLevel;
84 private int maxToneLevel;
85 private int minBalance;
86 private int maxBalance;
91 * @param model the projector model in use
92 * @param protocolHandler the protocol handler
93 * @param sourcesLabels the custom labels for sources
94 * @param readerThreadName the name of thread to be created
96 public RotelSimuConnector(RotelModel model, RotelAbstractProtocolHandler protocolHandler,
97 Map<RotelSource, String> sourcesLabels, String readerThreadName) {
98 super(protocolHandler, true, readerThreadName);
100 this.protocol = protocolHandler.getProtocol();
101 this.sourcesLabels = sourcesLabels;
103 this.maxVolume = model.hasVolumeControl() ? model.getVolumeMax() : 0;
104 this.maxToneLevel = model.hasToneControl() ? model.getToneLevelMax() : 0;
105 this.minToneLevel = -this.maxToneLevel;
106 this.maxBalance = model.hasBalanceControl() ? model.getBalanceLevelMax() : 0;
107 this.minBalance = -this.maxBalance;
108 List<RotelSource> modelSources = model.getSources();
109 RotelSource source = modelSources.isEmpty() ? RotelSource.CAT0_CD : modelSources.get(0);
110 sources = new RotelSource[] { source, source, source, source, source };
111 recordSource = source;
115 public synchronized void open() throws RotelException {
116 logger.debug("Opening simulated connection");
117 readerThread.start();
119 logger.debug("Simulated connection opened");
123 public synchronized void close() {
124 logger.debug("Closing simulated connection");
127 logger.debug("Simulated connection closed");
131 protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
132 synchronized (lock) {
133 int len = feedbackMsg.length - idxInFeedbackMsg;
135 if (len > dataBuffer.length) {
136 len = dataBuffer.length;
138 System.arraycopy(feedbackMsg, idxInFeedbackMsg, dataBuffer, 0, len);
139 idxInFeedbackMsg += len;
143 // Give more chance to someone else than the reader thread to get the lock
146 } catch (InterruptedException e) {
147 Thread.currentThread().interrupt();
153 * Built the simulated feedback message for a sent command
155 * @param cmd the sent command
156 * @param value the integer value considered in the sent command for volume, bass or treble adjustment
158 public void buildFeedbackMessage(RotelCommand cmd, @Nullable Integer value) {
159 String text = buildSourceLine1Response();
160 String textLine1Left = buildSourceLine1LeftResponse();
161 String textLine1Right = buildVolumeLine1RightResponse();
162 String textLine2 = "";
163 String textAscii = "";
164 boolean accepted = true;
165 boolean resetZone = true;
168 case ZONE1_VOLUME_UP:
169 case ZONE1_VOLUME_DOWN:
170 case ZONE1_VOLUME_SET:
171 case ZONE1_MUTE_TOGGLE:
175 case ZONE1_BASS_DOWN:
177 case ZONE1_TREBLE_UP:
178 case ZONE1_TREBLE_DOWN:
179 case ZONE1_TREBLE_SET:
180 case ZONE1_BALANCE_LEFT:
181 case ZONE1_BALANCE_RIGHT:
182 case ZONE1_BALANCE_SET:
185 case ZONE2_POWER_OFF:
187 case ZONE2_VOLUME_UP:
188 case ZONE2_VOLUME_DOWN:
189 case ZONE2_VOLUME_SET:
190 case ZONE2_MUTE_TOGGLE:
194 case ZONE2_BASS_DOWN:
196 case ZONE2_TREBLE_UP:
197 case ZONE2_TREBLE_DOWN:
198 case ZONE2_TREBLE_SET:
199 case ZONE2_BALANCE_LEFT:
200 case ZONE2_BALANCE_RIGHT:
201 case ZONE2_BALANCE_SET:
204 case ZONE3_POWER_OFF:
206 case ZONE3_VOLUME_UP:
207 case ZONE3_VOLUME_DOWN:
208 case ZONE3_VOLUME_SET:
209 case ZONE3_MUTE_TOGGLE:
213 case ZONE3_BASS_DOWN:
215 case ZONE3_TREBLE_UP:
216 case ZONE3_TREBLE_DOWN:
217 case ZONE3_TREBLE_SET:
218 case ZONE3_BALANCE_LEFT:
219 case ZONE3_BALANCE_RIGHT:
220 case ZONE3_BALANCE_SET:
223 case ZONE4_POWER_OFF:
225 case ZONE4_VOLUME_UP:
226 case ZONE4_VOLUME_DOWN:
227 case ZONE4_VOLUME_SET:
228 case ZONE4_MUTE_TOGGLE:
232 case ZONE4_BASS_DOWN:
234 case ZONE4_TREBLE_UP:
235 case ZONE4_TREBLE_DOWN:
236 case ZONE4_TREBLE_SET:
237 case ZONE4_BALANCE_LEFT:
238 case ZONE4_BALANCE_RIGHT:
239 case ZONE4_BALANCE_SET:
246 case DISPLAY_REFRESH:
249 case MAIN_ZONE_POWER_OFF:
251 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
252 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
253 powers[zone] = false;
256 text = buildSourceLine1Response();
257 textLine1Left = buildSourceLine1LeftResponse();
258 textLine1Right = buildVolumeLine1RightResponse();
259 textAscii = buildPowerAsciiResponse();
262 case MAIN_ZONE_POWER_ON:
264 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
265 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
269 text = buildSourceLine1Response();
270 textLine1Left = buildSourceLine1LeftResponse();
271 textLine1Right = buildVolumeLine1RightResponse();
272 textAscii = buildPowerAsciiResponse();
275 textAscii = buildPowerAsciiResponse();
277 case ZONE2_POWER_OFF:
278 case ZONE3_POWER_OFF:
279 case ZONE4_POWER_OFF:
280 powers[numZone] = false;
281 text = textLine2 = buildZonePowerResponse(numZone);
288 powers[numZone] = true;
289 text = textLine2 = buildZonePowerResponse(numZone);
293 case RECORD_FONCTION_SELECT:
294 if (model.getNumberOfZones() > 1 && model.getZoneSelectCmd() == cmd) {
296 if (showZone >= model.getNumberOfZones()) {
306 selectingRecord = powers[0];
308 textLine2 = buildRecordResponse();
309 } else if (showZone >= 2 && showZone <= 4) {
310 selectingRecord = false;
311 text = textLine2 = buildZonePowerResponse(showZone);
316 if (model.getNumberOfZones() == 1 || (model.getNumberOfZones() > 2 && model.getZoneSelectCmd() == cmd)
317 || (showZone == 1 && model.getZoneSelectCmd() != cmd)) {
320 if (model.getZoneSelectCmd() == cmd) {
321 if (!powers[0] && !powers[2]) {
324 } else if (showZone == 2) {
325 powers[2] = !powers[2];
329 } else if (showZone >= 2 && showZone <= 4) {
330 powers[showZone] = !powers[showZone];
332 if (showZone >= 2 && showZone <= 4) {
333 text = textLine2 = buildZonePowerResponse(showZone);
342 if (!accepted && numZone > 0 && powers[numZone]) {
345 case ZONE1_VOLUME_UP:
346 case ZONE2_VOLUME_UP:
347 case ZONE3_VOLUME_UP:
348 case ZONE4_VOLUME_UP:
349 if (volumes[numZone] < maxVolume) {
352 text = textLine2 = buildZoneVolumeResponse(numZone);
353 textAscii = buildVolumeAsciiResponse();
355 case ZONE1_VOLUME_DOWN:
356 case ZONE2_VOLUME_DOWN:
357 case ZONE3_VOLUME_DOWN:
358 case ZONE4_VOLUME_DOWN:
359 if (volumes[numZone] > minVolume) {
362 text = textLine2 = buildZoneVolumeResponse(numZone);
363 textAscii = buildVolumeAsciiResponse();
365 case ZONE1_VOLUME_SET:
366 case ZONE2_VOLUME_SET:
367 case ZONE3_VOLUME_SET:
368 case ZONE4_VOLUME_SET:
370 volumes[numZone] = value;
372 text = textLine2 = buildZoneVolumeResponse(numZone);
373 textAscii = buildVolumeAsciiResponse();
375 case ZONE1_MUTE_TOGGLE:
376 case ZONE2_MUTE_TOGGLE:
377 case ZONE3_MUTE_TOGGLE:
378 case ZONE4_MUTE_TOGGLE:
379 mutes[numZone] = !mutes[numZone];
380 text = textLine2 = buildZoneVolumeResponse(numZone);
381 textAscii = buildMuteAsciiResponse();
387 mutes[numZone] = true;
388 text = textLine2 = buildZoneVolumeResponse(numZone);
389 textAscii = buildMuteAsciiResponse();
395 mutes[numZone] = false;
396 text = textLine2 = buildZoneVolumeResponse(numZone);
397 textAscii = buildMuteAsciiResponse();
403 if (!tcbypass && basses[numZone] < maxToneLevel) {
404 basses[numZone] += STEP_TONE_LEVEL;
406 textAscii = buildBassAsciiResponse();
408 case ZONE1_BASS_DOWN:
409 case ZONE2_BASS_DOWN:
410 case ZONE3_BASS_DOWN:
411 case ZONE4_BASS_DOWN:
412 if (!tcbypass && basses[numZone] > minToneLevel) {
413 basses[numZone] -= STEP_TONE_LEVEL;
415 textAscii = buildBassAsciiResponse();
421 if (!tcbypass && value != null) {
422 basses[numZone] = value;
424 textAscii = buildBassAsciiResponse();
426 case ZONE1_TREBLE_UP:
427 case ZONE2_TREBLE_UP:
428 case ZONE3_TREBLE_UP:
429 case ZONE4_TREBLE_UP:
430 if (!tcbypass && trebles[numZone] < maxToneLevel) {
431 trebles[numZone] += STEP_TONE_LEVEL;
433 textAscii = buildTrebleAsciiResponse();
435 case ZONE1_TREBLE_DOWN:
436 case ZONE2_TREBLE_DOWN:
437 case ZONE3_TREBLE_DOWN:
438 case ZONE4_TREBLE_DOWN:
439 if (!tcbypass && trebles[numZone] > minToneLevel) {
440 trebles[numZone] -= STEP_TONE_LEVEL;
442 textAscii = buildTrebleAsciiResponse();
444 case ZONE1_TREBLE_SET:
445 case ZONE2_TREBLE_SET:
446 case ZONE3_TREBLE_SET:
447 case ZONE4_TREBLE_SET:
448 if (!tcbypass && value != null) {
449 trebles[numZone] = value;
451 textAscii = buildTrebleAsciiResponse();
453 case ZONE1_BALANCE_LEFT:
454 case ZONE2_BALANCE_LEFT:
455 case ZONE3_BALANCE_LEFT:
456 case ZONE4_BALANCE_LEFT:
457 if (balances[numZone] > minBalance) {
460 textAscii = buildBalanceAsciiResponse();
462 case ZONE1_BALANCE_RIGHT:
463 case ZONE2_BALANCE_RIGHT:
464 case ZONE3_BALANCE_RIGHT:
465 case ZONE4_BALANCE_RIGHT:
466 if (balances[numZone] < maxBalance) {
469 textAscii = buildBalanceAsciiResponse();
471 case ZONE1_BALANCE_SET:
472 case ZONE2_BALANCE_SET:
473 case ZONE3_BALANCE_SET:
474 case ZONE4_BALANCE_SET:
476 balances[numZone] = value;
478 textAscii = buildBalanceAsciiResponse();
486 // Check if command is a change of source input for a zone
487 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
490 sources[zone] = model.getZoneSourceFromCommand(cmd, zone);
491 text = textLine2 = buildZonePowerResponse(zone);
492 textAscii = buildSourceAsciiResponse();
498 } catch (RotelException e) {
503 if (!accepted && powers[2] && !model.hasZoneCommands(2) && model.getNumberOfZones() > 1 && showZone == 2) {
507 if (volumes[2] < maxVolume) {
510 text = textLine2 = buildZoneVolumeResponse(2);
514 if (volumes[2] > minVolume) {
517 text = textLine2 = buildZoneVolumeResponse(2);
524 text = textLine2 = buildZoneVolumeResponse(2);
533 sources[2] = model.getSourceFromCommand(cmd);
534 text = textLine2 = buildZonePowerResponse(2);
538 } catch (RotelException e) {
542 if (!accepted && powers[0]) {
546 textAscii = buildAsciiResponse(
547 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, AUTO);
550 textAscii = buildAsciiResponse(
551 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, MANUAL);
554 textAscii = buildAsciiResponse(KEY_VOLUME_MIN, minVolume);
557 textAscii = buildAsciiResponse(KEY_VOLUME_MAX, maxVolume);
560 case MAIN_ZONE_VOLUME_UP:
561 if (volumes[0] < maxVolume) {
564 text = buildVolumeLine1Response();
565 textLine1Right = buildVolumeLine1RightResponse();
566 textAscii = buildVolumeAsciiResponse();
569 case MAIN_ZONE_VOLUME_DOWN:
570 if (volumes[0] > minVolume) {
573 text = buildVolumeLine1Response();
574 textLine1Right = buildVolumeLine1RightResponse();
575 textAscii = buildVolumeAsciiResponse();
581 text = buildVolumeLine1Response();
582 textLine1Right = buildVolumeLine1RightResponse();
583 textAscii = buildVolumeAsciiResponse();
586 textAscii = buildVolumeAsciiResponse();
589 case MAIN_ZONE_MUTE_TOGGLE:
590 mutes[0] = !mutes[0];
591 text = buildSourceLine1Response();
592 textLine1Right = buildVolumeLine1RightResponse();
593 textAscii = buildMuteAsciiResponse();
596 case MAIN_ZONE_MUTE_ON:
598 text = buildSourceLine1Response();
599 textLine1Right = buildVolumeLine1RightResponse();
600 textAscii = buildMuteAsciiResponse();
603 case MAIN_ZONE_MUTE_OFF:
605 text = buildSourceLine1Response();
606 textLine1Right = buildVolumeLine1RightResponse();
607 textAscii = buildMuteAsciiResponse();
610 textAscii = buildMuteAsciiResponse();
613 textAscii = buildAsciiResponse(KEY_TONE_MAX, String.format("%02d", maxToneLevel));
615 case TONE_CONTROLS_ON:
617 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
619 case TONE_CONTROLS_OFF:
621 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
624 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
628 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
632 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
635 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
638 if (!tcbypass && basses[0] < maxToneLevel) {
639 basses[0] += STEP_TONE_LEVEL;
641 text = buildBassLine1Response();
642 textLine1Right = buildBassLine1RightResponse();
643 textAscii = buildBassAsciiResponse();
646 if (!tcbypass && basses[0] > minToneLevel) {
647 basses[0] -= STEP_TONE_LEVEL;
649 text = buildBassLine1Response();
650 textLine1Right = buildBassLine1RightResponse();
651 textAscii = buildBassAsciiResponse();
654 if (!tcbypass && value != null) {
657 text = buildBassLine1Response();
658 textLine1Right = buildBassLine1RightResponse();
659 textAscii = buildBassAsciiResponse();
662 textAscii = buildBassAsciiResponse();
665 if (!tcbypass && trebles[0] < maxToneLevel) {
666 trebles[0] += STEP_TONE_LEVEL;
668 text = buildTrebleLine1Response();
669 textLine1Right = buildTrebleLine1RightResponse();
670 textAscii = buildTrebleAsciiResponse();
673 if (!tcbypass && trebles[0] > minToneLevel) {
674 trebles[0] -= STEP_TONE_LEVEL;
676 text = buildTrebleLine1Response();
677 textLine1Right = buildTrebleLine1RightResponse();
678 textAscii = buildTrebleAsciiResponse();
681 if (!tcbypass && value != null) {
684 text = buildTrebleLine1Response();
685 textLine1Right = buildTrebleLine1RightResponse();
686 textAscii = buildTrebleAsciiResponse();
689 textAscii = buildTrebleAsciiResponse();
691 case TONE_CONTROL_SELECT:
692 showTreble = !showTreble;
694 text = buildTrebleLine1Response();
695 textLine1Right = buildTrebleLine1RightResponse();
697 text = buildBassLine1Response();
698 textLine1Right = buildBassLine1RightResponse();
702 if (balances[0] > minBalance) {
705 textAscii = buildBalanceAsciiResponse();
708 if (balances[0] < maxBalance) {
711 textAscii = buildBalanceAsciiResponse();
717 textAscii = buildBalanceAsciiResponse();
720 textAscii = buildBalanceAsciiResponse();
722 case SPEAKER_A_TOGGLE:
723 speakerA = !speakerA;
724 textAscii = buildSpeakerAsciiResponse();
728 textAscii = buildSpeakerAsciiResponse();
732 textAscii = buildSpeakerAsciiResponse();
734 case SPEAKER_B_TOGGLE:
735 speakerB = !speakerB;
736 textAscii = buildSpeakerAsciiResponse();
740 textAscii = buildSpeakerAsciiResponse();
744 textAscii = buildSpeakerAsciiResponse();
747 textAscii = buildSpeakerAsciiResponse();
750 playStatus = RotelPlayStatus.PLAYING;
751 textAscii = buildPlayStatusAsciiResponse();
754 playStatus = RotelPlayStatus.STOPPED;
755 textAscii = buildPlayStatusAsciiResponse();
758 switch (playStatus) {
760 playStatus = RotelPlayStatus.PAUSED;
764 playStatus = RotelPlayStatus.PLAYING;
767 textAscii = buildPlayStatusAsciiResponse();
771 textAscii = buildPlayStatusAsciiResponse();
775 textAscii = buildTrackAsciiResponse();
781 textAscii = buildTrackAsciiResponse();
784 textAscii = buildTrackAsciiResponse();
787 randomMode = !randomMode;
788 textAscii = buildRandomModeAsciiResponse();
791 textAscii = buildRandomModeAsciiResponse();
794 switch (repeatMode) {
796 repeatMode = RotelRepeatMode.DISC;
799 repeatMode = RotelRepeatMode.OFF;
802 repeatMode = RotelRepeatMode.TRACK;
805 textAscii = buildRepeatModeAsciiResponse();
808 textAscii = buildRepeatModeAsciiResponse();
810 case SOURCE_MULTI_INPUT:
811 multiinput = !multiinput;
812 text = "MULTI IN " + (multiinput ? "ON" : "OFF");
814 sources[0] = model.getSourceFromCommand(cmd);
815 textLine1Left = buildSourceLine1LeftResponse();
816 textAscii = buildSourceAsciiResponse();
818 } catch (RotelException e) {
823 textAscii = buildSourceAsciiResponse();
826 dsp = RotelDsp.CAT4_NONE;
827 textLine2 = "STEREO";
828 textAscii = buildDspAsciiResponse();
831 dsp = RotelDsp.CAT4_STEREO3;
832 textLine2 = "DOLBY 3 STEREO";
833 textAscii = buildDspAsciiResponse();
836 dsp = RotelDsp.CAT4_STEREO5;
837 textLine2 = "5CH STEREO";
838 textAscii = buildDspAsciiResponse();
841 dsp = RotelDsp.CAT4_STEREO7;
842 textLine2 = "7CH STEREO";
843 textAscii = buildDspAsciiResponse();
846 dsp = RotelDsp.CAT5_STEREO9;
847 textAscii = buildDspAsciiResponse();
850 dsp = RotelDsp.CAT5_STEREO11;
851 textAscii = buildDspAsciiResponse();
854 dsp = RotelDsp.CAT4_DSP1;
856 textAscii = buildDspAsciiResponse();
859 dsp = RotelDsp.CAT4_DSP2;
861 textAscii = buildDspAsciiResponse();
864 dsp = RotelDsp.CAT4_DSP3;
866 textAscii = buildDspAsciiResponse();
869 dsp = RotelDsp.CAT4_DSP4;
871 textAscii = buildDspAsciiResponse();
874 dsp = RotelDsp.CAT4_PROLOGIC;
875 textLine2 = "DOLBY PRO LOGIC";
876 textAscii = buildDspAsciiResponse();
879 dsp = RotelDsp.CAT4_PLII_CINEMA;
880 textLine2 = "DOLBY PL C";
881 textAscii = buildDspAsciiResponse();
884 dsp = RotelDsp.CAT4_PLII_MUSIC;
885 textLine2 = "DOLBY PL M";
886 textAscii = buildDspAsciiResponse();
889 dsp = RotelDsp.CAT4_PLII_GAME;
890 textLine2 = "DOLBY PL G";
891 textAscii = buildDspAsciiResponse();
894 dsp = RotelDsp.CAT4_PLIIZ;
895 textLine2 = "DOLBY PL z";
896 textAscii = buildDspAsciiResponse();
899 dsp = RotelDsp.CAT4_NEO6_MUSIC;
900 textLine2 = "DTS Neo:6 M";
901 textAscii = buildDspAsciiResponse();
904 dsp = RotelDsp.CAT4_NEO6_CINEMA;
905 textLine2 = "DTS Neo:6 C";
906 textAscii = buildDspAsciiResponse();
909 dsp = RotelDsp.CAT5_ATMOS;
910 textAscii = buildDspAsciiResponse();
913 dsp = RotelDsp.CAT5_NEURAL_X;
914 textAscii = buildDspAsciiResponse();
917 dsp = RotelDsp.CAT4_BYPASS;
918 textLine2 = "BYPASS";
919 textAscii = buildDspAsciiResponse();
922 textAscii = buildDspAsciiResponse();
925 textAscii = model.getNumberOfZones() > 1 ? buildAsciiResponse(KEY_FREQ, "44.1,48,none,176.4")
926 : buildAsciiResponse(KEY_FREQ, "44.1");
928 case DIMMER_LEVEL_SET:
932 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
934 case DIMMER_LEVEL_GET:
935 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
938 textAscii = buildAsciiResponse(KEY_MODEL, model.getName());
941 textAscii = buildAsciiResponse(KEY_VERSION, "1.00");
948 // Check if command is a change of source input for the main zone
950 sources[0] = model.getZoneSourceFromCommand(cmd, 1);
951 text = buildSourceLine1Response();
952 textLine1Left = buildSourceLine1LeftResponse();
953 textAscii = buildSourceAsciiResponse();
955 } catch (RotelException e) {
959 // Check if command is a change of source input
961 if (selectingRecord && !model.hasOtherThanPrimaryCommands()) {
962 recordSource = model.getSourceFromCommand(cmd);
964 sources[0] = model.getSourceFromCommand(cmd);
966 text = buildSourceLine1Response();
967 textLine1Left = buildSourceLine1LeftResponse();
968 textAscii = buildSourceAsciiResponse();
971 } catch (RotelException e) {
975 // Check if command is a change of record source
977 recordSource = model.getRecordSourceFromCommand(cmd);
978 text = buildSourceLine1Response();
979 textLine2 = buildRecordResponse();
981 } catch (RotelException e) {
990 if (cmd != RotelCommand.RECORD_FONCTION_SELECT) {
991 selectingRecord = false;
997 if (model.getRespNbChars() == 42) {
998 while (textLine1Left.length() < 14) {
999 textLine1Left += " ";
1001 while (textLine1Right.length() < 7) {
1002 textLine1Right += " ";
1004 while (textLine2.length() < 21) {
1007 text = textLine1Left + textLine1Right + textLine2;
1010 if (protocol == RotelProtocol.HEX) {
1011 byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), model.getRespNbChars());
1012 byte[] flags = new byte[model.getRespNbFlags()];
1014 model.setMultiInput(flags, multiinput);
1015 } catch (RotelException e) {
1018 model.setZone2(flags, powers[2]);
1019 } catch (RotelException e) {
1022 model.setZone3(flags, powers[3]);
1023 } catch (RotelException e) {
1026 model.setZone4(flags, powers[4]);
1027 } catch (RotelException e) {
1029 int size = 6 + model.getRespNbChars() + model.getRespNbFlags();
1030 byte[] dataBuffer = new byte[size];
1032 dataBuffer[idx++] = START;
1033 dataBuffer[idx++] = (byte) (size - 4);
1034 dataBuffer[idx++] = model.getDeviceId();
1035 dataBuffer[idx++] = STANDARD_RESPONSE;
1036 if (model.isCharsBeforeFlags()) {
1037 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1038 idx += model.getRespNbChars();
1039 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1040 idx += model.getRespNbFlags();
1042 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1043 idx += model.getRespNbFlags();
1044 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1045 idx += model.getRespNbChars();
1047 byte checksum = RotelHexProtocolHandler.computeCheckSum(dataBuffer, idx - 1);
1048 if ((checksum & 0x000000FF) == 0x000000FD) {
1049 dataBuffer[idx++] = (byte) 0xFD;
1050 dataBuffer[idx++] = 0;
1051 } else if ((checksum & 0x000000FF) == 0x000000FE) {
1052 dataBuffer[idx++] = (byte) 0xFD;
1053 dataBuffer[idx++] = 1;
1055 dataBuffer[idx++] = checksum;
1057 synchronized (lock) {
1058 feedbackMsg = Arrays.copyOf(dataBuffer, idx);
1059 idxInFeedbackMsg = 0;
1062 String command = textAscii + (protocol == RotelProtocol.ASCII_V1 ? "!" : "$");
1063 synchronized (lock) {
1064 feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
1065 idxInFeedbackMsg = 0;
1070 private String buildAsciiResponse(String key, String value) {
1071 return String.format("%s=%s", key, value);
1074 private String buildAsciiResponse(String key, int value) {
1075 return String.format("%s=%d", key, value);
1078 private String buildAsciiResponse(String key, boolean value) {
1079 return buildAsciiResponse(key, buildOnOffValue(value));
1082 private String buildOnOffValue(boolean on) {
1083 return on ? MSG_VALUE_ON : MSG_VALUE_OFF;
1086 private String buildPowerAsciiResponse() {
1087 return buildAsciiResponse(KEY_POWER, powers[0] ? POWER_ON : STANDBY);
1090 private String buildVolumeAsciiResponse() {
1091 if (model.getNumberOfZones() > 1) {
1092 StringJoiner sj = new StringJoiner(",");
1093 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1094 sj.add(String.format("%02d", volumes[zone]));
1096 return buildAsciiResponse(KEY_VOLUME, sj.toString());
1098 return buildAsciiResponse(KEY_VOLUME, String.format("%02d", volumes[0]));
1102 private String buildMuteAsciiResponse() {
1103 if (model.getNumberOfZones() > 1) {
1104 StringJoiner sj = new StringJoiner(",");
1105 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1106 sj.add(buildOnOffValue(mutes[zone]));
1108 return buildAsciiResponse(KEY_MUTE, sj.toString());
1110 return buildAsciiResponse(KEY_MUTE, mutes[0]);
1114 private String buildBassAsciiResponse() {
1115 if (model.getNumberOfZones() > 1) {
1116 StringJoiner sj = new StringJoiner(",");
1117 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1118 sj.add(buildBassTrebleValue(basses[zone]));
1120 return buildAsciiResponse(KEY_BASS, sj.toString());
1122 return buildAsciiResponse(KEY_BASS, buildBassTrebleValue(basses[0]));
1126 private String buildTrebleAsciiResponse() {
1127 if (model.getNumberOfZones() > 1) {
1128 StringJoiner sj = new StringJoiner(",");
1129 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1130 sj.add(buildBassTrebleValue(trebles[zone]));
1132 return buildAsciiResponse(KEY_TREBLE, sj.toString());
1134 return buildAsciiResponse(KEY_TREBLE, buildBassTrebleValue(trebles[0]));
1138 private String buildBassTrebleValue(int value) {
1139 if (tcbypass || value == 0) {
1141 } else if (value > 0) {
1142 return String.format("+%02d", value);
1144 return String.format("-%02d", -value);
1148 private String buildBalanceAsciiResponse() {
1149 if (model.getNumberOfZones() > 1) {
1150 StringJoiner sj = new StringJoiner(",");
1151 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1152 sj.add(buildBalanceValue(balances[zone]));
1154 return buildAsciiResponse(KEY_BALANCE, sj.toString());
1156 return buildAsciiResponse(KEY_BALANCE, buildBalanceValue(balances[0]));
1160 private String buildBalanceValue(int value) {
1163 } else if (value > 0) {
1164 return String.format("r%02d", value);
1166 return String.format("l%02d", -value);
1170 private String buildSpeakerAsciiResponse() {
1172 if (speakerA && speakerB) {
1173 value = MSG_VALUE_SPEAKER_AB;
1174 } else if (speakerA && !speakerB) {
1175 value = MSG_VALUE_SPEAKER_A;
1176 } else if (!speakerA && speakerB) {
1177 value = MSG_VALUE_SPEAKER_B;
1179 value = MSG_VALUE_OFF;
1181 return buildAsciiResponse(KEY_SPEAKER, value);
1184 private String buildPlayStatusAsciiResponse() {
1186 switch (playStatus) {
1197 return buildAsciiResponse(protocol == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_STATUS, status);
1200 private String buildTrackAsciiResponse() {
1201 return buildAsciiResponse(KEY_TRACK, String.format("%03d", track));
1204 private String buildRandomModeAsciiResponse() {
1205 return buildAsciiResponse(KEY_RANDOM, randomMode);
1208 private String buildRepeatModeAsciiResponse() {
1210 switch (repeatMode) {
1218 mode = MSG_VALUE_OFF;
1221 return buildAsciiResponse(KEY_REPEAT, mode);
1224 private String buildSourceAsciiResponse() {
1225 if (model.getNumberOfZones() > 1) {
1226 StringJoiner sj = new StringJoiner(",");
1227 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1228 sj.add(buildZoneSourceValue(sources[zone]));
1230 return buildAsciiResponse(KEY_INPUT, sj.toString());
1232 return buildAsciiResponse(KEY_SOURCE, buildSourceValue(sources[0]));
1236 private String buildSourceValue(RotelSource source) {
1238 RotelCommand command = source.getCommand();
1239 if (command != null) {
1240 str = protocol == RotelProtocol.ASCII_V1 ? command.getAsciiCommandV1() : command.getAsciiCommandV2();
1242 return str == null ? "" : str;
1245 private String buildZoneSourceValue(RotelSource source) {
1246 String str = buildSourceValue(source);
1247 int idx = str.indexOf("input_");
1248 return idx < 0 ? str : str.substring(idx + 6);
1251 private String buildDspAsciiResponse() {
1252 return buildAsciiResponse(KEY_DSP_MODE, dsp.getFeedback());
1255 private String buildSourceLine1Response() {
1259 } else if (mutes[0]) {
1262 text = getSourceLabel(sources[0], false) + " " + getSourceLabel(recordSource, true);
1267 private String buildSourceLine1LeftResponse() {
1272 text = getSourceLabel(sources[0], false);
1277 private String buildRecordResponse() {
1282 text = "REC " + getSourceLabel(recordSource, true);
1287 private String buildZonePowerResponse(int numZone) {
1290 zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1292 zone = String.format("ZONE%d", numZone);
1294 String state = powers[numZone] ? getSourceLabel(sources[numZone], true) : "OFF";
1295 return zone + " " + state;
1298 private String buildVolumeLine1Response() {
1300 if (volumes[0] == minVolume) {
1301 text = " VOLUME MIN ";
1302 } else if (volumes[0] == maxVolume) {
1303 text = " VOLUME MAX ";
1305 text = String.format(" VOLUME %02d ", volumes[0]);
1310 private String buildVolumeLine1RightResponse() {
1314 } else if (mutes[0]) {
1316 } else if (volumes[0] == minVolume) {
1318 } else if (volumes[0] == maxVolume) {
1321 text = String.format("VOL %02d", volumes[0]);
1326 private String buildZoneVolumeResponse(int numZone) {
1329 zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1331 zone = String.format("ZONE%d", numZone);
1334 if (mutes[numZone]) {
1335 text = zone + " MUTE ON";
1336 } else if (volumes[numZone] == minVolume) {
1337 text = zone + " VOL MIN";
1338 } else if (volumes[numZone] == maxVolume) {
1339 text = zone + " VOL MAX";
1341 text = String.format("%s VOL %02d", zone, volumes[numZone]);
1346 private String buildBassLine1Response() {
1348 if (basses[0] == minToneLevel) {
1349 text = " BASS MIN ";
1350 } else if (basses[0] == maxToneLevel) {
1351 text = " BASS MAX ";
1352 } else if (basses[0] == 0) {
1354 } else if (basses[0] > 0) {
1355 text = String.format(" BASS +%02d ", basses[0]);
1357 text = String.format(" BASS -%02d ", -basses[0]);
1362 private String buildBassLine1RightResponse() {
1364 if (basses[0] == minToneLevel) {
1366 } else if (basses[0] == maxToneLevel) {
1368 } else if (basses[0] == 0) {
1370 } else if (basses[0] > 0) {
1371 text = String.format("LF + %02d", basses[0]);
1373 text = String.format("LF - %02d", -basses[0]);
1378 private String buildTrebleLine1Response() {
1380 if (trebles[0] == minToneLevel) {
1381 text = " TREBLE MIN ";
1382 } else if (trebles[0] == maxToneLevel) {
1383 text = " TREBLE MAX ";
1384 } else if (trebles[0] == 0) {
1385 text = " TREBLE 0 ";
1386 } else if (trebles[0] > 0) {
1387 text = String.format(" TREBLE +%02d ", trebles[0]);
1389 text = String.format(" TREBLE -%02d ", -trebles[0]);
1394 private String buildTrebleLine1RightResponse() {
1396 if (trebles[0] == minToneLevel) {
1398 } else if (trebles[0] == maxToneLevel) {
1400 } else if (trebles[0] == 0) {
1402 } else if (trebles[0] > 0) {
1403 text = String.format("HF + %02d", trebles[0]);
1405 text = String.format("HF - %02d", -trebles[0]);
1410 private String getSourceLabel(RotelSource source, boolean considerFollowMain) {
1412 if (considerFollowMain && source.getName().equals(RotelSource.CAT1_FOLLOW_MAIN.getName())) {
1415 label = Objects.requireNonNullElse(sourcesLabels.get(source), source.getLabel());