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 boolean bypass = false;
65 private int[] volumes = { 50, 10, 20, 30, 40 };
66 private boolean[] mutes = { false, false, false, false, false };
67 private boolean tcbypass;
68 private int[] basses = { 0, 0, 0, 0, 0 };
69 private int[] trebles = { 0, 0, 0, 0, 0 };
70 private int[] balances = { 0, 0, 0, 0, 0 };
71 private boolean showTreble;
72 private boolean speakerA = true;
73 private boolean speakerB = false;
74 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
75 private int track = 1;
76 private boolean randomMode;
77 private RotelRepeatMode repeatMode = RotelRepeatMode.OFF;
78 private boolean selectingRecord;
82 private int minVolume;
83 private int maxVolume;
84 private int minToneLevel;
85 private int maxToneLevel;
86 private int minBalance;
87 private int maxBalance;
92 * @param model the projector model in use
93 * @param protocolHandler the protocol handler
94 * @param sourcesLabels the custom labels for sources
95 * @param readerThreadName the name of thread to be created
97 public RotelSimuConnector(RotelModel model, RotelAbstractProtocolHandler protocolHandler,
98 Map<RotelSource, String> sourcesLabels, String readerThreadName) {
99 super(protocolHandler, true, readerThreadName);
101 this.protocol = protocolHandler.getProtocol();
102 this.sourcesLabels = sourcesLabels;
104 this.maxVolume = model.hasVolumeControl() ? model.getVolumeMax() : 0;
105 this.maxToneLevel = model.hasToneControl() ? model.getToneLevelMax() : 0;
106 this.minToneLevel = -this.maxToneLevel;
107 this.maxBalance = model.hasBalanceControl() ? model.getBalanceLevelMax() : 0;
108 this.minBalance = -this.maxBalance;
109 List<RotelSource> modelSources = model.getSources();
110 RotelSource source = modelSources.isEmpty() ? RotelSource.CAT0_CD : modelSources.get(0);
111 sources = new RotelSource[] { source, source, source, source, source };
112 recordSource = source;
116 public synchronized void open() throws RotelException {
117 logger.debug("Opening simulated connection");
118 readerThread.start();
120 logger.debug("Simulated connection opened");
124 public synchronized void close() {
125 logger.debug("Closing simulated connection");
128 logger.debug("Simulated connection closed");
132 protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
133 synchronized (lock) {
134 int len = feedbackMsg.length - idxInFeedbackMsg;
136 if (len > dataBuffer.length) {
137 len = dataBuffer.length;
139 System.arraycopy(feedbackMsg, idxInFeedbackMsg, dataBuffer, 0, len);
140 idxInFeedbackMsg += len;
144 // Give more chance to someone else than the reader thread to get the lock
147 } catch (InterruptedException e) {
148 Thread.currentThread().interrupt();
154 * Built the simulated feedback message for a sent command
156 * @param cmd the sent command
157 * @param value the integer value considered in the sent command for volume, bass or treble adjustment
159 public void buildFeedbackMessage(RotelCommand cmd, @Nullable Integer value) {
160 String text = buildSourceLine1Response();
161 String textLine1Left = buildSourceLine1LeftResponse();
162 String textLine1Right = buildVolumeLine1RightResponse();
163 String textLine2 = "";
164 String textAscii = "";
165 boolean accepted = true;
166 boolean resetZone = true;
169 case ZONE1_VOLUME_UP:
170 case ZONE1_VOLUME_DOWN:
171 case ZONE1_VOLUME_SET:
172 case ZONE1_MUTE_TOGGLE:
176 case ZONE1_BASS_DOWN:
178 case ZONE1_TREBLE_UP:
179 case ZONE1_TREBLE_DOWN:
180 case ZONE1_TREBLE_SET:
181 case ZONE1_BALANCE_LEFT:
182 case ZONE1_BALANCE_RIGHT:
183 case ZONE1_BALANCE_SET:
186 case ZONE2_POWER_OFF:
188 case ZONE2_VOLUME_UP:
189 case ZONE2_VOLUME_DOWN:
190 case ZONE2_VOLUME_SET:
191 case ZONE2_MUTE_TOGGLE:
195 case ZONE2_BASS_DOWN:
197 case ZONE2_TREBLE_UP:
198 case ZONE2_TREBLE_DOWN:
199 case ZONE2_TREBLE_SET:
200 case ZONE2_BALANCE_LEFT:
201 case ZONE2_BALANCE_RIGHT:
202 case ZONE2_BALANCE_SET:
205 case ZONE3_POWER_OFF:
207 case ZONE3_VOLUME_UP:
208 case ZONE3_VOLUME_DOWN:
209 case ZONE3_VOLUME_SET:
210 case ZONE3_MUTE_TOGGLE:
214 case ZONE3_BASS_DOWN:
216 case ZONE3_TREBLE_UP:
217 case ZONE3_TREBLE_DOWN:
218 case ZONE3_TREBLE_SET:
219 case ZONE3_BALANCE_LEFT:
220 case ZONE3_BALANCE_RIGHT:
221 case ZONE3_BALANCE_SET:
224 case ZONE4_POWER_OFF:
226 case ZONE4_VOLUME_UP:
227 case ZONE4_VOLUME_DOWN:
228 case ZONE4_VOLUME_SET:
229 case ZONE4_MUTE_TOGGLE:
233 case ZONE4_BASS_DOWN:
235 case ZONE4_TREBLE_UP:
236 case ZONE4_TREBLE_DOWN:
237 case ZONE4_TREBLE_SET:
238 case ZONE4_BALANCE_LEFT:
239 case ZONE4_BALANCE_RIGHT:
240 case ZONE4_BALANCE_SET:
247 case DISPLAY_REFRESH:
250 case MAIN_ZONE_POWER_OFF:
252 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
253 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
254 powers[zone] = false;
257 text = buildSourceLine1Response();
258 textLine1Left = buildSourceLine1LeftResponse();
259 textLine1Right = buildVolumeLine1RightResponse();
260 textAscii = buildPowerAsciiResponse();
263 case MAIN_ZONE_POWER_ON:
265 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
266 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
270 text = buildSourceLine1Response();
271 textLine1Left = buildSourceLine1LeftResponse();
272 textLine1Right = buildVolumeLine1RightResponse();
273 textAscii = buildPowerAsciiResponse();
276 textAscii = buildPowerAsciiResponse();
278 case ZONE2_POWER_OFF:
279 case ZONE3_POWER_OFF:
280 case ZONE4_POWER_OFF:
281 powers[numZone] = false;
282 text = textLine2 = buildZonePowerResponse(numZone);
289 powers[numZone] = true;
290 text = textLine2 = buildZonePowerResponse(numZone);
294 case RECORD_FONCTION_SELECT:
295 if (model.getNumberOfZones() > 1 && model.getZoneSelectCmd() == cmd) {
297 if (showZone >= model.getNumberOfZones()) {
307 selectingRecord = powers[0];
309 textLine2 = buildRecordResponse();
310 } else if (showZone >= 2 && showZone <= 4) {
311 selectingRecord = false;
312 text = textLine2 = buildZonePowerResponse(showZone);
317 if (model.getNumberOfZones() == 1 || (model.getNumberOfZones() > 2 && model.getZoneSelectCmd() == cmd)
318 || (showZone == 1 && model.getZoneSelectCmd() != cmd)) {
321 if (model.getZoneSelectCmd() == cmd) {
322 if (!powers[0] && !powers[2]) {
325 } else if (showZone == 2) {
326 powers[2] = !powers[2];
330 } else if (showZone >= 2 && showZone <= 4) {
331 powers[showZone] = !powers[showZone];
333 if (showZone >= 2 && showZone <= 4) {
334 text = textLine2 = buildZonePowerResponse(showZone);
343 if (!accepted && numZone > 0 && powers[numZone]) {
346 case ZONE1_VOLUME_UP:
347 case ZONE2_VOLUME_UP:
348 case ZONE3_VOLUME_UP:
349 case ZONE4_VOLUME_UP:
350 if (volumes[numZone] < maxVolume) {
353 text = textLine2 = buildZoneVolumeResponse(numZone);
354 textAscii = buildVolumeAsciiResponse();
356 case ZONE1_VOLUME_DOWN:
357 case ZONE2_VOLUME_DOWN:
358 case ZONE3_VOLUME_DOWN:
359 case ZONE4_VOLUME_DOWN:
360 if (volumes[numZone] > minVolume) {
363 text = textLine2 = buildZoneVolumeResponse(numZone);
364 textAscii = buildVolumeAsciiResponse();
366 case ZONE1_VOLUME_SET:
367 case ZONE2_VOLUME_SET:
368 case ZONE3_VOLUME_SET:
369 case ZONE4_VOLUME_SET:
371 volumes[numZone] = value;
373 text = textLine2 = buildZoneVolumeResponse(numZone);
374 textAscii = buildVolumeAsciiResponse();
376 case ZONE1_MUTE_TOGGLE:
377 case ZONE2_MUTE_TOGGLE:
378 case ZONE3_MUTE_TOGGLE:
379 case ZONE4_MUTE_TOGGLE:
380 mutes[numZone] = !mutes[numZone];
381 text = textLine2 = buildZoneVolumeResponse(numZone);
382 textAscii = buildMuteAsciiResponse();
388 mutes[numZone] = true;
389 text = textLine2 = buildZoneVolumeResponse(numZone);
390 textAscii = buildMuteAsciiResponse();
396 mutes[numZone] = false;
397 text = textLine2 = buildZoneVolumeResponse(numZone);
398 textAscii = buildMuteAsciiResponse();
404 if (!tcbypass && basses[numZone] < maxToneLevel) {
405 basses[numZone] += STEP_TONE_LEVEL;
407 textAscii = buildBassAsciiResponse();
409 case ZONE1_BASS_DOWN:
410 case ZONE2_BASS_DOWN:
411 case ZONE3_BASS_DOWN:
412 case ZONE4_BASS_DOWN:
413 if (!tcbypass && basses[numZone] > minToneLevel) {
414 basses[numZone] -= STEP_TONE_LEVEL;
416 textAscii = buildBassAsciiResponse();
422 if (!tcbypass && value != null) {
423 basses[numZone] = value;
425 textAscii = buildBassAsciiResponse();
427 case ZONE1_TREBLE_UP:
428 case ZONE2_TREBLE_UP:
429 case ZONE3_TREBLE_UP:
430 case ZONE4_TREBLE_UP:
431 if (!tcbypass && trebles[numZone] < maxToneLevel) {
432 trebles[numZone] += STEP_TONE_LEVEL;
434 textAscii = buildTrebleAsciiResponse();
436 case ZONE1_TREBLE_DOWN:
437 case ZONE2_TREBLE_DOWN:
438 case ZONE3_TREBLE_DOWN:
439 case ZONE4_TREBLE_DOWN:
440 if (!tcbypass && trebles[numZone] > minToneLevel) {
441 trebles[numZone] -= STEP_TONE_LEVEL;
443 textAscii = buildTrebleAsciiResponse();
445 case ZONE1_TREBLE_SET:
446 case ZONE2_TREBLE_SET:
447 case ZONE3_TREBLE_SET:
448 case ZONE4_TREBLE_SET:
449 if (!tcbypass && value != null) {
450 trebles[numZone] = value;
452 textAscii = buildTrebleAsciiResponse();
454 case ZONE1_BALANCE_LEFT:
455 case ZONE2_BALANCE_LEFT:
456 case ZONE3_BALANCE_LEFT:
457 case ZONE4_BALANCE_LEFT:
458 if (balances[numZone] > minBalance) {
461 textAscii = buildBalanceAsciiResponse();
463 case ZONE1_BALANCE_RIGHT:
464 case ZONE2_BALANCE_RIGHT:
465 case ZONE3_BALANCE_RIGHT:
466 case ZONE4_BALANCE_RIGHT:
467 if (balances[numZone] < maxBalance) {
470 textAscii = buildBalanceAsciiResponse();
472 case ZONE1_BALANCE_SET:
473 case ZONE2_BALANCE_SET:
474 case ZONE3_BALANCE_SET:
475 case ZONE4_BALANCE_SET:
477 balances[numZone] = value;
479 textAscii = buildBalanceAsciiResponse();
487 // Check if command is a change of source input for a zone
488 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
491 sources[zone] = model.getZoneSourceFromCommand(cmd, zone);
492 text = textLine2 = buildZonePowerResponse(zone);
493 textAscii = buildSourceAsciiResponse();
499 } catch (RotelException e) {
504 if (!accepted && powers[2] && !model.hasZoneCommands(2) && model.getNumberOfZones() > 1 && showZone == 2) {
508 if (volumes[2] < maxVolume) {
511 text = textLine2 = buildZoneVolumeResponse(2);
515 if (volumes[2] > minVolume) {
518 text = textLine2 = buildZoneVolumeResponse(2);
525 text = textLine2 = buildZoneVolumeResponse(2);
534 sources[2] = model.getSourceFromCommand(cmd);
535 text = textLine2 = buildZonePowerResponse(2);
539 } catch (RotelException e) {
543 if (!accepted && powers[0]) {
547 textAscii = buildAsciiResponse(
548 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, AUTO);
551 textAscii = buildAsciiResponse(
552 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, MANUAL);
555 textAscii = buildAsciiResponse(KEY_VOLUME_MIN, minVolume);
558 textAscii = buildAsciiResponse(KEY_VOLUME_MAX, maxVolume);
561 case MAIN_ZONE_VOLUME_UP:
562 if (volumes[0] < maxVolume) {
565 text = buildVolumeLine1Response();
566 textLine1Right = buildVolumeLine1RightResponse();
567 textAscii = buildVolumeAsciiResponse();
570 case MAIN_ZONE_VOLUME_DOWN:
571 if (volumes[0] > minVolume) {
574 text = buildVolumeLine1Response();
575 textLine1Right = buildVolumeLine1RightResponse();
576 textAscii = buildVolumeAsciiResponse();
582 text = buildVolumeLine1Response();
583 textLine1Right = buildVolumeLine1RightResponse();
584 textAscii = buildVolumeAsciiResponse();
587 textAscii = buildVolumeAsciiResponse();
590 case MAIN_ZONE_MUTE_TOGGLE:
591 mutes[0] = !mutes[0];
592 text = buildSourceLine1Response();
593 textLine1Right = buildVolumeLine1RightResponse();
594 textAscii = buildMuteAsciiResponse();
597 case MAIN_ZONE_MUTE_ON:
599 text = buildSourceLine1Response();
600 textLine1Right = buildVolumeLine1RightResponse();
601 textAscii = buildMuteAsciiResponse();
604 case MAIN_ZONE_MUTE_OFF:
606 text = buildSourceLine1Response();
607 textLine1Right = buildVolumeLine1RightResponse();
608 textAscii = buildMuteAsciiResponse();
611 textAscii = buildMuteAsciiResponse();
614 textAscii = buildAsciiResponse(KEY_TONE_MAX, String.format("%02d", maxToneLevel));
616 case TONE_CONTROLS_ON:
618 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
620 case TONE_CONTROLS_OFF:
622 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
625 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
629 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
633 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
636 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
639 if (!tcbypass && basses[0] < maxToneLevel) {
640 basses[0] += STEP_TONE_LEVEL;
642 text = buildBassLine1Response();
643 textLine1Right = buildBassLine1RightResponse();
644 textAscii = buildBassAsciiResponse();
647 if (!tcbypass && basses[0] > minToneLevel) {
648 basses[0] -= STEP_TONE_LEVEL;
650 text = buildBassLine1Response();
651 textLine1Right = buildBassLine1RightResponse();
652 textAscii = buildBassAsciiResponse();
655 if (!tcbypass && value != null) {
658 text = buildBassLine1Response();
659 textLine1Right = buildBassLine1RightResponse();
660 textAscii = buildBassAsciiResponse();
663 textAscii = buildBassAsciiResponse();
666 if (!tcbypass && trebles[0] < maxToneLevel) {
667 trebles[0] += STEP_TONE_LEVEL;
669 text = buildTrebleLine1Response();
670 textLine1Right = buildTrebleLine1RightResponse();
671 textAscii = buildTrebleAsciiResponse();
674 if (!tcbypass && trebles[0] > minToneLevel) {
675 trebles[0] -= STEP_TONE_LEVEL;
677 text = buildTrebleLine1Response();
678 textLine1Right = buildTrebleLine1RightResponse();
679 textAscii = buildTrebleAsciiResponse();
682 if (!tcbypass && value != null) {
685 text = buildTrebleLine1Response();
686 textLine1Right = buildTrebleLine1RightResponse();
687 textAscii = buildTrebleAsciiResponse();
690 textAscii = buildTrebleAsciiResponse();
692 case TONE_CONTROL_SELECT:
693 showTreble = !showTreble;
695 text = buildTrebleLine1Response();
696 textLine1Right = buildTrebleLine1RightResponse();
698 text = buildBassLine1Response();
699 textLine1Right = buildBassLine1RightResponse();
703 if (balances[0] > minBalance) {
706 textAscii = buildBalanceAsciiResponse();
709 if (balances[0] < maxBalance) {
712 textAscii = buildBalanceAsciiResponse();
718 textAscii = buildBalanceAsciiResponse();
721 textAscii = buildBalanceAsciiResponse();
723 case SPEAKER_A_TOGGLE:
724 speakerA = !speakerA;
725 textAscii = buildSpeakerAsciiResponse();
729 textAscii = buildSpeakerAsciiResponse();
733 textAscii = buildSpeakerAsciiResponse();
735 case SPEAKER_B_TOGGLE:
736 speakerB = !speakerB;
737 textAscii = buildSpeakerAsciiResponse();
741 textAscii = buildSpeakerAsciiResponse();
745 textAscii = buildSpeakerAsciiResponse();
748 textAscii = buildSpeakerAsciiResponse();
751 playStatus = RotelPlayStatus.PLAYING;
752 textAscii = buildPlayStatusAsciiResponse();
755 playStatus = RotelPlayStatus.STOPPED;
756 textAscii = buildPlayStatusAsciiResponse();
759 switch (playStatus) {
761 playStatus = RotelPlayStatus.PAUSED;
765 playStatus = RotelPlayStatus.PLAYING;
768 textAscii = buildPlayStatusAsciiResponse();
772 textAscii = buildPlayStatusAsciiResponse();
776 textAscii = buildTrackAsciiResponse();
782 textAscii = buildTrackAsciiResponse();
785 textAscii = buildTrackAsciiResponse();
788 randomMode = !randomMode;
789 textAscii = buildRandomModeAsciiResponse();
792 textAscii = buildRandomModeAsciiResponse();
795 switch (repeatMode) {
797 repeatMode = RotelRepeatMode.DISC;
800 repeatMode = RotelRepeatMode.OFF;
803 repeatMode = RotelRepeatMode.TRACK;
806 textAscii = buildRepeatModeAsciiResponse();
809 textAscii = buildRepeatModeAsciiResponse();
811 case SOURCE_MULTI_INPUT:
812 multiinput = !multiinput;
813 text = "MULTI IN " + (multiinput ? "ON" : "OFF");
815 sources[0] = model.getSourceFromCommand(cmd);
816 textLine1Left = buildSourceLine1LeftResponse();
817 textAscii = buildSourceAsciiResponse();
819 } catch (RotelException e) {
824 textAscii = buildSourceAsciiResponse();
827 dsp = RotelDsp.CAT4_NONE;
828 textLine2 = bypass ? "BYPASS" : "STEREO";
829 textAscii = buildDspAsciiResponse();
832 dsp = RotelDsp.CAT4_STEREO3;
833 textLine2 = "DOLBY 3 STEREO";
834 textAscii = buildDspAsciiResponse();
837 dsp = RotelDsp.CAT4_STEREO5;
838 textLine2 = "5CH STEREO";
839 textAscii = buildDspAsciiResponse();
842 dsp = RotelDsp.CAT4_STEREO7;
843 textLine2 = "7CH STEREO";
844 textAscii = buildDspAsciiResponse();
847 dsp = RotelDsp.CAT5_STEREO9;
848 textAscii = buildDspAsciiResponse();
851 dsp = RotelDsp.CAT5_STEREO11;
852 textAscii = buildDspAsciiResponse();
855 dsp = RotelDsp.CAT4_DSP1;
857 textAscii = buildDspAsciiResponse();
860 dsp = RotelDsp.CAT4_DSP2;
862 textAscii = buildDspAsciiResponse();
865 dsp = RotelDsp.CAT4_DSP3;
867 textAscii = buildDspAsciiResponse();
870 dsp = RotelDsp.CAT4_DSP4;
872 textAscii = buildDspAsciiResponse();
875 dsp = RotelDsp.CAT4_PROLOGIC;
876 textLine2 = "DOLBY PRO LOGIC";
877 textAscii = buildDspAsciiResponse();
880 dsp = RotelDsp.CAT4_PLII_CINEMA;
881 textLine2 = "DOLBY PL C";
882 textAscii = buildDspAsciiResponse();
885 dsp = RotelDsp.CAT4_PLII_MUSIC;
886 textLine2 = "DOLBY PL M";
887 textAscii = buildDspAsciiResponse();
890 dsp = RotelDsp.CAT4_PLII_GAME;
891 textLine2 = "DOLBY PL G";
892 textAscii = buildDspAsciiResponse();
895 dsp = RotelDsp.CAT4_PLIIZ;
896 textLine2 = "DOLBY PL z";
897 textAscii = buildDspAsciiResponse();
900 dsp = RotelDsp.CAT4_NEO6_MUSIC;
901 textLine2 = "DTS Neo:6 M";
902 textAscii = buildDspAsciiResponse();
905 dsp = RotelDsp.CAT4_NEO6_CINEMA;
906 textLine2 = "DTS Neo:6 C";
907 textAscii = buildDspAsciiResponse();
910 dsp = RotelDsp.CAT5_ATMOS;
911 textAscii = buildDspAsciiResponse();
914 dsp = RotelDsp.CAT5_NEURAL_X;
915 textAscii = buildDspAsciiResponse();
918 dsp = RotelDsp.CAT5_BYPASS;
919 textAscii = buildDspAsciiResponse();
922 textAscii = buildDspAsciiResponse();
924 case STEREO_BYPASS_TOGGLE:
926 textLine2 = bypass ? "BYPASS" : "STEREO";
929 textAscii = model.getNumberOfZones() > 1 ? buildAsciiResponse(KEY_FREQ, "44.1,48,none,176.4")
930 : buildAsciiResponse(KEY_FREQ, "44.1");
932 case DIMMER_LEVEL_SET:
936 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
938 case DIMMER_LEVEL_GET:
939 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
942 textAscii = buildAsciiResponse(KEY_MODEL, model.getName());
945 textAscii = buildAsciiResponse(KEY_VERSION, "1.00");
952 // Check if command is a change of source input for the main zone
954 sources[0] = model.getZoneSourceFromCommand(cmd, 1);
955 text = buildSourceLine1Response();
956 textLine1Left = buildSourceLine1LeftResponse();
957 textAscii = buildSourceAsciiResponse();
959 } catch (RotelException e) {
963 // Check if command is a change of source input
965 if (selectingRecord && !model.hasOtherThanPrimaryCommands()) {
966 recordSource = model.getSourceFromCommand(cmd);
968 sources[0] = model.getSourceFromCommand(cmd);
970 text = buildSourceLine1Response();
971 textLine1Left = buildSourceLine1LeftResponse();
972 textAscii = buildSourceAsciiResponse();
975 } catch (RotelException e) {
979 // Check if command is a change of record source
981 recordSource = model.getRecordSourceFromCommand(cmd);
982 text = buildSourceLine1Response();
983 textLine2 = buildRecordResponse();
985 } catch (RotelException e) {
994 if (cmd != RotelCommand.RECORD_FONCTION_SELECT) {
995 selectingRecord = false;
1001 if (model.getRespNbChars() == 42) {
1002 while (textLine1Left.length() < 14) {
1003 textLine1Left += " ";
1005 while (textLine1Right.length() < 7) {
1006 textLine1Right += " ";
1008 while (textLine2.length() < 21) {
1011 text = textLine1Left + textLine1Right + textLine2;
1014 if (protocol == RotelProtocol.HEX) {
1015 byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), model.getRespNbChars());
1016 byte[] flags = new byte[model.getRespNbFlags()];
1018 model.setMultiInput(flags, multiinput);
1019 } catch (RotelException e) {
1022 model.setZone2(flags, powers[2]);
1023 } catch (RotelException e) {
1026 model.setZone3(flags, powers[3]);
1027 } catch (RotelException e) {
1030 model.setZone4(flags, powers[4]);
1031 } catch (RotelException e) {
1033 int size = 6 + model.getRespNbChars() + model.getRespNbFlags();
1034 byte[] dataBuffer = new byte[size];
1036 dataBuffer[idx++] = START;
1037 dataBuffer[idx++] = (byte) (size - 4);
1038 dataBuffer[idx++] = model.getDeviceId();
1039 dataBuffer[idx++] = STANDARD_RESPONSE;
1040 if (model.isCharsBeforeFlags()) {
1041 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1042 idx += model.getRespNbChars();
1043 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1044 idx += model.getRespNbFlags();
1046 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1047 idx += model.getRespNbFlags();
1048 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1049 idx += model.getRespNbChars();
1051 byte checksum = RotelHexProtocolHandler.computeCheckSum(dataBuffer, idx - 1);
1052 if ((checksum & 0x000000FF) == 0x000000FD) {
1053 dataBuffer[idx++] = (byte) 0xFD;
1054 dataBuffer[idx++] = 0;
1055 } else if ((checksum & 0x000000FF) == 0x000000FE) {
1056 dataBuffer[idx++] = (byte) 0xFD;
1057 dataBuffer[idx++] = 1;
1059 dataBuffer[idx++] = checksum;
1061 synchronized (lock) {
1062 feedbackMsg = Arrays.copyOf(dataBuffer, idx);
1063 idxInFeedbackMsg = 0;
1066 String command = textAscii + (protocol == RotelProtocol.ASCII_V1 ? "!" : "$");
1067 synchronized (lock) {
1068 feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
1069 idxInFeedbackMsg = 0;
1074 private String buildAsciiResponse(String key, String value) {
1075 return String.format("%s=%s", key, value);
1078 private String buildAsciiResponse(String key, int value) {
1079 return String.format("%s=%d", key, value);
1082 private String buildAsciiResponse(String key, boolean value) {
1083 return buildAsciiResponse(key, buildOnOffValue(value));
1086 private String buildOnOffValue(boolean on) {
1087 return on ? MSG_VALUE_ON : MSG_VALUE_OFF;
1090 private String buildPowerAsciiResponse() {
1091 return buildAsciiResponse(KEY_POWER, powers[0] ? POWER_ON : STANDBY);
1094 private String buildVolumeAsciiResponse() {
1095 if (model.getNumberOfZones() > 1) {
1096 StringJoiner sj = new StringJoiner(",");
1097 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1098 sj.add(String.format("%02d", volumes[zone]));
1100 return buildAsciiResponse(KEY_VOLUME, sj.toString());
1102 return buildAsciiResponse(KEY_VOLUME, String.format("%02d", volumes[0]));
1106 private String buildMuteAsciiResponse() {
1107 if (model.getNumberOfZones() > 1) {
1108 StringJoiner sj = new StringJoiner(",");
1109 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1110 sj.add(buildOnOffValue(mutes[zone]));
1112 return buildAsciiResponse(KEY_MUTE, sj.toString());
1114 return buildAsciiResponse(KEY_MUTE, mutes[0]);
1118 private String buildBassAsciiResponse() {
1119 if (model.getNumberOfZones() > 1) {
1120 StringJoiner sj = new StringJoiner(",");
1121 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1122 sj.add(buildBassTrebleValue(basses[zone]));
1124 return buildAsciiResponse(KEY_BASS, sj.toString());
1126 return buildAsciiResponse(KEY_BASS, buildBassTrebleValue(basses[0]));
1130 private String buildTrebleAsciiResponse() {
1131 if (model.getNumberOfZones() > 1) {
1132 StringJoiner sj = new StringJoiner(",");
1133 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1134 sj.add(buildBassTrebleValue(trebles[zone]));
1136 return buildAsciiResponse(KEY_TREBLE, sj.toString());
1138 return buildAsciiResponse(KEY_TREBLE, buildBassTrebleValue(trebles[0]));
1142 private String buildBassTrebleValue(int value) {
1143 if (tcbypass || value == 0) {
1145 } else if (value > 0) {
1146 return String.format("+%02d", value);
1148 return String.format("-%02d", -value);
1152 private String buildBalanceAsciiResponse() {
1153 if (model.getNumberOfZones() > 1) {
1154 StringJoiner sj = new StringJoiner(",");
1155 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1156 sj.add(buildBalanceValue(balances[zone]));
1158 return buildAsciiResponse(KEY_BALANCE, sj.toString());
1160 return buildAsciiResponse(KEY_BALANCE, buildBalanceValue(balances[0]));
1164 private String buildBalanceValue(int value) {
1167 } else if (value > 0) {
1168 return String.format("r%02d", value);
1170 return String.format("l%02d", -value);
1174 private String buildSpeakerAsciiResponse() {
1176 if (speakerA && speakerB) {
1177 value = MSG_VALUE_SPEAKER_AB;
1178 } else if (speakerA && !speakerB) {
1179 value = MSG_VALUE_SPEAKER_A;
1180 } else if (!speakerA && speakerB) {
1181 value = MSG_VALUE_SPEAKER_B;
1183 value = MSG_VALUE_OFF;
1185 return buildAsciiResponse(KEY_SPEAKER, value);
1188 private String buildPlayStatusAsciiResponse() {
1190 switch (playStatus) {
1201 return buildAsciiResponse(protocol == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_STATUS, status);
1204 private String buildTrackAsciiResponse() {
1205 return buildAsciiResponse(KEY_TRACK, String.format("%03d", track));
1208 private String buildRandomModeAsciiResponse() {
1209 return buildAsciiResponse(KEY_RANDOM, randomMode);
1212 private String buildRepeatModeAsciiResponse() {
1214 switch (repeatMode) {
1222 mode = MSG_VALUE_OFF;
1225 return buildAsciiResponse(KEY_REPEAT, mode);
1228 private String buildSourceAsciiResponse() {
1229 if (model.getNumberOfZones() > 1) {
1230 StringJoiner sj = new StringJoiner(",");
1231 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1232 sj.add(buildZoneSourceValue(sources[zone]));
1234 return buildAsciiResponse(KEY_INPUT, sj.toString());
1236 return buildAsciiResponse(KEY_SOURCE, buildSourceValue(sources[0]));
1240 private String buildSourceValue(RotelSource source) {
1242 RotelCommand command = source.getCommand();
1243 if (command != null) {
1244 str = protocol == RotelProtocol.ASCII_V1 ? command.getAsciiCommandV1() : command.getAsciiCommandV2();
1246 return str == null ? "" : str;
1249 private String buildZoneSourceValue(RotelSource source) {
1250 String str = buildSourceValue(source);
1251 int idx = str.indexOf("input_");
1252 return idx < 0 ? str : str.substring(idx + 6);
1255 private String buildDspAsciiResponse() {
1256 return buildAsciiResponse(KEY_DSP_MODE, dsp.getFeedback());
1259 private String buildSourceLine1Response() {
1263 } else if (mutes[0]) {
1266 text = getSourceLabel(sources[0], false) + " " + getSourceLabel(recordSource, true);
1271 private String buildSourceLine1LeftResponse() {
1276 text = getSourceLabel(sources[0], false);
1281 private String buildRecordResponse() {
1286 text = "REC " + getSourceLabel(recordSource, true);
1291 private String buildZonePowerResponse(int numZone) {
1294 zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1296 zone = String.format("ZONE%d", numZone);
1298 String state = powers[numZone] ? getSourceLabel(sources[numZone], true) : "OFF";
1299 return zone + " " + state;
1302 private String buildVolumeLine1Response() {
1304 if (volumes[0] == minVolume) {
1305 text = " VOLUME MIN ";
1306 } else if (volumes[0] == maxVolume) {
1307 text = " VOLUME MAX ";
1309 text = String.format(" VOLUME %02d ", volumes[0]);
1314 private String buildVolumeLine1RightResponse() {
1318 } else if (mutes[0]) {
1320 } else if (volumes[0] == minVolume) {
1322 } else if (volumes[0] == maxVolume) {
1325 text = String.format("VOL %02d", volumes[0]);
1330 private String buildZoneVolumeResponse(int numZone) {
1333 zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1335 zone = String.format("ZONE%d", numZone);
1338 if (mutes[numZone]) {
1339 text = zone + " MUTE ON";
1340 } else if (volumes[numZone] == minVolume) {
1341 text = zone + " VOL MIN";
1342 } else if (volumes[numZone] == maxVolume) {
1343 text = zone + " VOL MAX";
1345 text = String.format("%s VOL %02d", zone, volumes[numZone]);
1350 private String buildBassLine1Response() {
1352 if (basses[0] == minToneLevel) {
1353 text = " BASS MIN ";
1354 } else if (basses[0] == maxToneLevel) {
1355 text = " BASS MAX ";
1356 } else if (basses[0] == 0) {
1358 } else if (basses[0] > 0) {
1359 text = String.format(" BASS +%02d ", basses[0]);
1361 text = String.format(" BASS -%02d ", -basses[0]);
1366 private String buildBassLine1RightResponse() {
1368 if (basses[0] == minToneLevel) {
1370 } else if (basses[0] == maxToneLevel) {
1372 } else if (basses[0] == 0) {
1374 } else if (basses[0] > 0) {
1375 text = String.format("LF + %02d", basses[0]);
1377 text = String.format("LF - %02d", -basses[0]);
1382 private String buildTrebleLine1Response() {
1384 if (trebles[0] == minToneLevel) {
1385 text = " TREBLE MIN ";
1386 } else if (trebles[0] == maxToneLevel) {
1387 text = " TREBLE MAX ";
1388 } else if (trebles[0] == 0) {
1389 text = " TREBLE 0 ";
1390 } else if (trebles[0] > 0) {
1391 text = String.format(" TREBLE +%02d ", trebles[0]);
1393 text = String.format(" TREBLE -%02d ", -trebles[0]);
1398 private String buildTrebleLine1RightResponse() {
1400 if (trebles[0] == minToneLevel) {
1402 } else if (trebles[0] == maxToneLevel) {
1404 } else if (trebles[0] == 0) {
1406 } else if (trebles[0] > 0) {
1407 text = String.format("HF + %02d", trebles[0]);
1409 text = String.format("HF - %02d", -trebles[0]);
1414 private String getSourceLabel(RotelSource source, boolean considerFollowMain) {
1416 if (considerFollowMain && source.getName().equals(RotelSource.CAT1_FOLLOW_MAIN.getName())) {
1419 label = Objects.requireNonNullElse(sourcesLabels.get(source), source.getLabel());