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.protocol.RotelAbstractProtocolHandler;
32 import org.openhab.binding.rotel.internal.protocol.RotelProtocol;
33 import org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
38 * Class for simulating the communication with the Rotel device
40 * @author Laurent Garnier - Initial contribution
43 public class RotelSimuConnector extends RotelConnector {
45 private static final int STEP_TONE_LEVEL = 1;
47 private final Logger logger = LoggerFactory.getLogger(RotelSimuConnector.class);
49 private final RotelModel model;
50 private final RotelProtocol protocol;
51 private final Map<RotelSource, String> sourcesLabels;
53 private Object lock = new Object();
55 private byte[] feedbackMsg = new byte[1];
56 private int idxInFeedbackMsg = feedbackMsg.length;
58 private boolean[] powers = { false, false, false, false, false };
59 private RotelSource[] sources;
60 private RotelSource recordSource;
61 private boolean multiinput;
62 private RotelDsp dsp = RotelDsp.CAT4_NONE;
63 private int[] volumes = { 50, 10, 20, 30, 40 };
64 private boolean[] mutes = { false, false, false, false, false };
65 private boolean tcbypass;
66 private int[] basses = { 0, 0, 0, 0, 0 };
67 private int[] trebles = { 0, 0, 0, 0, 0 };
68 private int[] balances = { 0, 0, 0, 0, 0 };
69 private boolean showTreble;
70 private boolean speakerA = true;
71 private boolean speakerB = false;
72 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
73 private int track = 1;
74 private boolean selectingRecord;
78 private int minVolume;
79 private int maxVolume;
80 private int minToneLevel;
81 private int maxToneLevel;
82 private int minBalance;
83 private int maxBalance;
88 * @param model the projector model in use
89 * @param protocolHandler the protocol handler
90 * @param sourcesLabels the custom labels for sources
91 * @param readerThreadName the name of thread to be created
93 public RotelSimuConnector(RotelModel model, RotelAbstractProtocolHandler protocolHandler,
94 Map<RotelSource, String> sourcesLabels, String readerThreadName) {
95 super(protocolHandler, true, readerThreadName);
97 this.protocol = protocolHandler.getProtocol();
98 this.sourcesLabels = sourcesLabels;
100 this.maxVolume = model.hasVolumeControl() ? model.getVolumeMax() : 0;
101 this.maxToneLevel = model.hasToneControl() ? model.getToneLevelMax() : 0;
102 this.minToneLevel = -this.maxToneLevel;
103 this.maxBalance = model.hasBalanceControl() ? model.getBalanceLevelMax() : 0;
104 this.minBalance = -this.maxBalance;
105 List<RotelSource> modelSources = model.getSources();
106 RotelSource source = modelSources.isEmpty() ? RotelSource.CAT0_CD : modelSources.get(0);
107 sources = new RotelSource[] { source, source, source, source, source };
108 recordSource = source;
112 public synchronized void open() throws RotelException {
113 logger.debug("Opening simulated connection");
114 readerThread.start();
116 logger.debug("Simulated connection opened");
120 public synchronized void close() {
121 logger.debug("Closing simulated connection");
124 logger.debug("Simulated connection closed");
128 protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
129 synchronized (lock) {
130 int len = feedbackMsg.length - idxInFeedbackMsg;
132 if (len > dataBuffer.length) {
133 len = dataBuffer.length;
135 System.arraycopy(feedbackMsg, idxInFeedbackMsg, dataBuffer, 0, len);
136 idxInFeedbackMsg += len;
140 // Give more chance to someone else than the reader thread to get the lock
143 } catch (InterruptedException e) {
144 Thread.currentThread().interrupt();
150 * Built the simulated feedback message for a sent command
152 * @param cmd the sent command
153 * @param value the integer value considered in the sent command for volume, bass or treble adjustment
155 public void buildFeedbackMessage(RotelCommand cmd, @Nullable Integer value) {
156 String text = buildSourceLine1Response();
157 String textLine1Left = buildSourceLine1LeftResponse();
158 String textLine1Right = buildVolumeLine1RightResponse();
159 String textLine2 = "";
160 String textAscii = "";
161 boolean accepted = true;
162 boolean resetZone = true;
165 case ZONE1_VOLUME_UP:
166 case ZONE1_VOLUME_DOWN:
167 case ZONE1_VOLUME_SET:
168 case ZONE1_MUTE_TOGGLE:
172 case ZONE1_BASS_DOWN:
174 case ZONE1_TREBLE_UP:
175 case ZONE1_TREBLE_DOWN:
176 case ZONE1_TREBLE_SET:
177 case ZONE1_BALANCE_LEFT:
178 case ZONE1_BALANCE_RIGHT:
179 case ZONE1_BALANCE_SET:
182 case ZONE2_POWER_OFF:
184 case ZONE2_VOLUME_UP:
185 case ZONE2_VOLUME_DOWN:
186 case ZONE2_VOLUME_SET:
187 case ZONE2_MUTE_TOGGLE:
191 case ZONE2_BASS_DOWN:
193 case ZONE2_TREBLE_UP:
194 case ZONE2_TREBLE_DOWN:
195 case ZONE2_TREBLE_SET:
196 case ZONE2_BALANCE_LEFT:
197 case ZONE2_BALANCE_RIGHT:
198 case ZONE2_BALANCE_SET:
201 case ZONE3_POWER_OFF:
203 case ZONE3_VOLUME_UP:
204 case ZONE3_VOLUME_DOWN:
205 case ZONE3_VOLUME_SET:
206 case ZONE3_MUTE_TOGGLE:
210 case ZONE3_BASS_DOWN:
212 case ZONE3_TREBLE_UP:
213 case ZONE3_TREBLE_DOWN:
214 case ZONE3_TREBLE_SET:
215 case ZONE3_BALANCE_LEFT:
216 case ZONE3_BALANCE_RIGHT:
217 case ZONE3_BALANCE_SET:
220 case ZONE4_POWER_OFF:
222 case ZONE4_VOLUME_UP:
223 case ZONE4_VOLUME_DOWN:
224 case ZONE4_VOLUME_SET:
225 case ZONE4_MUTE_TOGGLE:
229 case ZONE4_BASS_DOWN:
231 case ZONE4_TREBLE_UP:
232 case ZONE4_TREBLE_DOWN:
233 case ZONE4_TREBLE_SET:
234 case ZONE4_BALANCE_LEFT:
235 case ZONE4_BALANCE_RIGHT:
236 case ZONE4_BALANCE_SET:
243 case DISPLAY_REFRESH:
246 case MAIN_ZONE_POWER_OFF:
248 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
249 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
250 powers[zone] = false;
253 text = buildSourceLine1Response();
254 textLine1Left = buildSourceLine1LeftResponse();
255 textLine1Right = buildVolumeLine1RightResponse();
256 textAscii = buildPowerAsciiResponse();
259 case MAIN_ZONE_POWER_ON:
261 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
262 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
266 text = buildSourceLine1Response();
267 textLine1Left = buildSourceLine1LeftResponse();
268 textLine1Right = buildVolumeLine1RightResponse();
269 textAscii = buildPowerAsciiResponse();
272 textAscii = buildPowerAsciiResponse();
274 case ZONE2_POWER_OFF:
275 case ZONE3_POWER_OFF:
276 case ZONE4_POWER_OFF:
277 powers[numZone] = false;
278 text = textLine2 = buildZonePowerResponse(numZone);
285 powers[numZone] = true;
286 text = textLine2 = buildZonePowerResponse(numZone);
290 case RECORD_FONCTION_SELECT:
291 if (model.getNumberOfZones() > 1 && model.getZoneSelectCmd() == cmd) {
293 if (showZone >= model.getNumberOfZones()) {
303 selectingRecord = powers[0];
305 textLine2 = buildRecordResponse();
306 } else if (showZone >= 2 && showZone <= 4) {
307 selectingRecord = false;
308 text = textLine2 = buildZonePowerResponse(showZone);
313 if (model.getNumberOfZones() == 1 || (model.getNumberOfZones() > 2 && model.getZoneSelectCmd() == cmd)
314 || (showZone == 1 && model.getZoneSelectCmd() != cmd)) {
317 if (model.getZoneSelectCmd() == cmd) {
318 if (!powers[0] && !powers[2]) {
321 } else if (showZone == 2) {
322 powers[2] = !powers[2];
326 } else if (showZone >= 2 && showZone <= 4) {
327 powers[showZone] = !powers[showZone];
329 if (showZone >= 2 && showZone <= 4) {
330 text = textLine2 = buildZonePowerResponse(showZone);
339 if (!accepted && numZone > 0 && powers[numZone]) {
342 case ZONE1_VOLUME_UP:
343 case ZONE2_VOLUME_UP:
344 case ZONE3_VOLUME_UP:
345 case ZONE4_VOLUME_UP:
346 if (volumes[numZone] < maxVolume) {
349 text = textLine2 = buildZoneVolumeResponse(numZone);
350 textAscii = buildVolumeAsciiResponse();
352 case ZONE1_VOLUME_DOWN:
353 case ZONE2_VOLUME_DOWN:
354 case ZONE3_VOLUME_DOWN:
355 case ZONE4_VOLUME_DOWN:
356 if (volumes[numZone] > minVolume) {
359 text = textLine2 = buildZoneVolumeResponse(numZone);
360 textAscii = buildVolumeAsciiResponse();
362 case ZONE1_VOLUME_SET:
363 case ZONE2_VOLUME_SET:
364 case ZONE3_VOLUME_SET:
365 case ZONE4_VOLUME_SET:
367 volumes[numZone] = value;
369 text = textLine2 = buildZoneVolumeResponse(numZone);
370 textAscii = buildVolumeAsciiResponse();
372 case ZONE1_MUTE_TOGGLE:
373 case ZONE2_MUTE_TOGGLE:
374 case ZONE3_MUTE_TOGGLE:
375 case ZONE4_MUTE_TOGGLE:
376 mutes[numZone] = !mutes[numZone];
377 text = textLine2 = buildZoneVolumeResponse(numZone);
378 textAscii = buildMuteAsciiResponse();
384 mutes[numZone] = true;
385 text = textLine2 = buildZoneVolumeResponse(numZone);
386 textAscii = buildMuteAsciiResponse();
392 mutes[numZone] = false;
393 text = textLine2 = buildZoneVolumeResponse(numZone);
394 textAscii = buildMuteAsciiResponse();
400 if (!tcbypass && basses[numZone] < maxToneLevel) {
401 basses[numZone] += STEP_TONE_LEVEL;
403 textAscii = buildBassAsciiResponse();
405 case ZONE1_BASS_DOWN:
406 case ZONE2_BASS_DOWN:
407 case ZONE3_BASS_DOWN:
408 case ZONE4_BASS_DOWN:
409 if (!tcbypass && basses[numZone] > minToneLevel) {
410 basses[numZone] -= STEP_TONE_LEVEL;
412 textAscii = buildBassAsciiResponse();
418 if (!tcbypass && value != null) {
419 basses[numZone] = value;
421 textAscii = buildBassAsciiResponse();
423 case ZONE1_TREBLE_UP:
424 case ZONE2_TREBLE_UP:
425 case ZONE3_TREBLE_UP:
426 case ZONE4_TREBLE_UP:
427 if (!tcbypass && trebles[numZone] < maxToneLevel) {
428 trebles[numZone] += STEP_TONE_LEVEL;
430 textAscii = buildTrebleAsciiResponse();
432 case ZONE1_TREBLE_DOWN:
433 case ZONE2_TREBLE_DOWN:
434 case ZONE3_TREBLE_DOWN:
435 case ZONE4_TREBLE_DOWN:
436 if (!tcbypass && trebles[numZone] > minToneLevel) {
437 trebles[numZone] -= STEP_TONE_LEVEL;
439 textAscii = buildTrebleAsciiResponse();
441 case ZONE1_TREBLE_SET:
442 case ZONE2_TREBLE_SET:
443 case ZONE3_TREBLE_SET:
444 case ZONE4_TREBLE_SET:
445 if (!tcbypass && value != null) {
446 trebles[numZone] = value;
448 textAscii = buildTrebleAsciiResponse();
450 case ZONE1_BALANCE_LEFT:
451 case ZONE2_BALANCE_LEFT:
452 case ZONE3_BALANCE_LEFT:
453 case ZONE4_BALANCE_LEFT:
454 if (balances[numZone] > minBalance) {
457 textAscii = buildBalanceAsciiResponse();
459 case ZONE1_BALANCE_RIGHT:
460 case ZONE2_BALANCE_RIGHT:
461 case ZONE3_BALANCE_RIGHT:
462 case ZONE4_BALANCE_RIGHT:
463 if (balances[numZone] < maxBalance) {
466 textAscii = buildBalanceAsciiResponse();
468 case ZONE1_BALANCE_SET:
469 case ZONE2_BALANCE_SET:
470 case ZONE3_BALANCE_SET:
471 case ZONE4_BALANCE_SET:
473 balances[numZone] = value;
475 textAscii = buildBalanceAsciiResponse();
483 // Check if command is a change of source input for a zone
484 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
487 sources[zone] = model.getZoneSourceFromCommand(cmd, zone);
488 text = textLine2 = buildZonePowerResponse(zone);
489 textAscii = buildSourceAsciiResponse();
495 } catch (RotelException e) {
500 if (!accepted && powers[2] && !model.hasZoneCommands(2) && model.getNumberOfZones() > 1 && showZone == 2) {
504 if (volumes[2] < maxVolume) {
507 text = textLine2 = buildZoneVolumeResponse(2);
511 if (volumes[2] > minVolume) {
514 text = textLine2 = buildZoneVolumeResponse(2);
521 text = textLine2 = buildZoneVolumeResponse(2);
530 sources[2] = model.getSourceFromCommand(cmd);
531 text = textLine2 = buildZonePowerResponse(2);
535 } catch (RotelException e) {
539 if (!accepted && powers[0]) {
543 textAscii = buildAsciiResponse(
544 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, AUTO);
547 textAscii = buildAsciiResponse(
548 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, MANUAL);
551 textAscii = buildAsciiResponse(KEY_VOLUME_MIN, minVolume);
554 textAscii = buildAsciiResponse(KEY_VOLUME_MAX, maxVolume);
557 case MAIN_ZONE_VOLUME_UP:
558 if (volumes[0] < maxVolume) {
561 text = buildVolumeLine1Response();
562 textLine1Right = buildVolumeLine1RightResponse();
563 textAscii = buildVolumeAsciiResponse();
566 case MAIN_ZONE_VOLUME_DOWN:
567 if (volumes[0] > minVolume) {
570 text = buildVolumeLine1Response();
571 textLine1Right = buildVolumeLine1RightResponse();
572 textAscii = buildVolumeAsciiResponse();
578 text = buildVolumeLine1Response();
579 textLine1Right = buildVolumeLine1RightResponse();
580 textAscii = buildVolumeAsciiResponse();
583 textAscii = buildVolumeAsciiResponse();
586 case MAIN_ZONE_MUTE_TOGGLE:
587 mutes[0] = !mutes[0];
588 text = buildSourceLine1Response();
589 textLine1Right = buildVolumeLine1RightResponse();
590 textAscii = buildMuteAsciiResponse();
593 case MAIN_ZONE_MUTE_ON:
595 text = buildSourceLine1Response();
596 textLine1Right = buildVolumeLine1RightResponse();
597 textAscii = buildMuteAsciiResponse();
600 case MAIN_ZONE_MUTE_OFF:
602 text = buildSourceLine1Response();
603 textLine1Right = buildVolumeLine1RightResponse();
604 textAscii = buildMuteAsciiResponse();
607 textAscii = buildMuteAsciiResponse();
610 textAscii = buildAsciiResponse(KEY_TONE_MAX, String.format("%02d", maxToneLevel));
612 case TONE_CONTROLS_ON:
614 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
616 case TONE_CONTROLS_OFF:
618 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
621 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
625 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
629 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
632 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
635 if (!tcbypass && basses[0] < maxToneLevel) {
636 basses[0] += STEP_TONE_LEVEL;
638 text = buildBassLine1Response();
639 textLine1Right = buildBassLine1RightResponse();
640 textAscii = buildBassAsciiResponse();
643 if (!tcbypass && basses[0] > minToneLevel) {
644 basses[0] -= STEP_TONE_LEVEL;
646 text = buildBassLine1Response();
647 textLine1Right = buildBassLine1RightResponse();
648 textAscii = buildBassAsciiResponse();
651 if (!tcbypass && value != null) {
654 text = buildBassLine1Response();
655 textLine1Right = buildBassLine1RightResponse();
656 textAscii = buildBassAsciiResponse();
659 textAscii = buildBassAsciiResponse();
662 if (!tcbypass && trebles[0] < maxToneLevel) {
663 trebles[0] += STEP_TONE_LEVEL;
665 text = buildTrebleLine1Response();
666 textLine1Right = buildTrebleLine1RightResponse();
667 textAscii = buildTrebleAsciiResponse();
670 if (!tcbypass && trebles[0] > minToneLevel) {
671 trebles[0] -= STEP_TONE_LEVEL;
673 text = buildTrebleLine1Response();
674 textLine1Right = buildTrebleLine1RightResponse();
675 textAscii = buildTrebleAsciiResponse();
678 if (!tcbypass && value != null) {
681 text = buildTrebleLine1Response();
682 textLine1Right = buildTrebleLine1RightResponse();
683 textAscii = buildTrebleAsciiResponse();
686 textAscii = buildTrebleAsciiResponse();
688 case TONE_CONTROL_SELECT:
689 showTreble = !showTreble;
691 text = buildTrebleLine1Response();
692 textLine1Right = buildTrebleLine1RightResponse();
694 text = buildBassLine1Response();
695 textLine1Right = buildBassLine1RightResponse();
699 if (balances[0] > minBalance) {
702 textAscii = buildBalanceAsciiResponse();
705 if (balances[0] < maxBalance) {
708 textAscii = buildBalanceAsciiResponse();
714 textAscii = buildBalanceAsciiResponse();
717 textAscii = buildBalanceAsciiResponse();
719 case SPEAKER_A_TOGGLE:
720 speakerA = !speakerA;
721 textAscii = buildSpeakerAsciiResponse();
725 textAscii = buildSpeakerAsciiResponse();
729 textAscii = buildSpeakerAsciiResponse();
731 case SPEAKER_B_TOGGLE:
732 speakerB = !speakerB;
733 textAscii = buildSpeakerAsciiResponse();
737 textAscii = buildSpeakerAsciiResponse();
741 textAscii = buildSpeakerAsciiResponse();
744 textAscii = buildSpeakerAsciiResponse();
747 playStatus = RotelPlayStatus.PLAYING;
748 textAscii = buildPlayStatusAsciiResponse();
751 playStatus = RotelPlayStatus.STOPPED;
752 textAscii = buildPlayStatusAsciiResponse();
755 switch (playStatus) {
757 playStatus = RotelPlayStatus.PAUSED;
761 playStatus = RotelPlayStatus.PLAYING;
764 textAscii = buildPlayStatusAsciiResponse();
768 textAscii = buildPlayStatusAsciiResponse();
772 textAscii = buildTrackAsciiResponse();
778 textAscii = buildTrackAsciiResponse();
781 textAscii = buildTrackAsciiResponse();
783 case SOURCE_MULTI_INPUT:
784 multiinput = !multiinput;
785 text = "MULTI IN " + (multiinput ? "ON" : "OFF");
787 sources[0] = model.getSourceFromCommand(cmd);
788 textLine1Left = buildSourceLine1LeftResponse();
789 textAscii = buildSourceAsciiResponse();
791 } catch (RotelException e) {
796 textAscii = buildSourceAsciiResponse();
799 dsp = RotelDsp.CAT4_NONE;
800 textLine2 = "STEREO";
801 textAscii = buildDspAsciiResponse();
804 dsp = RotelDsp.CAT4_STEREO3;
805 textLine2 = "DOLBY 3 STEREO";
806 textAscii = buildDspAsciiResponse();
809 dsp = RotelDsp.CAT4_STEREO5;
810 textLine2 = "5CH STEREO";
811 textAscii = buildDspAsciiResponse();
814 dsp = RotelDsp.CAT4_STEREO7;
815 textLine2 = "7CH STEREO";
816 textAscii = buildDspAsciiResponse();
819 dsp = RotelDsp.CAT5_STEREO9;
820 textAscii = buildDspAsciiResponse();
823 dsp = RotelDsp.CAT5_STEREO11;
824 textAscii = buildDspAsciiResponse();
827 dsp = RotelDsp.CAT4_DSP1;
829 textAscii = buildDspAsciiResponse();
832 dsp = RotelDsp.CAT4_DSP2;
834 textAscii = buildDspAsciiResponse();
837 dsp = RotelDsp.CAT4_DSP3;
839 textAscii = buildDspAsciiResponse();
842 dsp = RotelDsp.CAT4_DSP4;
844 textAscii = buildDspAsciiResponse();
847 dsp = RotelDsp.CAT4_PROLOGIC;
848 textLine2 = "DOLBY PRO LOGIC";
849 textAscii = buildDspAsciiResponse();
852 dsp = RotelDsp.CAT4_PLII_CINEMA;
853 textLine2 = "DOLBY PL C";
854 textAscii = buildDspAsciiResponse();
857 dsp = RotelDsp.CAT4_PLII_MUSIC;
858 textLine2 = "DOLBY PL M";
859 textAscii = buildDspAsciiResponse();
862 dsp = RotelDsp.CAT4_PLII_GAME;
863 textLine2 = "DOLBY PL G";
864 textAscii = buildDspAsciiResponse();
867 dsp = RotelDsp.CAT4_PLIIZ;
868 textLine2 = "DOLBY PL z";
869 textAscii = buildDspAsciiResponse();
872 dsp = RotelDsp.CAT4_NEO6_MUSIC;
873 textLine2 = "DTS Neo:6 M";
874 textAscii = buildDspAsciiResponse();
877 dsp = RotelDsp.CAT4_NEO6_CINEMA;
878 textLine2 = "DTS Neo:6 C";
879 textAscii = buildDspAsciiResponse();
882 dsp = RotelDsp.CAT5_ATMOS;
883 textAscii = buildDspAsciiResponse();
886 dsp = RotelDsp.CAT5_NEURAL_X;
887 textAscii = buildDspAsciiResponse();
890 dsp = RotelDsp.CAT4_BYPASS;
891 textLine2 = "BYPASS";
892 textAscii = buildDspAsciiResponse();
895 textAscii = buildDspAsciiResponse();
898 textAscii = model.getNumberOfZones() > 1 ? buildAsciiResponse(KEY_FREQ, "44.1,48,none,176.4")
899 : buildAsciiResponse(KEY_FREQ, "44.1");
901 case DIMMER_LEVEL_SET:
905 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
907 case DIMMER_LEVEL_GET:
908 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
911 textAscii = buildAsciiResponse(KEY_MODEL, model.getName());
914 textAscii = buildAsciiResponse(KEY_VERSION, "1.00");
921 // Check if command is a change of source input for the main zone
923 sources[0] = model.getZoneSourceFromCommand(cmd, 1);
924 text = buildSourceLine1Response();
925 textLine1Left = buildSourceLine1LeftResponse();
926 textAscii = buildSourceAsciiResponse();
928 } catch (RotelException e) {
932 // Check if command is a change of source input
934 if (selectingRecord && !model.hasOtherThanPrimaryCommands()) {
935 recordSource = model.getSourceFromCommand(cmd);
937 sources[0] = model.getSourceFromCommand(cmd);
939 text = buildSourceLine1Response();
940 textLine1Left = buildSourceLine1LeftResponse();
941 textAscii = buildSourceAsciiResponse();
944 } catch (RotelException e) {
948 // Check if command is a change of record source
950 recordSource = model.getRecordSourceFromCommand(cmd);
951 text = buildSourceLine1Response();
952 textLine2 = buildRecordResponse();
954 } catch (RotelException e) {
963 if (cmd != RotelCommand.RECORD_FONCTION_SELECT) {
964 selectingRecord = false;
970 if (model.getRespNbChars() == 42) {
971 while (textLine1Left.length() < 14) {
972 textLine1Left += " ";
974 while (textLine1Right.length() < 7) {
975 textLine1Right += " ";
977 while (textLine2.length() < 21) {
980 text = textLine1Left + textLine1Right + textLine2;
983 if (protocol == RotelProtocol.HEX) {
984 byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), model.getRespNbChars());
985 byte[] flags = new byte[model.getRespNbFlags()];
987 model.setMultiInput(flags, multiinput);
988 } catch (RotelException e) {
991 model.setZone2(flags, powers[2]);
992 } catch (RotelException e) {
995 model.setZone3(flags, powers[3]);
996 } catch (RotelException e) {
999 model.setZone4(flags, powers[4]);
1000 } catch (RotelException e) {
1002 int size = 6 + model.getRespNbChars() + model.getRespNbFlags();
1003 byte[] dataBuffer = new byte[size];
1005 dataBuffer[idx++] = START;
1006 dataBuffer[idx++] = (byte) (size - 4);
1007 dataBuffer[idx++] = model.getDeviceId();
1008 dataBuffer[idx++] = STANDARD_RESPONSE;
1009 if (model.isCharsBeforeFlags()) {
1010 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1011 idx += model.getRespNbChars();
1012 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1013 idx += model.getRespNbFlags();
1015 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1016 idx += model.getRespNbFlags();
1017 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1018 idx += model.getRespNbChars();
1020 byte checksum = RotelHexProtocolHandler.computeCheckSum(dataBuffer, idx - 1);
1021 if ((checksum & 0x000000FF) == 0x000000FD) {
1022 dataBuffer[idx++] = (byte) 0xFD;
1023 dataBuffer[idx++] = 0;
1024 } else if ((checksum & 0x000000FF) == 0x000000FE) {
1025 dataBuffer[idx++] = (byte) 0xFD;
1026 dataBuffer[idx++] = 1;
1028 dataBuffer[idx++] = checksum;
1030 synchronized (lock) {
1031 feedbackMsg = Arrays.copyOf(dataBuffer, idx);
1032 idxInFeedbackMsg = 0;
1035 String command = textAscii + (protocol == RotelProtocol.ASCII_V1 ? "!" : "$");
1036 synchronized (lock) {
1037 feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
1038 idxInFeedbackMsg = 0;
1043 private String buildAsciiResponse(String key, String value) {
1044 return String.format("%s=%s", key, value);
1047 private String buildAsciiResponse(String key, int value) {
1048 return String.format("%s=%d", key, value);
1051 private String buildAsciiResponse(String key, boolean value) {
1052 return buildAsciiResponse(key, buildOnOffValue(value));
1055 private String buildOnOffValue(boolean on) {
1056 return on ? MSG_VALUE_ON : MSG_VALUE_OFF;
1059 private String buildPowerAsciiResponse() {
1060 return buildAsciiResponse(KEY_POWER, powers[0] ? POWER_ON : STANDBY);
1063 private String buildVolumeAsciiResponse() {
1064 if (model.getNumberOfZones() > 1) {
1065 StringJoiner sj = new StringJoiner(",");
1066 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1067 sj.add(String.format("%02d", volumes[zone]));
1069 return buildAsciiResponse(KEY_VOLUME, sj.toString());
1071 return buildAsciiResponse(KEY_VOLUME, String.format("%02d", volumes[0]));
1075 private String buildMuteAsciiResponse() {
1076 if (model.getNumberOfZones() > 1) {
1077 StringJoiner sj = new StringJoiner(",");
1078 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1079 sj.add(buildOnOffValue(mutes[zone]));
1081 return buildAsciiResponse(KEY_MUTE, sj.toString());
1083 return buildAsciiResponse(KEY_MUTE, mutes[0]);
1087 private String buildBassAsciiResponse() {
1088 if (model.getNumberOfZones() > 1) {
1089 StringJoiner sj = new StringJoiner(",");
1090 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1091 sj.add(buildBassTrebleValue(basses[zone]));
1093 return buildAsciiResponse(KEY_BASS, sj.toString());
1095 return buildAsciiResponse(KEY_BASS, buildBassTrebleValue(basses[0]));
1099 private String buildTrebleAsciiResponse() {
1100 if (model.getNumberOfZones() > 1) {
1101 StringJoiner sj = new StringJoiner(",");
1102 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1103 sj.add(buildBassTrebleValue(trebles[zone]));
1105 return buildAsciiResponse(KEY_TREBLE, sj.toString());
1107 return buildAsciiResponse(KEY_TREBLE, buildBassTrebleValue(trebles[0]));
1111 private String buildBassTrebleValue(int value) {
1112 if (tcbypass || value == 0) {
1114 } else if (value > 0) {
1115 return String.format("+%02d", value);
1117 return String.format("-%02d", -value);
1121 private String buildBalanceAsciiResponse() {
1122 if (model.getNumberOfZones() > 1) {
1123 StringJoiner sj = new StringJoiner(",");
1124 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1125 sj.add(buildBalanceValue(balances[zone]));
1127 return buildAsciiResponse(KEY_BALANCE, sj.toString());
1129 return buildAsciiResponse(KEY_BALANCE, buildBalanceValue(balances[0]));
1133 private String buildBalanceValue(int value) {
1136 } else if (value > 0) {
1137 return String.format("r%02d", value);
1139 return String.format("l%02d", -value);
1143 private String buildSpeakerAsciiResponse() {
1145 if (speakerA && speakerB) {
1146 value = MSG_VALUE_SPEAKER_AB;
1147 } else if (speakerA && !speakerB) {
1148 value = MSG_VALUE_SPEAKER_A;
1149 } else if (!speakerA && speakerB) {
1150 value = MSG_VALUE_SPEAKER_B;
1152 value = MSG_VALUE_OFF;
1154 return buildAsciiResponse(KEY_SPEAKER, value);
1157 private String buildPlayStatusAsciiResponse() {
1159 switch (playStatus) {
1170 return buildAsciiResponse(protocol == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_STATUS, status);
1173 private String buildTrackAsciiResponse() {
1174 return buildAsciiResponse(KEY_TRACK, String.format("%03d", track));
1177 private String buildSourceAsciiResponse() {
1178 if (model.getNumberOfZones() > 1) {
1179 StringJoiner sj = new StringJoiner(",");
1180 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1181 sj.add(buildZoneSourceValue(sources[zone]));
1183 return buildAsciiResponse(KEY_INPUT, sj.toString());
1185 return buildAsciiResponse(KEY_SOURCE, buildSourceValue(sources[0]));
1189 private String buildSourceValue(RotelSource source) {
1191 RotelCommand command = source.getCommand();
1192 if (command != null) {
1193 str = protocol == RotelProtocol.ASCII_V1 ? command.getAsciiCommandV1() : command.getAsciiCommandV2();
1195 return str == null ? "" : str;
1198 private String buildZoneSourceValue(RotelSource source) {
1199 String str = buildSourceValue(source);
1200 int idx = str.indexOf("input_");
1201 return idx < 0 ? str : str.substring(idx + 6);
1204 private String buildDspAsciiResponse() {
1205 return buildAsciiResponse(KEY_DSP_MODE, dsp.getFeedback());
1208 private String buildSourceLine1Response() {
1212 } else if (mutes[0]) {
1215 text = getSourceLabel(sources[0], false) + " " + getSourceLabel(recordSource, true);
1220 private String buildSourceLine1LeftResponse() {
1225 text = getSourceLabel(sources[0], false);
1230 private String buildRecordResponse() {
1235 text = "REC " + getSourceLabel(recordSource, true);
1240 private String buildZonePowerResponse(int numZone) {
1243 zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1245 zone = String.format("ZONE%d", numZone);
1247 String state = powers[numZone] ? getSourceLabel(sources[numZone], true) : "OFF";
1248 return zone + " " + state;
1251 private String buildVolumeLine1Response() {
1253 if (volumes[0] == minVolume) {
1254 text = " VOLUME MIN ";
1255 } else if (volumes[0] == maxVolume) {
1256 text = " VOLUME MAX ";
1258 text = String.format(" VOLUME %02d ", volumes[0]);
1263 private String buildVolumeLine1RightResponse() {
1267 } else if (mutes[0]) {
1269 } else if (volumes[0] == minVolume) {
1271 } else if (volumes[0] == maxVolume) {
1274 text = String.format("VOL %02d", volumes[0]);
1279 private String buildZoneVolumeResponse(int numZone) {
1282 zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1284 zone = String.format("ZONE%d", numZone);
1287 if (mutes[numZone]) {
1288 text = zone + " MUTE ON";
1289 } else if (volumes[numZone] == minVolume) {
1290 text = zone + " VOL MIN";
1291 } else if (volumes[numZone] == maxVolume) {
1292 text = zone + " VOL MAX";
1294 text = String.format("%s VOL %02d", zone, volumes[numZone]);
1299 private String buildBassLine1Response() {
1301 if (basses[0] == minToneLevel) {
1302 text = " BASS MIN ";
1303 } else if (basses[0] == maxToneLevel) {
1304 text = " BASS MAX ";
1305 } else if (basses[0] == 0) {
1307 } else if (basses[0] > 0) {
1308 text = String.format(" BASS +%02d ", basses[0]);
1310 text = String.format(" BASS -%02d ", -basses[0]);
1315 private String buildBassLine1RightResponse() {
1317 if (basses[0] == minToneLevel) {
1319 } else if (basses[0] == maxToneLevel) {
1321 } else if (basses[0] == 0) {
1323 } else if (basses[0] > 0) {
1324 text = String.format("LF + %02d", basses[0]);
1326 text = String.format("LF - %02d", -basses[0]);
1331 private String buildTrebleLine1Response() {
1333 if (trebles[0] == minToneLevel) {
1334 text = " TREBLE MIN ";
1335 } else if (trebles[0] == maxToneLevel) {
1336 text = " TREBLE MAX ";
1337 } else if (trebles[0] == 0) {
1338 text = " TREBLE 0 ";
1339 } else if (trebles[0] > 0) {
1340 text = String.format(" TREBLE +%02d ", trebles[0]);
1342 text = String.format(" TREBLE -%02d ", -trebles[0]);
1347 private String buildTrebleLine1RightResponse() {
1349 if (trebles[0] == minToneLevel) {
1351 } else if (trebles[0] == maxToneLevel) {
1353 } else if (trebles[0] == 0) {
1355 } else if (trebles[0] > 0) {
1356 text = String.format("HF + %02d", trebles[0]);
1358 text = String.format("HF - %02d", -trebles[0]);
1363 private String getSourceLabel(RotelSource source, boolean considerFollowMain) {
1365 if (considerFollowMain && source.getName().equals(RotelSource.CAT1_FOLLOW_MAIN.getName())) {
1368 label = Objects.requireNonNullElse(sourcesLabels.get(source), source.getLabel());