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;
47 private static final double STEP_DECIBEL = 0.5;
49 private final Logger logger = LoggerFactory.getLogger(RotelSimuConnector.class);
51 private final RotelModel model;
52 private final RotelProtocol protocol;
53 private final Map<RotelSource, String> sourcesLabels;
55 private Object lock = new Object();
57 private byte[] feedbackMsg = new byte[1];
58 private int idxInFeedbackMsg = feedbackMsg.length;
60 private boolean[] powers = { false, false, false, false, false };
61 private String powerMode = POWER_NORMAL;
62 private RotelSource[] sources;
63 private RotelSource recordSource;
64 private boolean multiinput;
65 private RotelDsp dsp = RotelDsp.CAT4_NONE;
66 private boolean bypass = false;
67 private int[] volumes = { 50, 10, 20, 30, 40 };
68 private boolean[] mutes = { false, false, false, false, false };
69 private boolean tcbypass;
70 private int[] basses = { 0, 0, 0, 0, 0 };
71 private int[] trebles = { 0, 0, 0, 0, 0 };
72 private int[] balances = { 0, 0, 0, 0, 0 };
73 private boolean showTreble;
74 private boolean speakerA = true;
75 private boolean speakerB = false;
76 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
77 private int track = 1;
78 private boolean randomMode;
79 private RotelRepeatMode repeatMode = RotelRepeatMode.OFF;
80 private boolean selectingRecord;
83 private int pcUsbClass = 1;
84 private double subLevel;
85 private double centerLevel;
86 private double surroundRightLevel;
87 private double surroundLefLevel;
88 private double centerBackRightLevel;
89 private double centerBackLefLevel;
90 private double ceilingFrontRightLevel;
91 private double ceilingFrontLefLevel;
92 private double ceilingRearRightLevel;
93 private double ceilingRearLefLevel;
95 private int minVolume;
96 private int maxVolume;
97 private int minToneLevel;
98 private int maxToneLevel;
99 private int minBalance;
100 private int maxBalance;
105 * @param model the projector model in use
106 * @param protocolHandler the protocol handler
107 * @param sourcesLabels the custom labels for sources
108 * @param readerThreadName the name of thread to be created
110 public RotelSimuConnector(RotelModel model, RotelAbstractProtocolHandler protocolHandler,
111 Map<RotelSource, String> sourcesLabels, String readerThreadName) {
112 super(protocolHandler, true, readerThreadName);
114 this.protocol = protocolHandler.getProtocol();
115 this.sourcesLabels = sourcesLabels;
117 this.maxVolume = model.hasVolumeControl() ? model.getVolumeMax() : 0;
118 this.maxToneLevel = model.hasToneControl() ? model.getToneLevelMax() : 0;
119 this.minToneLevel = -this.maxToneLevel;
120 this.maxBalance = model.hasBalanceControl() ? model.getBalanceLevelMax() : 0;
121 this.minBalance = -this.maxBalance;
122 List<RotelSource> modelSources = model.getSources();
123 RotelSource source = modelSources.isEmpty() ? RotelSource.CAT0_CD : modelSources.get(0);
124 sources = new RotelSource[] { source, source, source, source, source };
125 recordSource = source;
129 public synchronized void open() throws RotelException {
130 logger.debug("Opening simulated connection");
131 readerThread.start();
133 logger.debug("Simulated connection opened");
137 public synchronized void close() {
138 logger.debug("Closing simulated connection");
141 logger.debug("Simulated connection closed");
145 protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
146 synchronized (lock) {
147 int len = feedbackMsg.length - idxInFeedbackMsg;
149 if (len > dataBuffer.length) {
150 len = dataBuffer.length;
152 System.arraycopy(feedbackMsg, idxInFeedbackMsg, dataBuffer, 0, len);
153 idxInFeedbackMsg += len;
157 // Give more chance to someone else than the reader thread to get the lock
160 } catch (InterruptedException e) {
161 Thread.currentThread().interrupt();
167 * Built the simulated feedback message for a sent command
169 * @param cmd the sent command
170 * @param value the integer value considered in the sent command for volume, bass or treble adjustment
172 public void buildFeedbackMessage(RotelCommand cmd, @Nullable Integer value) {
173 String text = buildSourceLine1Response();
174 String textLine1Left = buildSourceLine1LeftResponse();
175 String textLine1Right = buildVolumeLine1RightResponse();
176 String textLine2 = "";
177 String textAscii = "";
178 boolean accepted = true;
179 boolean resetZone = true;
182 case ZONE1_VOLUME_UP:
183 case ZONE1_VOLUME_DOWN:
184 case ZONE1_VOLUME_SET:
185 case ZONE1_MUTE_TOGGLE:
189 case ZONE1_BASS_DOWN:
191 case ZONE1_TREBLE_UP:
192 case ZONE1_TREBLE_DOWN:
193 case ZONE1_TREBLE_SET:
194 case ZONE1_BALANCE_LEFT:
195 case ZONE1_BALANCE_RIGHT:
196 case ZONE1_BALANCE_SET:
199 case ZONE2_POWER_OFF:
201 case ZONE2_VOLUME_UP:
202 case ZONE2_VOLUME_DOWN:
203 case ZONE2_VOLUME_SET:
204 case ZONE2_MUTE_TOGGLE:
208 case ZONE2_BASS_DOWN:
210 case ZONE2_TREBLE_UP:
211 case ZONE2_TREBLE_DOWN:
212 case ZONE2_TREBLE_SET:
213 case ZONE2_BALANCE_LEFT:
214 case ZONE2_BALANCE_RIGHT:
215 case ZONE2_BALANCE_SET:
218 case ZONE3_POWER_OFF:
220 case ZONE3_VOLUME_UP:
221 case ZONE3_VOLUME_DOWN:
222 case ZONE3_VOLUME_SET:
223 case ZONE3_MUTE_TOGGLE:
227 case ZONE3_BASS_DOWN:
229 case ZONE3_TREBLE_UP:
230 case ZONE3_TREBLE_DOWN:
231 case ZONE3_TREBLE_SET:
232 case ZONE3_BALANCE_LEFT:
233 case ZONE3_BALANCE_RIGHT:
234 case ZONE3_BALANCE_SET:
237 case ZONE4_POWER_OFF:
239 case ZONE4_VOLUME_UP:
240 case ZONE4_VOLUME_DOWN:
241 case ZONE4_VOLUME_SET:
242 case ZONE4_MUTE_TOGGLE:
246 case ZONE4_BASS_DOWN:
248 case ZONE4_TREBLE_UP:
249 case ZONE4_TREBLE_DOWN:
250 case ZONE4_TREBLE_SET:
251 case ZONE4_BALANCE_LEFT:
252 case ZONE4_BALANCE_RIGHT:
253 case ZONE4_BALANCE_SET:
260 case DISPLAY_REFRESH:
263 case MAIN_ZONE_POWER_OFF:
265 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
266 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
267 powers[zone] = false;
270 text = buildSourceLine1Response();
271 textLine1Left = buildSourceLine1LeftResponse();
272 textLine1Right = buildVolumeLine1RightResponse();
273 textAscii = buildPowerAsciiResponse();
276 case MAIN_ZONE_POWER_ON:
278 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
279 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
283 text = buildSourceLine1Response();
284 textLine1Left = buildSourceLine1LeftResponse();
285 textLine1Right = buildVolumeLine1RightResponse();
286 textAscii = buildPowerAsciiResponse();
289 textAscii = buildPowerAsciiResponse();
291 case ZONE2_POWER_OFF:
292 case ZONE3_POWER_OFF:
293 case ZONE4_POWER_OFF:
294 powers[numZone] = false;
295 text = textLine2 = buildZonePowerResponse(numZone);
302 powers[numZone] = true;
303 text = textLine2 = buildZonePowerResponse(numZone);
307 case RECORD_FONCTION_SELECT:
308 if (model.getNumberOfZones() > 1 && model.getZoneSelectCmd() == cmd) {
310 if (showZone >= model.getNumberOfZones()) {
320 selectingRecord = powers[0];
322 textLine2 = buildRecordResponse();
323 } else if (showZone >= 2 && showZone <= 4) {
324 selectingRecord = false;
325 text = textLine2 = buildZonePowerResponse(showZone);
330 if (model.getNumberOfZones() == 1 || (model.getNumberOfZones() > 2 && model.getZoneSelectCmd() == cmd)
331 || (showZone == 1 && model.getZoneSelectCmd() != cmd)) {
334 if (model.getZoneSelectCmd() == cmd) {
335 if (!powers[0] && !powers[2]) {
338 } else if (showZone == 2) {
339 powers[2] = !powers[2];
343 } else if (showZone >= 2 && showZone <= 4) {
344 powers[showZone] = !powers[showZone];
346 if (showZone >= 2 && showZone <= 4) {
347 text = textLine2 = buildZonePowerResponse(showZone);
356 if (!accepted && numZone > 0 && powers[numZone]) {
359 case ZONE1_VOLUME_UP:
360 case ZONE2_VOLUME_UP:
361 case ZONE3_VOLUME_UP:
362 case ZONE4_VOLUME_UP:
363 if (volumes[numZone] < maxVolume) {
366 text = textLine2 = buildZoneVolumeResponse(numZone);
367 textAscii = buildVolumeAsciiResponse();
369 case ZONE1_VOLUME_DOWN:
370 case ZONE2_VOLUME_DOWN:
371 case ZONE3_VOLUME_DOWN:
372 case ZONE4_VOLUME_DOWN:
373 if (volumes[numZone] > minVolume) {
376 text = textLine2 = buildZoneVolumeResponse(numZone);
377 textAscii = buildVolumeAsciiResponse();
379 case ZONE1_VOLUME_SET:
380 case ZONE2_VOLUME_SET:
381 case ZONE3_VOLUME_SET:
382 case ZONE4_VOLUME_SET:
384 volumes[numZone] = value;
386 text = textLine2 = buildZoneVolumeResponse(numZone);
387 textAscii = buildVolumeAsciiResponse();
389 case ZONE1_MUTE_TOGGLE:
390 case ZONE2_MUTE_TOGGLE:
391 case ZONE3_MUTE_TOGGLE:
392 case ZONE4_MUTE_TOGGLE:
393 mutes[numZone] = !mutes[numZone];
394 text = textLine2 = buildZoneVolumeResponse(numZone);
395 textAscii = buildMuteAsciiResponse();
401 mutes[numZone] = true;
402 text = textLine2 = buildZoneVolumeResponse(numZone);
403 textAscii = buildMuteAsciiResponse();
409 mutes[numZone] = false;
410 text = textLine2 = buildZoneVolumeResponse(numZone);
411 textAscii = buildMuteAsciiResponse();
417 if (!tcbypass && basses[numZone] < maxToneLevel) {
418 basses[numZone] += STEP_TONE_LEVEL;
420 textAscii = buildBassAsciiResponse();
422 case ZONE1_BASS_DOWN:
423 case ZONE2_BASS_DOWN:
424 case ZONE3_BASS_DOWN:
425 case ZONE4_BASS_DOWN:
426 if (!tcbypass && basses[numZone] > minToneLevel) {
427 basses[numZone] -= STEP_TONE_LEVEL;
429 textAscii = buildBassAsciiResponse();
435 if (!tcbypass && value != null) {
436 basses[numZone] = value;
438 textAscii = buildBassAsciiResponse();
440 case ZONE1_TREBLE_UP:
441 case ZONE2_TREBLE_UP:
442 case ZONE3_TREBLE_UP:
443 case ZONE4_TREBLE_UP:
444 if (!tcbypass && trebles[numZone] < maxToneLevel) {
445 trebles[numZone] += STEP_TONE_LEVEL;
447 textAscii = buildTrebleAsciiResponse();
449 case ZONE1_TREBLE_DOWN:
450 case ZONE2_TREBLE_DOWN:
451 case ZONE3_TREBLE_DOWN:
452 case ZONE4_TREBLE_DOWN:
453 if (!tcbypass && trebles[numZone] > minToneLevel) {
454 trebles[numZone] -= STEP_TONE_LEVEL;
456 textAscii = buildTrebleAsciiResponse();
458 case ZONE1_TREBLE_SET:
459 case ZONE2_TREBLE_SET:
460 case ZONE3_TREBLE_SET:
461 case ZONE4_TREBLE_SET:
462 if (!tcbypass && value != null) {
463 trebles[numZone] = value;
465 textAscii = buildTrebleAsciiResponse();
467 case ZONE1_BALANCE_LEFT:
468 case ZONE2_BALANCE_LEFT:
469 case ZONE3_BALANCE_LEFT:
470 case ZONE4_BALANCE_LEFT:
471 if (balances[numZone] > minBalance) {
474 textAscii = buildBalanceAsciiResponse();
476 case ZONE1_BALANCE_RIGHT:
477 case ZONE2_BALANCE_RIGHT:
478 case ZONE3_BALANCE_RIGHT:
479 case ZONE4_BALANCE_RIGHT:
480 if (balances[numZone] < maxBalance) {
483 textAscii = buildBalanceAsciiResponse();
485 case ZONE1_BALANCE_SET:
486 case ZONE2_BALANCE_SET:
487 case ZONE3_BALANCE_SET:
488 case ZONE4_BALANCE_SET:
490 balances[numZone] = value;
492 textAscii = buildBalanceAsciiResponse();
500 // Check if command is a change of source input for a zone
501 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
504 sources[zone] = model.getZoneSourceFromCommand(cmd, zone);
505 text = textLine2 = buildZonePowerResponse(zone);
506 textAscii = buildSourceAsciiResponse();
512 } catch (RotelException e) {
517 if (!accepted && powers[2] && !model.hasZoneCommands(2) && model.getNumberOfZones() > 1 && showZone == 2) {
521 if (volumes[2] < maxVolume) {
524 text = textLine2 = buildZoneVolumeResponse(2);
528 if (volumes[2] > minVolume) {
531 text = textLine2 = buildZoneVolumeResponse(2);
538 text = textLine2 = buildZoneVolumeResponse(2);
547 sources[2] = model.getSourceFromCommand(cmd);
548 text = textLine2 = buildZonePowerResponse(2);
552 } catch (RotelException e) {
556 if (!accepted && powers[0]) {
560 textAscii = buildAsciiResponse(
561 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, AUTO);
564 textAscii = buildAsciiResponse(
565 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, MANUAL);
567 case POWER_MODE_QUICK:
568 powerMode = POWER_QUICK;
569 textAscii = buildAsciiResponse(KEY_POWER_MODE, powerMode);
571 case POWER_MODE_NORMAL:
572 powerMode = POWER_NORMAL;
573 textAscii = buildAsciiResponse(KEY_POWER_MODE, powerMode);
576 textAscii = buildAsciiResponse(KEY_POWER_MODE, powerMode);
579 textAscii = buildAsciiResponse(KEY_VOLUME_MIN, minVolume);
582 textAscii = buildAsciiResponse(KEY_VOLUME_MAX, maxVolume);
585 case MAIN_ZONE_VOLUME_UP:
586 if (volumes[0] < maxVolume) {
589 text = buildVolumeLine1Response();
590 textLine1Right = buildVolumeLine1RightResponse();
591 textAscii = buildVolumeAsciiResponse();
594 case MAIN_ZONE_VOLUME_DOWN:
595 if (volumes[0] > minVolume) {
598 text = buildVolumeLine1Response();
599 textLine1Right = buildVolumeLine1RightResponse();
600 textAscii = buildVolumeAsciiResponse();
606 text = buildVolumeLine1Response();
607 textLine1Right = buildVolumeLine1RightResponse();
608 textAscii = buildVolumeAsciiResponse();
611 textAscii = buildVolumeAsciiResponse();
614 case MAIN_ZONE_MUTE_TOGGLE:
615 mutes[0] = !mutes[0];
616 text = buildSourceLine1Response();
617 textLine1Right = buildVolumeLine1RightResponse();
618 textAscii = buildMuteAsciiResponse();
621 case MAIN_ZONE_MUTE_ON:
623 text = buildSourceLine1Response();
624 textLine1Right = buildVolumeLine1RightResponse();
625 textAscii = buildMuteAsciiResponse();
628 case MAIN_ZONE_MUTE_OFF:
630 text = buildSourceLine1Response();
631 textLine1Right = buildVolumeLine1RightResponse();
632 textAscii = buildMuteAsciiResponse();
635 textAscii = buildMuteAsciiResponse();
638 textAscii = buildAsciiResponse(KEY_TONE_MAX, String.format("%02d", maxToneLevel));
640 case TONE_CONTROLS_ON:
642 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
644 case TONE_CONTROLS_OFF:
646 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
649 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
653 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
657 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
660 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
663 if (!tcbypass && basses[0] < maxToneLevel) {
664 basses[0] += STEP_TONE_LEVEL;
666 text = buildBassLine1Response();
667 textLine1Right = buildBassLine1RightResponse();
668 textAscii = buildBassAsciiResponse();
671 if (!tcbypass && basses[0] > minToneLevel) {
672 basses[0] -= STEP_TONE_LEVEL;
674 text = buildBassLine1Response();
675 textLine1Right = buildBassLine1RightResponse();
676 textAscii = buildBassAsciiResponse();
679 if (!tcbypass && value != null) {
682 text = buildBassLine1Response();
683 textLine1Right = buildBassLine1RightResponse();
684 textAscii = buildBassAsciiResponse();
687 textAscii = buildBassAsciiResponse();
690 if (!tcbypass && trebles[0] < maxToneLevel) {
691 trebles[0] += STEP_TONE_LEVEL;
693 text = buildTrebleLine1Response();
694 textLine1Right = buildTrebleLine1RightResponse();
695 textAscii = buildTrebleAsciiResponse();
698 if (!tcbypass && trebles[0] > minToneLevel) {
699 trebles[0] -= STEP_TONE_LEVEL;
701 text = buildTrebleLine1Response();
702 textLine1Right = buildTrebleLine1RightResponse();
703 textAscii = buildTrebleAsciiResponse();
706 if (!tcbypass && value != null) {
709 text = buildTrebleLine1Response();
710 textLine1Right = buildTrebleLine1RightResponse();
711 textAscii = buildTrebleAsciiResponse();
714 textAscii = buildTrebleAsciiResponse();
716 case TONE_CONTROL_SELECT:
717 showTreble = !showTreble;
719 text = buildTrebleLine1Response();
720 textLine1Right = buildTrebleLine1RightResponse();
722 text = buildBassLine1Response();
723 textLine1Right = buildBassLine1RightResponse();
727 if (balances[0] > minBalance) {
730 textAscii = buildBalanceAsciiResponse();
733 if (balances[0] < maxBalance) {
736 textAscii = buildBalanceAsciiResponse();
742 textAscii = buildBalanceAsciiResponse();
745 textAscii = buildBalanceAsciiResponse();
747 case SPEAKER_A_TOGGLE:
748 speakerA = !speakerA;
749 textAscii = buildSpeakerAsciiResponse();
753 textAscii = buildSpeakerAsciiResponse();
757 textAscii = buildSpeakerAsciiResponse();
759 case SPEAKER_B_TOGGLE:
760 speakerB = !speakerB;
761 textAscii = buildSpeakerAsciiResponse();
765 textAscii = buildSpeakerAsciiResponse();
769 textAscii = buildSpeakerAsciiResponse();
772 textAscii = buildSpeakerAsciiResponse();
775 playStatus = RotelPlayStatus.PLAYING;
776 textAscii = buildPlayStatusAsciiResponse();
779 playStatus = RotelPlayStatus.STOPPED;
780 textAscii = buildPlayStatusAsciiResponse();
783 switch (playStatus) {
785 playStatus = RotelPlayStatus.PAUSED;
789 playStatus = RotelPlayStatus.PLAYING;
792 textAscii = buildPlayStatusAsciiResponse();
796 textAscii = buildPlayStatusAsciiResponse();
800 textAscii = buildTrackAsciiResponse();
806 textAscii = buildTrackAsciiResponse();
809 textAscii = buildTrackAsciiResponse();
812 randomMode = !randomMode;
813 textAscii = buildRandomModeAsciiResponse();
816 textAscii = buildRandomModeAsciiResponse();
819 switch (repeatMode) {
821 repeatMode = RotelRepeatMode.DISC;
824 repeatMode = RotelRepeatMode.OFF;
827 repeatMode = RotelRepeatMode.TRACK;
830 textAscii = buildRepeatModeAsciiResponse();
833 textAscii = buildRepeatModeAsciiResponse();
835 case SOURCE_MULTI_INPUT:
836 multiinput = !multiinput;
837 text = "MULTI IN " + (multiinput ? "ON" : "OFF");
839 sources[0] = model.getSourceFromCommand(cmd);
840 textLine1Left = buildSourceLine1LeftResponse();
841 textAscii = buildSourceAsciiResponse();
843 } catch (RotelException e) {
848 textAscii = buildSourceAsciiResponse();
851 dsp = RotelDsp.CAT4_NONE;
852 textLine2 = bypass ? "BYPASS" : "STEREO";
853 textAscii = buildDspAsciiResponse();
856 dsp = RotelDsp.CAT4_STEREO3;
857 textLine2 = "DOLBY 3 STEREO";
858 textAscii = buildDspAsciiResponse();
861 dsp = RotelDsp.CAT4_STEREO5;
862 textLine2 = "5CH STEREO";
863 textAscii = buildDspAsciiResponse();
866 dsp = RotelDsp.CAT4_STEREO7;
867 textLine2 = "7CH STEREO";
868 textAscii = buildDspAsciiResponse();
871 dsp = RotelDsp.CAT5_STEREO9;
872 textAscii = buildDspAsciiResponse();
875 dsp = RotelDsp.CAT5_STEREO11;
876 textAscii = buildDspAsciiResponse();
879 dsp = RotelDsp.CAT4_DSP1;
881 textAscii = buildDspAsciiResponse();
884 dsp = RotelDsp.CAT4_DSP2;
886 textAscii = buildDspAsciiResponse();
889 dsp = RotelDsp.CAT4_DSP3;
891 textAscii = buildDspAsciiResponse();
894 dsp = RotelDsp.CAT4_DSP4;
896 textAscii = buildDspAsciiResponse();
899 dsp = RotelDsp.CAT4_PROLOGIC;
900 textLine2 = "DOLBY PRO LOGIC";
901 textAscii = buildDspAsciiResponse();
904 dsp = RotelDsp.CAT4_PLII_CINEMA;
905 textLine2 = "DOLBY PL C";
906 textAscii = buildDspAsciiResponse();
909 dsp = RotelDsp.CAT4_PLII_MUSIC;
910 textLine2 = "DOLBY PL M";
911 textAscii = buildDspAsciiResponse();
914 dsp = RotelDsp.CAT4_PLII_GAME;
915 textLine2 = "DOLBY PL G";
916 textAscii = buildDspAsciiResponse();
919 dsp = RotelDsp.CAT4_PLIIZ;
920 textLine2 = "DOLBY PL z";
921 textAscii = buildDspAsciiResponse();
924 dsp = RotelDsp.CAT4_NEO6_MUSIC;
925 textLine2 = "DTS Neo:6 M";
926 textAscii = buildDspAsciiResponse();
929 dsp = RotelDsp.CAT4_NEO6_CINEMA;
930 textLine2 = "DTS Neo:6 C";
931 textAscii = buildDspAsciiResponse();
934 dsp = RotelDsp.CAT5_ATMOS;
935 textAscii = buildDspAsciiResponse();
938 dsp = RotelDsp.CAT5_NEURAL_X;
939 textAscii = buildDspAsciiResponse();
942 dsp = RotelDsp.CAT5_BYPASS;
943 textAscii = buildDspAsciiResponse();
946 textAscii = buildDspAsciiResponse();
948 case STEREO_BYPASS_TOGGLE:
950 textLine2 = bypass ? "BYPASS" : "STEREO";
953 textAscii = model.getNumberOfZones() > 1 ? buildAsciiResponse(KEY_FREQ, "44.1,48,none,176.4")
954 : buildAsciiResponse(KEY_FREQ, "44.1");
957 subLevel += STEP_DECIBEL;
958 textAscii = buildAsciiResponse(KEY_SUB_LEVEL, buildDecibelValue(subLevel));
961 subLevel -= STEP_DECIBEL;
962 textAscii = buildAsciiResponse(KEY_SUB_LEVEL, buildDecibelValue(subLevel));
965 centerLevel += STEP_DECIBEL;
966 textAscii = buildAsciiResponse(KEY_CENTER_LEVEL, buildDecibelValue(centerLevel));
969 centerLevel -= STEP_DECIBEL;
970 textAscii = buildAsciiResponse(KEY_CENTER_LEVEL, buildDecibelValue(centerLevel));
973 surroundRightLevel += STEP_DECIBEL;
974 textAscii = buildAsciiResponse(KEY_SURROUND_RIGHT_LEVEL, buildDecibelValue(surroundRightLevel));
977 surroundRightLevel -= STEP_DECIBEL;
978 textAscii = buildAsciiResponse(KEY_SURROUND_RIGHT_LEVEL, buildDecibelValue(surroundRightLevel));
981 surroundLefLevel += STEP_DECIBEL;
982 textAscii = buildAsciiResponse(KEY_SURROUND_LEFT_LEVEL, buildDecibelValue(surroundLefLevel));
985 surroundLefLevel -= STEP_DECIBEL;
986 textAscii = buildAsciiResponse(KEY_SURROUND_LEFT_LEVEL, buildDecibelValue(surroundLefLevel));
989 centerBackRightLevel += STEP_DECIBEL;
990 textAscii = buildAsciiResponse(KEY_CENTER_BACK_RIGHT_LEVEL,
991 buildDecibelValue(centerBackRightLevel));
994 centerBackRightLevel -= STEP_DECIBEL;
995 textAscii = buildAsciiResponse(KEY_CENTER_BACK_RIGHT_LEVEL,
996 buildDecibelValue(centerBackRightLevel));
999 centerBackLefLevel += STEP_DECIBEL;
1000 textAscii = buildAsciiResponse(KEY_CENTER_BACK_LEFT_LEVEL, buildDecibelValue(centerBackLefLevel));
1002 case CBL_LEVEL_DOWN:
1003 centerBackLefLevel -= STEP_DECIBEL;
1004 textAscii = buildAsciiResponse(KEY_CENTER_BACK_LEFT_LEVEL, buildDecibelValue(centerBackLefLevel));
1007 ceilingFrontRightLevel += STEP_DECIBEL;
1008 textAscii = buildAsciiResponse(KEY_CEILING_FRONT_RIGHT_LEVEL,
1009 buildDecibelValue(ceilingFrontRightLevel));
1011 case CFR_LEVEL_DOWN:
1012 ceilingFrontRightLevel -= STEP_DECIBEL;
1013 textAscii = buildAsciiResponse(KEY_CEILING_FRONT_RIGHT_LEVEL,
1014 buildDecibelValue(ceilingFrontRightLevel));
1017 ceilingFrontLefLevel += STEP_DECIBEL;
1018 textAscii = buildAsciiResponse(KEY_CEILING_FRONT_LEFT_LEVEL,
1019 buildDecibelValue(ceilingFrontLefLevel));
1021 case CFL_LEVEL_DOWN:
1022 ceilingFrontLefLevel -= STEP_DECIBEL;
1023 textAscii = buildAsciiResponse(KEY_CEILING_FRONT_LEFT_LEVEL,
1024 buildDecibelValue(ceilingFrontLefLevel));
1027 ceilingRearRightLevel += STEP_DECIBEL;
1028 textAscii = buildAsciiResponse(KEY_CEILING_REAR_RIGHT_LEVEL,
1029 buildDecibelValue(ceilingRearRightLevel));
1031 case CRR_LEVEL_DOWN:
1032 ceilingRearRightLevel -= STEP_DECIBEL;
1033 textAscii = buildAsciiResponse(KEY_CEILING_REAR_RIGHT_LEVEL,
1034 buildDecibelValue(ceilingRearRightLevel));
1037 ceilingRearLefLevel += STEP_DECIBEL;
1038 textAscii = buildAsciiResponse(KEY_CEILING_REAR_LEFT_LEVEL, buildDecibelValue(ceilingRearLefLevel));
1040 case CRL_LEVEL_DOWN:
1041 ceilingRearLefLevel -= STEP_DECIBEL;
1042 textAscii = buildAsciiResponse(KEY_CEILING_REAR_LEFT_LEVEL, buildDecibelValue(ceilingRearLefLevel));
1044 case DIMMER_LEVEL_SET:
1045 if (value != null) {
1048 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
1050 case DIMMER_LEVEL_GET:
1051 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
1055 textAscii = buildAsciiResponse(KEY_PCUSB_CLASS, pcUsbClass);
1059 textAscii = buildAsciiResponse(KEY_PCUSB_CLASS, pcUsbClass);
1062 textAscii = buildAsciiResponse(KEY_PCUSB_CLASS, pcUsbClass);
1065 textAscii = buildAsciiResponse(KEY_MODEL, model.getName());
1068 textAscii = buildAsciiResponse(KEY_VERSION, "1.00");
1075 // Check if command is a change of source input for the main zone
1077 sources[0] = model.getZoneSourceFromCommand(cmd, 1);
1078 text = buildSourceLine1Response();
1079 textLine1Left = buildSourceLine1LeftResponse();
1080 textAscii = buildSourceAsciiResponse();
1082 } catch (RotelException e) {
1086 // Check if command is a change of source input
1088 if (selectingRecord && !model.hasOtherThanPrimaryCommands()) {
1089 recordSource = model.getSourceFromCommand(cmd);
1091 sources[0] = model.getSourceFromCommand(cmd);
1093 text = buildSourceLine1Response();
1094 textLine1Left = buildSourceLine1LeftResponse();
1095 textAscii = buildSourceAsciiResponse();
1098 } catch (RotelException e) {
1102 // Check if command is a change of record source
1104 recordSource = model.getRecordSourceFromCommand(cmd);
1105 text = buildSourceLine1Response();
1106 textLine2 = buildRecordResponse();
1108 } catch (RotelException e) {
1117 if (cmd != RotelCommand.RECORD_FONCTION_SELECT) {
1118 selectingRecord = false;
1124 if (model.getRespNbChars() == 42) {
1125 while (textLine1Left.length() < 14) {
1126 textLine1Left += " ";
1128 while (textLine1Right.length() < 7) {
1129 textLine1Right += " ";
1131 while (textLine2.length() < 21) {
1134 text = textLine1Left + textLine1Right + textLine2;
1137 if (protocol == RotelProtocol.HEX) {
1138 byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), model.getRespNbChars());
1139 byte[] flags = new byte[model.getRespNbFlags()];
1141 model.setMultiInput(flags, multiinput);
1142 } catch (RotelException e) {
1145 model.setZone2(flags, powers[2]);
1146 } catch (RotelException e) {
1149 model.setZone3(flags, powers[3]);
1150 } catch (RotelException e) {
1153 model.setZone4(flags, powers[4]);
1154 } catch (RotelException e) {
1156 int size = 6 + model.getRespNbChars() + model.getRespNbFlags();
1157 byte[] dataBuffer = new byte[size];
1159 dataBuffer[idx++] = START;
1160 dataBuffer[idx++] = (byte) (size - 4);
1161 dataBuffer[idx++] = model.getDeviceId();
1162 dataBuffer[idx++] = STANDARD_RESPONSE;
1163 if (model.isCharsBeforeFlags()) {
1164 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1165 idx += model.getRespNbChars();
1166 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1167 idx += model.getRespNbFlags();
1169 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1170 idx += model.getRespNbFlags();
1171 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1172 idx += model.getRespNbChars();
1174 byte checksum = RotelHexProtocolHandler.computeCheckSum(dataBuffer, idx - 1);
1175 if ((checksum & 0x000000FF) == 0x000000FD) {
1176 dataBuffer[idx++] = (byte) 0xFD;
1177 dataBuffer[idx++] = 0;
1178 } else if ((checksum & 0x000000FF) == 0x000000FE) {
1179 dataBuffer[idx++] = (byte) 0xFD;
1180 dataBuffer[idx++] = 1;
1182 dataBuffer[idx++] = checksum;
1184 synchronized (lock) {
1185 feedbackMsg = Arrays.copyOf(dataBuffer, idx);
1186 idxInFeedbackMsg = 0;
1189 String command = textAscii + (protocol == RotelProtocol.ASCII_V1 ? "!" : "$");
1190 synchronized (lock) {
1191 feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
1192 idxInFeedbackMsg = 0;
1197 private String buildAsciiResponse(String key, String value) {
1198 return String.format("%s=%s", key, value);
1201 private String buildAsciiResponse(String key, int value) {
1202 return String.format("%s=%d", key, value);
1205 private String buildAsciiResponse(String key, boolean value) {
1206 return buildAsciiResponse(key, buildOnOffValue(value));
1209 private String buildOnOffValue(boolean on) {
1210 return on ? MSG_VALUE_ON : MSG_VALUE_OFF;
1213 private String buildPowerAsciiResponse() {
1214 return buildAsciiResponse(KEY_POWER, powers[0] ? POWER_ON : STANDBY);
1217 private String buildVolumeAsciiResponse() {
1218 if (model.getNumberOfZones() > 1) {
1219 StringJoiner sj = new StringJoiner(",");
1220 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1221 sj.add(String.format("%02d", volumes[zone]));
1223 return buildAsciiResponse(KEY_VOLUME, sj.toString());
1225 return buildAsciiResponse(KEY_VOLUME, String.format("%02d", volumes[0]));
1229 private String buildMuteAsciiResponse() {
1230 if (model.getNumberOfZones() > 1) {
1231 StringJoiner sj = new StringJoiner(",");
1232 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1233 sj.add(buildOnOffValue(mutes[zone]));
1235 return buildAsciiResponse(KEY_MUTE, sj.toString());
1237 return buildAsciiResponse(KEY_MUTE, mutes[0]);
1241 private String buildBassAsciiResponse() {
1242 if (model.getNumberOfZones() > 1) {
1243 StringJoiner sj = new StringJoiner(",");
1244 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1245 sj.add(buildBassTrebleValue(basses[zone]));
1247 return buildAsciiResponse(KEY_BASS, sj.toString());
1249 return buildAsciiResponse(KEY_BASS, buildBassTrebleValue(basses[0]));
1253 private String buildTrebleAsciiResponse() {
1254 if (model.getNumberOfZones() > 1) {
1255 StringJoiner sj = new StringJoiner(",");
1256 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1257 sj.add(buildBassTrebleValue(trebles[zone]));
1259 return buildAsciiResponse(KEY_TREBLE, sj.toString());
1261 return buildAsciiResponse(KEY_TREBLE, buildBassTrebleValue(trebles[0]));
1265 private String buildBassTrebleValue(int value) {
1266 if (tcbypass || value == 0) {
1268 } else if (value > 0) {
1269 return String.format("+%02d", value);
1271 return String.format("-%02d", -value);
1275 private String buildBalanceAsciiResponse() {
1276 if (model.getNumberOfZones() > 1) {
1277 StringJoiner sj = new StringJoiner(",");
1278 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1279 sj.add(buildBalanceValue(balances[zone]));
1281 return buildAsciiResponse(KEY_BALANCE, sj.toString());
1283 return buildAsciiResponse(KEY_BALANCE, buildBalanceValue(balances[0]));
1287 private String buildBalanceValue(int value) {
1290 } else if (value > 0) {
1291 return String.format("r%02d", value);
1293 return String.format("l%02d", -value);
1297 private String buildSpeakerAsciiResponse() {
1299 if (speakerA && speakerB) {
1300 value = MSG_VALUE_SPEAKER_AB;
1301 } else if (speakerA && !speakerB) {
1302 value = MSG_VALUE_SPEAKER_A;
1303 } else if (!speakerA && speakerB) {
1304 value = MSG_VALUE_SPEAKER_B;
1306 value = MSG_VALUE_OFF;
1308 return buildAsciiResponse(KEY_SPEAKER, value);
1311 private String buildPlayStatusAsciiResponse() {
1313 switch (playStatus) {
1324 return buildAsciiResponse(protocol == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_STATUS, status);
1327 private String buildTrackAsciiResponse() {
1328 return buildAsciiResponse(KEY_TRACK, String.format("%03d", track));
1331 private String buildRandomModeAsciiResponse() {
1332 return buildAsciiResponse(KEY_RANDOM, randomMode);
1335 private String buildRepeatModeAsciiResponse() {
1337 switch (repeatMode) {
1345 mode = MSG_VALUE_OFF;
1348 return buildAsciiResponse(KEY_REPEAT, mode);
1351 private String buildSourceAsciiResponse() {
1352 if (model.getNumberOfZones() > 1) {
1353 StringJoiner sj = new StringJoiner(",");
1354 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1355 sj.add(buildZoneSourceValue(sources[zone]));
1357 return buildAsciiResponse(KEY_INPUT, sj.toString());
1359 return buildAsciiResponse(KEY_SOURCE, buildSourceValue(sources[0]));
1363 private String buildSourceValue(RotelSource source) {
1365 RotelCommand command = source.getCommand();
1366 if (command != null) {
1367 str = protocol == RotelProtocol.ASCII_V1 ? command.getAsciiCommandV1() : command.getAsciiCommandV2();
1369 return str == null ? "" : str;
1372 private String buildZoneSourceValue(RotelSource source) {
1373 String str = buildSourceValue(source);
1374 int idx = str.indexOf("input_");
1375 return idx < 0 ? str : str.substring(idx + 6);
1378 private String buildDspAsciiResponse() {
1379 return buildAsciiResponse(KEY_DSP_MODE, dsp.getFeedback());
1382 private String buildDecibelValue(double value) {
1386 return String.format("%+05.1fdb", value).replace(",", ".");
1390 private String buildSourceLine1Response() {
1394 } else if (mutes[0]) {
1397 text = getSourceLabel(sources[0], false) + " " + getSourceLabel(recordSource, true);
1402 private String buildSourceLine1LeftResponse() {
1407 text = getSourceLabel(sources[0], false);
1412 private String buildRecordResponse() {
1417 text = "REC " + getSourceLabel(recordSource, true);
1422 private String buildZonePowerResponse(int numZone) {
1425 zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1427 zone = String.format("ZONE%d", numZone);
1429 String state = powers[numZone] ? getSourceLabel(sources[numZone], true) : "OFF";
1430 return zone + " " + state;
1433 private String buildVolumeLine1Response() {
1435 if (volumes[0] == minVolume) {
1436 text = " VOLUME MIN ";
1437 } else if (volumes[0] == maxVolume) {
1438 text = " VOLUME MAX ";
1440 text = String.format(" VOLUME %02d ", volumes[0]);
1445 private String buildVolumeLine1RightResponse() {
1449 } else if (mutes[0]) {
1451 } else if (volumes[0] == minVolume) {
1453 } else if (volumes[0] == maxVolume) {
1456 text = String.format("VOL %02d", volumes[0]);
1461 private String buildZoneVolumeResponse(int numZone) {
1464 zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1466 zone = String.format("ZONE%d", numZone);
1469 if (mutes[numZone]) {
1470 text = zone + " MUTE ON";
1471 } else if (volumes[numZone] == minVolume) {
1472 text = zone + " VOL MIN";
1473 } else if (volumes[numZone] == maxVolume) {
1474 text = zone + " VOL MAX";
1476 text = String.format("%s VOL %02d", zone, volumes[numZone]);
1481 private String buildBassLine1Response() {
1483 if (basses[0] == minToneLevel) {
1484 text = " BASS MIN ";
1485 } else if (basses[0] == maxToneLevel) {
1486 text = " BASS MAX ";
1487 } else if (basses[0] == 0) {
1489 } else if (basses[0] > 0) {
1490 text = String.format(" BASS +%02d ", basses[0]);
1492 text = String.format(" BASS -%02d ", -basses[0]);
1497 private String buildBassLine1RightResponse() {
1499 if (basses[0] == minToneLevel) {
1501 } else if (basses[0] == maxToneLevel) {
1503 } else if (basses[0] == 0) {
1505 } else if (basses[0] > 0) {
1506 text = String.format("LF + %02d", basses[0]);
1508 text = String.format("LF - %02d", -basses[0]);
1513 private String buildTrebleLine1Response() {
1515 if (trebles[0] == minToneLevel) {
1516 text = " TREBLE MIN ";
1517 } else if (trebles[0] == maxToneLevel) {
1518 text = " TREBLE MAX ";
1519 } else if (trebles[0] == 0) {
1520 text = " TREBLE 0 ";
1521 } else if (trebles[0] > 0) {
1522 text = String.format(" TREBLE +%02d ", trebles[0]);
1524 text = String.format(" TREBLE -%02d ", -trebles[0]);
1529 private String buildTrebleLine1RightResponse() {
1531 if (trebles[0] == minToneLevel) {
1533 } else if (trebles[0] == maxToneLevel) {
1535 } else if (trebles[0] == 0) {
1537 } else if (trebles[0] > 0) {
1538 text = String.format("HF + %02d", trebles[0]);
1540 text = String.format("HF - %02d", -trebles[0]);
1545 private String getSourceLabel(RotelSource source, boolean considerFollowMain) {
1547 if (considerFollowMain && source.getName().equals(RotelSource.CAT1_FOLLOW_MAIN.getName())) {
1550 label = Objects.requireNonNullElse(sourcesLabels.get(source), source.getLabel());