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;
48 private static final String FIRMWARE = "V1.1.8";
50 private final Logger logger = LoggerFactory.getLogger(RotelSimuConnector.class);
52 private final RotelModel model;
53 private final RotelProtocol protocol;
54 private final Map<RotelSource, String> sourcesLabels;
56 private Object lock = new Object();
58 private byte[] feedbackMsg = new byte[1];
59 private int idxInFeedbackMsg = feedbackMsg.length;
61 private boolean[] powers = { false, false, false, false, false };
62 private String powerMode = POWER_NORMAL;
63 private RotelSource[] sources;
64 private RotelSource recordSource;
65 private boolean multiinput;
66 private RotelDsp dsp = RotelDsp.CAT4_NONE;
67 private boolean bypass = false;
68 private int[] volumes = { 50, 10, 20, 30, 40 };
69 private boolean[] mutes = { false, false, false, false, false };
70 private boolean tcbypass;
71 private int[] basses = { 0, 0, 0, 0, 0 };
72 private int[] trebles = { 0, 0, 0, 0, 0 };
73 private int[] balances = { 0, 0, 0, 0, 0 };
74 private boolean showTreble;
75 private boolean speakerA = true;
76 private boolean speakerB = false;
77 private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
78 private int track = 1;
79 private boolean randomMode;
80 private RotelRepeatMode repeatMode = RotelRepeatMode.OFF;
81 private boolean selectingRecord;
84 private int pcUsbClass = 1;
85 private double subLevel;
86 private double centerLevel;
87 private double surroundRightLevel;
88 private double surroundLefLevel;
89 private double centerBackRightLevel;
90 private double centerBackLefLevel;
91 private double ceilingFrontRightLevel;
92 private double ceilingFrontLefLevel;
93 private double ceilingRearRightLevel;
94 private double ceilingRearLefLevel;
96 private int minVolume;
97 private int maxVolume;
98 private int minToneLevel;
99 private int maxToneLevel;
100 private int minBalance;
101 private int maxBalance;
106 * @param model the projector model in use
107 * @param protocolHandler the protocol handler
108 * @param sourcesLabels the custom labels for sources
109 * @param readerThreadName the name of thread to be created
111 public RotelSimuConnector(RotelModel model, RotelAbstractProtocolHandler protocolHandler,
112 Map<RotelSource, String> sourcesLabels, String readerThreadName) {
113 super(protocolHandler, true, readerThreadName);
115 this.protocol = protocolHandler.getProtocol();
116 this.sourcesLabels = sourcesLabels;
118 this.maxVolume = model.hasVolumeControl() ? model.getVolumeMax() : 0;
119 this.maxToneLevel = model.hasToneControl() ? model.getToneLevelMax() : 0;
120 this.minToneLevel = -this.maxToneLevel;
121 this.maxBalance = model.hasBalanceControl() ? model.getBalanceLevelMax() : 0;
122 this.minBalance = -this.maxBalance;
123 List<RotelSource> modelSources = model.getSources();
124 RotelSource source = modelSources.isEmpty() ? RotelSource.CAT0_CD : modelSources.get(0);
125 sources = new RotelSource[] { source, source, source, source, source };
126 recordSource = source;
130 public synchronized void open() throws RotelException {
131 logger.debug("Opening simulated connection");
132 readerThread.start();
134 logger.debug("Simulated connection opened");
138 public synchronized void close() {
139 logger.debug("Closing simulated connection");
142 logger.debug("Simulated connection closed");
146 protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
147 synchronized (lock) {
148 int len = feedbackMsg.length - idxInFeedbackMsg;
150 if (len > dataBuffer.length) {
151 len = dataBuffer.length;
153 System.arraycopy(feedbackMsg, idxInFeedbackMsg, dataBuffer, 0, len);
154 idxInFeedbackMsg += len;
158 // Give more chance to someone else than the reader thread to get the lock
161 } catch (InterruptedException e) {
162 Thread.currentThread().interrupt();
168 * Built the simulated feedback message for a sent command
170 * @param cmd the sent command
171 * @param value the integer value considered in the sent command for volume, bass or treble adjustment
173 public void buildFeedbackMessage(RotelCommand cmd, @Nullable Integer value) {
174 String text = buildSourceLine1Response();
175 String textLine1Left = buildSourceLine1LeftResponse();
176 String textLine1Right = buildVolumeLine1RightResponse();
177 String textLine2 = "";
178 String textAscii = "";
179 boolean variableLength = false;
180 boolean accepted = true;
181 boolean resetZone = true;
184 case ZONE1_VOLUME_UP:
185 case ZONE1_VOLUME_DOWN:
186 case ZONE1_VOLUME_SET:
187 case ZONE1_MUTE_TOGGLE:
191 case ZONE1_BASS_DOWN:
193 case ZONE1_TREBLE_UP:
194 case ZONE1_TREBLE_DOWN:
195 case ZONE1_TREBLE_SET:
196 case ZONE1_BALANCE_LEFT:
197 case ZONE1_BALANCE_RIGHT:
198 case ZONE1_BALANCE_SET:
201 case ZONE2_POWER_OFF:
203 case ZONE2_VOLUME_UP:
204 case ZONE2_VOLUME_DOWN:
205 case ZONE2_VOLUME_SET:
206 case ZONE2_MUTE_TOGGLE:
210 case ZONE2_BASS_DOWN:
212 case ZONE2_TREBLE_UP:
213 case ZONE2_TREBLE_DOWN:
214 case ZONE2_TREBLE_SET:
215 case ZONE2_BALANCE_LEFT:
216 case ZONE2_BALANCE_RIGHT:
217 case ZONE2_BALANCE_SET:
220 case ZONE3_POWER_OFF:
222 case ZONE3_VOLUME_UP:
223 case ZONE3_VOLUME_DOWN:
224 case ZONE3_VOLUME_SET:
225 case ZONE3_MUTE_TOGGLE:
229 case ZONE3_BASS_DOWN:
231 case ZONE3_TREBLE_UP:
232 case ZONE3_TREBLE_DOWN:
233 case ZONE3_TREBLE_SET:
234 case ZONE3_BALANCE_LEFT:
235 case ZONE3_BALANCE_RIGHT:
236 case ZONE3_BALANCE_SET:
239 case ZONE4_POWER_OFF:
241 case ZONE4_VOLUME_UP:
242 case ZONE4_VOLUME_DOWN:
243 case ZONE4_VOLUME_SET:
244 case ZONE4_MUTE_TOGGLE:
248 case ZONE4_BASS_DOWN:
250 case ZONE4_TREBLE_UP:
251 case ZONE4_TREBLE_DOWN:
252 case ZONE4_TREBLE_SET:
253 case ZONE4_BALANCE_LEFT:
254 case ZONE4_BALANCE_RIGHT:
255 case ZONE4_BALANCE_SET:
262 case DISPLAY_REFRESH:
265 case MAIN_ZONE_POWER_OFF:
267 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
268 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
269 powers[zone] = false;
272 text = buildSourceLine1Response();
273 textLine1Left = buildSourceLine1LeftResponse();
274 textLine1Right = buildVolumeLine1RightResponse();
275 textAscii = buildPowerAsciiResponse();
278 case MAIN_ZONE_POWER_ON:
280 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
281 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
285 text = buildSourceLine1Response();
286 textLine1Left = buildSourceLine1LeftResponse();
287 textLine1Right = buildVolumeLine1RightResponse();
288 textAscii = buildPowerAsciiResponse();
291 textAscii = buildPowerAsciiResponse();
293 case ZONE2_POWER_OFF:
294 case ZONE3_POWER_OFF:
295 case ZONE4_POWER_OFF:
296 powers[numZone] = false;
297 text = textLine2 = buildZonePowerResponse(numZone);
304 powers[numZone] = true;
305 text = textLine2 = buildZonePowerResponse(numZone);
309 case RECORD_FONCTION_SELECT:
310 if (model.getNumberOfZones() > 1 && model.getZoneSelectCmd() == cmd) {
312 if (showZone >= model.getNumberOfZones()) {
322 selectingRecord = powers[0];
324 textLine2 = buildRecordResponse();
325 } else if (showZone >= 2 && showZone <= 4) {
326 selectingRecord = false;
327 text = textLine2 = buildZonePowerResponse(showZone);
332 if (model.getNumberOfZones() == 1 || (model.getNumberOfZones() > 2 && model.getZoneSelectCmd() == cmd)
333 || (showZone == 1 && model.getZoneSelectCmd() != cmd)) {
336 if (model.getZoneSelectCmd() == cmd) {
337 if (!powers[0] && !powers[2]) {
340 } else if (showZone == 2) {
341 powers[2] = !powers[2];
345 } else if (showZone >= 2 && showZone <= 4) {
346 powers[showZone] = !powers[showZone];
348 if (showZone >= 2 && showZone <= 4) {
349 text = textLine2 = buildZonePowerResponse(showZone);
358 if (!accepted && numZone > 0 && powers[numZone]) {
361 case ZONE1_VOLUME_UP:
362 case ZONE2_VOLUME_UP:
363 case ZONE3_VOLUME_UP:
364 case ZONE4_VOLUME_UP:
365 if (volumes[numZone] < maxVolume) {
368 text = textLine2 = buildZoneVolumeResponse(numZone);
369 textAscii = buildVolumeAsciiResponse();
371 case ZONE1_VOLUME_DOWN:
372 case ZONE2_VOLUME_DOWN:
373 case ZONE3_VOLUME_DOWN:
374 case ZONE4_VOLUME_DOWN:
375 if (volumes[numZone] > minVolume) {
378 text = textLine2 = buildZoneVolumeResponse(numZone);
379 textAscii = buildVolumeAsciiResponse();
381 case ZONE1_VOLUME_SET:
382 case ZONE2_VOLUME_SET:
383 case ZONE3_VOLUME_SET:
384 case ZONE4_VOLUME_SET:
386 volumes[numZone] = value;
388 text = textLine2 = buildZoneVolumeResponse(numZone);
389 textAscii = buildVolumeAsciiResponse();
391 case ZONE1_MUTE_TOGGLE:
392 case ZONE2_MUTE_TOGGLE:
393 case ZONE3_MUTE_TOGGLE:
394 case ZONE4_MUTE_TOGGLE:
395 mutes[numZone] = !mutes[numZone];
396 text = textLine2 = buildZoneVolumeResponse(numZone);
397 textAscii = buildMuteAsciiResponse();
403 mutes[numZone] = true;
404 text = textLine2 = buildZoneVolumeResponse(numZone);
405 textAscii = buildMuteAsciiResponse();
411 mutes[numZone] = false;
412 text = textLine2 = buildZoneVolumeResponse(numZone);
413 textAscii = buildMuteAsciiResponse();
419 if (!tcbypass && basses[numZone] < maxToneLevel) {
420 basses[numZone] += STEP_TONE_LEVEL;
422 textAscii = buildBassAsciiResponse();
424 case ZONE1_BASS_DOWN:
425 case ZONE2_BASS_DOWN:
426 case ZONE3_BASS_DOWN:
427 case ZONE4_BASS_DOWN:
428 if (!tcbypass && basses[numZone] > minToneLevel) {
429 basses[numZone] -= STEP_TONE_LEVEL;
431 textAscii = buildBassAsciiResponse();
437 if (!tcbypass && value != null) {
438 basses[numZone] = value;
440 textAscii = buildBassAsciiResponse();
442 case ZONE1_TREBLE_UP:
443 case ZONE2_TREBLE_UP:
444 case ZONE3_TREBLE_UP:
445 case ZONE4_TREBLE_UP:
446 if (!tcbypass && trebles[numZone] < maxToneLevel) {
447 trebles[numZone] += STEP_TONE_LEVEL;
449 textAscii = buildTrebleAsciiResponse();
451 case ZONE1_TREBLE_DOWN:
452 case ZONE2_TREBLE_DOWN:
453 case ZONE3_TREBLE_DOWN:
454 case ZONE4_TREBLE_DOWN:
455 if (!tcbypass && trebles[numZone] > minToneLevel) {
456 trebles[numZone] -= STEP_TONE_LEVEL;
458 textAscii = buildTrebleAsciiResponse();
460 case ZONE1_TREBLE_SET:
461 case ZONE2_TREBLE_SET:
462 case ZONE3_TREBLE_SET:
463 case ZONE4_TREBLE_SET:
464 if (!tcbypass && value != null) {
465 trebles[numZone] = value;
467 textAscii = buildTrebleAsciiResponse();
469 case ZONE1_BALANCE_LEFT:
470 case ZONE2_BALANCE_LEFT:
471 case ZONE3_BALANCE_LEFT:
472 case ZONE4_BALANCE_LEFT:
473 if (balances[numZone] > minBalance) {
476 textAscii = buildBalanceAsciiResponse();
478 case ZONE1_BALANCE_RIGHT:
479 case ZONE2_BALANCE_RIGHT:
480 case ZONE3_BALANCE_RIGHT:
481 case ZONE4_BALANCE_RIGHT:
482 if (balances[numZone] < maxBalance) {
485 textAscii = buildBalanceAsciiResponse();
487 case ZONE1_BALANCE_SET:
488 case ZONE2_BALANCE_SET:
489 case ZONE3_BALANCE_SET:
490 case ZONE4_BALANCE_SET:
492 balances[numZone] = value;
494 textAscii = buildBalanceAsciiResponse();
502 // Check if command is a change of source input for a zone
503 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
506 sources[zone] = model.getZoneSourceFromCommand(cmd, zone);
507 text = textLine2 = buildZonePowerResponse(zone);
508 textAscii = buildSourceAsciiResponse();
514 } catch (RotelException e) {
519 if (!accepted && powers[2] && !model.hasZoneCommands(2) && model.getNumberOfZones() > 1 && showZone == 2) {
523 if (volumes[2] < maxVolume) {
526 text = textLine2 = buildZoneVolumeResponse(2);
530 if (volumes[2] > minVolume) {
533 text = textLine2 = buildZoneVolumeResponse(2);
540 text = textLine2 = buildZoneVolumeResponse(2);
549 sources[2] = model.getSourceFromCommand(cmd);
550 text = textLine2 = buildZonePowerResponse(2);
554 } catch (RotelException e) {
558 if (!accepted && powers[0]) {
562 textAscii = buildAsciiResponse(
563 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, AUTO);
566 textAscii = buildAsciiResponse(
567 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, MANUAL);
569 case POWER_MODE_QUICK:
570 powerMode = POWER_QUICK;
571 textAscii = buildAsciiResponse(KEY_POWER_MODE, powerMode);
573 case POWER_MODE_NORMAL:
574 powerMode = POWER_NORMAL;
575 textAscii = buildAsciiResponse(KEY_POWER_MODE, powerMode);
578 textAscii = buildAsciiResponse(KEY_POWER_MODE, powerMode);
581 textAscii = buildAsciiResponse(KEY_VOLUME_MIN, minVolume);
584 textAscii = buildAsciiResponse(KEY_VOLUME_MAX, maxVolume);
587 case MAIN_ZONE_VOLUME_UP:
588 if (volumes[0] < maxVolume) {
591 text = buildVolumeLine1Response();
592 textLine1Right = buildVolumeLine1RightResponse();
593 textAscii = buildVolumeAsciiResponse();
596 case MAIN_ZONE_VOLUME_DOWN:
597 if (volumes[0] > minVolume) {
600 text = buildVolumeLine1Response();
601 textLine1Right = buildVolumeLine1RightResponse();
602 textAscii = buildVolumeAsciiResponse();
608 text = buildVolumeLine1Response();
609 textLine1Right = buildVolumeLine1RightResponse();
610 textAscii = buildVolumeAsciiResponse();
613 textAscii = buildVolumeAsciiResponse();
616 case MAIN_ZONE_MUTE_TOGGLE:
617 mutes[0] = !mutes[0];
618 text = buildSourceLine1Response();
619 textLine1Right = buildVolumeLine1RightResponse();
620 textAscii = buildMuteAsciiResponse();
623 case MAIN_ZONE_MUTE_ON:
625 text = buildSourceLine1Response();
626 textLine1Right = buildVolumeLine1RightResponse();
627 textAscii = buildMuteAsciiResponse();
630 case MAIN_ZONE_MUTE_OFF:
632 text = buildSourceLine1Response();
633 textLine1Right = buildVolumeLine1RightResponse();
634 textAscii = buildMuteAsciiResponse();
637 textAscii = buildMuteAsciiResponse();
640 textAscii = buildAsciiResponse(KEY_TONE_MAX, String.format("%02d", maxToneLevel));
642 case TONE_CONTROLS_ON:
644 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
646 case TONE_CONTROLS_OFF:
648 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
651 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
655 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
659 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
662 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
665 if (!tcbypass && basses[0] < maxToneLevel) {
666 basses[0] += STEP_TONE_LEVEL;
668 text = buildBassLine1Response();
669 textLine1Right = buildBassLine1RightResponse();
670 textAscii = buildBassAsciiResponse();
673 if (!tcbypass && basses[0] > minToneLevel) {
674 basses[0] -= STEP_TONE_LEVEL;
676 text = buildBassLine1Response();
677 textLine1Right = buildBassLine1RightResponse();
678 textAscii = buildBassAsciiResponse();
681 if (!tcbypass && value != null) {
684 text = buildBassLine1Response();
685 textLine1Right = buildBassLine1RightResponse();
686 textAscii = buildBassAsciiResponse();
689 textAscii = buildBassAsciiResponse();
692 if (!tcbypass && trebles[0] < maxToneLevel) {
693 trebles[0] += STEP_TONE_LEVEL;
695 text = buildTrebleLine1Response();
696 textLine1Right = buildTrebleLine1RightResponse();
697 textAscii = buildTrebleAsciiResponse();
700 if (!tcbypass && trebles[0] > minToneLevel) {
701 trebles[0] -= STEP_TONE_LEVEL;
703 text = buildTrebleLine1Response();
704 textLine1Right = buildTrebleLine1RightResponse();
705 textAscii = buildTrebleAsciiResponse();
708 if (!tcbypass && value != null) {
711 text = buildTrebleLine1Response();
712 textLine1Right = buildTrebleLine1RightResponse();
713 textAscii = buildTrebleAsciiResponse();
716 textAscii = buildTrebleAsciiResponse();
718 case TONE_CONTROL_SELECT:
719 showTreble = !showTreble;
721 text = buildTrebleLine1Response();
722 textLine1Right = buildTrebleLine1RightResponse();
724 text = buildBassLine1Response();
725 textLine1Right = buildBassLine1RightResponse();
729 if (balances[0] > minBalance) {
732 textAscii = buildBalanceAsciiResponse();
735 if (balances[0] < maxBalance) {
738 textAscii = buildBalanceAsciiResponse();
744 textAscii = buildBalanceAsciiResponse();
747 textAscii = buildBalanceAsciiResponse();
749 case SPEAKER_A_TOGGLE:
750 speakerA = !speakerA;
751 textAscii = buildSpeakerAsciiResponse();
755 textAscii = buildSpeakerAsciiResponse();
759 textAscii = buildSpeakerAsciiResponse();
761 case SPEAKER_B_TOGGLE:
762 speakerB = !speakerB;
763 textAscii = buildSpeakerAsciiResponse();
767 textAscii = buildSpeakerAsciiResponse();
771 textAscii = buildSpeakerAsciiResponse();
774 textAscii = buildSpeakerAsciiResponse();
777 playStatus = RotelPlayStatus.PLAYING;
778 textAscii = buildPlayStatusAsciiResponse();
781 playStatus = RotelPlayStatus.STOPPED;
782 textAscii = buildPlayStatusAsciiResponse();
785 switch (playStatus) {
787 playStatus = RotelPlayStatus.PAUSED;
791 playStatus = RotelPlayStatus.PLAYING;
794 textAscii = buildPlayStatusAsciiResponse();
798 textAscii = buildPlayStatusAsciiResponse();
802 textAscii = buildTrackAsciiResponse();
808 textAscii = buildTrackAsciiResponse();
811 textAscii = buildTrackAsciiResponse();
814 randomMode = !randomMode;
815 textAscii = buildRandomModeAsciiResponse();
818 textAscii = buildRandomModeAsciiResponse();
821 switch (repeatMode) {
823 repeatMode = RotelRepeatMode.DISC;
826 repeatMode = RotelRepeatMode.OFF;
829 repeatMode = RotelRepeatMode.TRACK;
832 textAscii = buildRepeatModeAsciiResponse();
835 textAscii = buildRepeatModeAsciiResponse();
837 case SOURCE_MULTI_INPUT:
838 multiinput = !multiinput;
839 text = "MULTI IN " + (multiinput ? "ON" : "OFF");
841 sources[0] = model.getSourceFromCommand(cmd);
842 textLine1Left = buildSourceLine1LeftResponse();
843 textAscii = buildSourceAsciiResponse();
845 } catch (RotelException e) {
850 textAscii = buildSourceAsciiResponse();
853 dsp = RotelDsp.CAT4_NONE;
854 textLine2 = bypass ? "BYPASS" : "STEREO";
855 textAscii = buildDspAsciiResponse();
858 dsp = RotelDsp.CAT4_STEREO3;
859 textLine2 = "DOLBY 3 STEREO";
860 textAscii = buildDspAsciiResponse();
863 dsp = RotelDsp.CAT4_STEREO5;
864 textLine2 = "5CH STEREO";
865 textAscii = buildDspAsciiResponse();
868 dsp = RotelDsp.CAT4_STEREO7;
869 textLine2 = "7CH STEREO";
870 textAscii = buildDspAsciiResponse();
873 dsp = RotelDsp.CAT5_STEREO9;
874 textAscii = buildDspAsciiResponse();
877 dsp = RotelDsp.CAT5_STEREO11;
878 textAscii = buildDspAsciiResponse();
881 dsp = RotelDsp.CAT4_DSP1;
883 textAscii = buildDspAsciiResponse();
886 dsp = RotelDsp.CAT4_DSP2;
888 textAscii = buildDspAsciiResponse();
891 dsp = RotelDsp.CAT4_DSP3;
893 textAscii = buildDspAsciiResponse();
896 dsp = RotelDsp.CAT4_DSP4;
898 textAscii = buildDspAsciiResponse();
901 dsp = RotelDsp.CAT4_PROLOGIC;
902 textLine2 = "DOLBY PRO LOGIC";
903 textAscii = buildDspAsciiResponse();
906 dsp = RotelDsp.CAT4_PLII_CINEMA;
907 textLine2 = "DOLBY PL C";
908 textAscii = buildDspAsciiResponse();
911 dsp = RotelDsp.CAT4_PLII_MUSIC;
912 textLine2 = "DOLBY PL M";
913 textAscii = buildDspAsciiResponse();
916 dsp = RotelDsp.CAT4_PLII_GAME;
917 textLine2 = "DOLBY PL G";
918 textAscii = buildDspAsciiResponse();
921 dsp = RotelDsp.CAT4_PLIIZ;
922 textLine2 = "DOLBY PL z";
923 textAscii = buildDspAsciiResponse();
926 dsp = RotelDsp.CAT4_NEO6_MUSIC;
927 textLine2 = "DTS Neo:6 M";
928 textAscii = buildDspAsciiResponse();
931 dsp = RotelDsp.CAT4_NEO6_CINEMA;
932 textLine2 = "DTS Neo:6 C";
933 textAscii = buildDspAsciiResponse();
936 dsp = RotelDsp.CAT5_ATMOS;
937 textAscii = buildDspAsciiResponse();
940 dsp = RotelDsp.CAT5_NEURAL_X;
941 textAscii = buildDspAsciiResponse();
944 dsp = RotelDsp.CAT5_BYPASS;
945 textAscii = buildDspAsciiResponse();
948 textAscii = buildDspAsciiResponse();
950 case STEREO_BYPASS_TOGGLE:
952 textLine2 = bypass ? "BYPASS" : "STEREO";
955 textAscii = model.getNumberOfZones() > 1 ? buildAsciiResponse(KEY_FREQ, "44.1,48,none,176.4")
956 : buildAsciiResponse(KEY_FREQ, "44.1");
959 subLevel += STEP_DECIBEL;
960 textAscii = buildAsciiResponse(KEY_SUB_LEVEL, buildDecibelValue(subLevel));
963 subLevel -= STEP_DECIBEL;
964 textAscii = buildAsciiResponse(KEY_SUB_LEVEL, buildDecibelValue(subLevel));
967 centerLevel += STEP_DECIBEL;
968 textAscii = buildAsciiResponse(KEY_CENTER_LEVEL, buildDecibelValue(centerLevel));
971 centerLevel -= STEP_DECIBEL;
972 textAscii = buildAsciiResponse(KEY_CENTER_LEVEL, buildDecibelValue(centerLevel));
975 surroundRightLevel += STEP_DECIBEL;
976 textAscii = buildAsciiResponse(KEY_SURROUND_RIGHT_LEVEL, buildDecibelValue(surroundRightLevel));
979 surroundRightLevel -= STEP_DECIBEL;
980 textAscii = buildAsciiResponse(KEY_SURROUND_RIGHT_LEVEL, buildDecibelValue(surroundRightLevel));
983 surroundLefLevel += STEP_DECIBEL;
984 textAscii = buildAsciiResponse(KEY_SURROUND_LEFT_LEVEL, buildDecibelValue(surroundLefLevel));
987 surroundLefLevel -= STEP_DECIBEL;
988 textAscii = buildAsciiResponse(KEY_SURROUND_LEFT_LEVEL, buildDecibelValue(surroundLefLevel));
991 centerBackRightLevel += STEP_DECIBEL;
992 textAscii = buildAsciiResponse(KEY_CENTER_BACK_RIGHT_LEVEL,
993 buildDecibelValue(centerBackRightLevel));
996 centerBackRightLevel -= STEP_DECIBEL;
997 textAscii = buildAsciiResponse(KEY_CENTER_BACK_RIGHT_LEVEL,
998 buildDecibelValue(centerBackRightLevel));
1001 centerBackLefLevel += STEP_DECIBEL;
1002 textAscii = buildAsciiResponse(KEY_CENTER_BACK_LEFT_LEVEL, buildDecibelValue(centerBackLefLevel));
1004 case CBL_LEVEL_DOWN:
1005 centerBackLefLevel -= STEP_DECIBEL;
1006 textAscii = buildAsciiResponse(KEY_CENTER_BACK_LEFT_LEVEL, buildDecibelValue(centerBackLefLevel));
1009 ceilingFrontRightLevel += STEP_DECIBEL;
1010 textAscii = buildAsciiResponse(KEY_CEILING_FRONT_RIGHT_LEVEL,
1011 buildDecibelValue(ceilingFrontRightLevel));
1013 case CFR_LEVEL_DOWN:
1014 ceilingFrontRightLevel -= STEP_DECIBEL;
1015 textAscii = buildAsciiResponse(KEY_CEILING_FRONT_RIGHT_LEVEL,
1016 buildDecibelValue(ceilingFrontRightLevel));
1019 ceilingFrontLefLevel += STEP_DECIBEL;
1020 textAscii = buildAsciiResponse(KEY_CEILING_FRONT_LEFT_LEVEL,
1021 buildDecibelValue(ceilingFrontLefLevel));
1023 case CFL_LEVEL_DOWN:
1024 ceilingFrontLefLevel -= STEP_DECIBEL;
1025 textAscii = buildAsciiResponse(KEY_CEILING_FRONT_LEFT_LEVEL,
1026 buildDecibelValue(ceilingFrontLefLevel));
1029 ceilingRearRightLevel += STEP_DECIBEL;
1030 textAscii = buildAsciiResponse(KEY_CEILING_REAR_RIGHT_LEVEL,
1031 buildDecibelValue(ceilingRearRightLevel));
1033 case CRR_LEVEL_DOWN:
1034 ceilingRearRightLevel -= STEP_DECIBEL;
1035 textAscii = buildAsciiResponse(KEY_CEILING_REAR_RIGHT_LEVEL,
1036 buildDecibelValue(ceilingRearRightLevel));
1039 ceilingRearLefLevel += STEP_DECIBEL;
1040 textAscii = buildAsciiResponse(KEY_CEILING_REAR_LEFT_LEVEL, buildDecibelValue(ceilingRearLefLevel));
1042 case CRL_LEVEL_DOWN:
1043 ceilingRearLefLevel -= STEP_DECIBEL;
1044 textAscii = buildAsciiResponse(KEY_CEILING_REAR_LEFT_LEVEL, buildDecibelValue(ceilingRearLefLevel));
1046 case DIMMER_LEVEL_SET:
1047 if (value != null) {
1050 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
1052 case DIMMER_LEVEL_GET:
1053 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
1057 textAscii = buildAsciiResponse(KEY_PCUSB_CLASS, pcUsbClass);
1061 textAscii = buildAsciiResponse(KEY_PCUSB_CLASS, pcUsbClass);
1064 textAscii = buildAsciiResponse(KEY_PCUSB_CLASS, pcUsbClass);
1067 if (protocol == RotelProtocol.ASCII_V1) {
1068 variableLength = true;
1069 textAscii = buildAsciiResponse(KEY_PRODUCT_TYPE,
1070 String.format("%d,%s", model.getName().length(), model.getName()));
1072 textAscii = buildAsciiResponse(KEY_MODEL, model.getName());
1076 if (protocol == RotelProtocol.ASCII_V1) {
1077 variableLength = true;
1078 textAscii = buildAsciiResponse(KEY_PRODUCT_VERSION,
1079 String.format("%d,%s", FIRMWARE.length(), FIRMWARE));
1081 textAscii = buildAsciiResponse(KEY_VERSION, FIRMWARE);
1089 // Check if command is a change of source input for the main zone
1091 sources[0] = model.getZoneSourceFromCommand(cmd, 1);
1092 text = buildSourceLine1Response();
1093 textLine1Left = buildSourceLine1LeftResponse();
1094 textAscii = buildSourceAsciiResponse();
1096 } catch (RotelException e) {
1100 // Check if command is a change of source input
1102 if (selectingRecord && !model.hasOtherThanPrimaryCommands()) {
1103 recordSource = model.getSourceFromCommand(cmd);
1105 sources[0] = model.getSourceFromCommand(cmd);
1107 text = buildSourceLine1Response();
1108 textLine1Left = buildSourceLine1LeftResponse();
1109 textAscii = buildSourceAsciiResponse();
1112 } catch (RotelException e) {
1116 // Check if command is a change of record source
1118 recordSource = model.getRecordSourceFromCommand(cmd);
1119 text = buildSourceLine1Response();
1120 textLine2 = buildRecordResponse();
1122 } catch (RotelException e) {
1131 if (cmd != RotelCommand.RECORD_FONCTION_SELECT) {
1132 selectingRecord = false;
1138 if (model.getRespNbChars() == 42) {
1139 while (textLine1Left.length() < 14) {
1140 textLine1Left += " ";
1142 while (textLine1Right.length() < 7) {
1143 textLine1Right += " ";
1145 while (textLine2.length() < 21) {
1148 text = textLine1Left + textLine1Right + textLine2;
1151 if (protocol == RotelProtocol.HEX) {
1152 byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), model.getRespNbChars());
1153 byte[] flags = new byte[model.getRespNbFlags()];
1155 model.setMultiInput(flags, multiinput);
1156 } catch (RotelException e) {
1159 model.setZone2(flags, powers[2]);
1160 } catch (RotelException e) {
1163 model.setZone3(flags, powers[3]);
1164 } catch (RotelException e) {
1167 model.setZone4(flags, powers[4]);
1168 } catch (RotelException e) {
1170 int size = 6 + model.getRespNbChars() + model.getRespNbFlags();
1171 byte[] dataBuffer = new byte[size];
1173 dataBuffer[idx++] = START;
1174 dataBuffer[idx++] = (byte) (size - 4);
1175 dataBuffer[idx++] = model.getDeviceId();
1176 dataBuffer[idx++] = STANDARD_RESPONSE;
1177 if (model.isCharsBeforeFlags()) {
1178 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1179 idx += model.getRespNbChars();
1180 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1181 idx += model.getRespNbFlags();
1183 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1184 idx += model.getRespNbFlags();
1185 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1186 idx += model.getRespNbChars();
1188 byte checksum = RotelHexProtocolHandler.computeCheckSum(dataBuffer, idx - 1);
1189 if ((checksum & 0x000000FF) == 0x000000FD) {
1190 dataBuffer[idx++] = (byte) 0xFD;
1191 dataBuffer[idx++] = 0;
1192 } else if ((checksum & 0x000000FF) == 0x000000FE) {
1193 dataBuffer[idx++] = (byte) 0xFD;
1194 dataBuffer[idx++] = 1;
1196 dataBuffer[idx++] = checksum;
1198 synchronized (lock) {
1199 feedbackMsg = Arrays.copyOf(dataBuffer, idx);
1200 idxInFeedbackMsg = 0;
1203 String command = textAscii;
1204 if (protocol == RotelProtocol.ASCII_V1 && !variableLength) {
1206 } else if (protocol == RotelProtocol.ASCII_V2 && !variableLength) {
1208 } else if (protocol == RotelProtocol.ASCII_V2 && variableLength) {
1211 synchronized (lock) {
1212 feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
1213 idxInFeedbackMsg = 0;
1218 private String buildAsciiResponse(String key, String value) {
1219 return String.format("%s=%s", key, value);
1222 private String buildAsciiResponse(String key, int value) {
1223 return String.format("%s=%d", key, value);
1226 private String buildAsciiResponse(String key, boolean value) {
1227 return buildAsciiResponse(key, buildOnOffValue(value));
1230 private String buildOnOffValue(boolean on) {
1231 return on ? MSG_VALUE_ON : MSG_VALUE_OFF;
1234 private String buildPowerAsciiResponse() {
1235 return buildAsciiResponse(KEY_POWER, powers[0] ? POWER_ON : STANDBY);
1238 private String buildVolumeAsciiResponse() {
1239 if (model.getNumberOfZones() > 1) {
1240 StringJoiner sj = new StringJoiner(",");
1241 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1242 sj.add(String.format("%02d", volumes[zone]));
1244 return buildAsciiResponse(KEY_VOLUME, sj.toString());
1246 return buildAsciiResponse(KEY_VOLUME, String.format("%02d", volumes[0]));
1250 private String buildMuteAsciiResponse() {
1251 if (model.getNumberOfZones() > 1) {
1252 StringJoiner sj = new StringJoiner(",");
1253 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1254 sj.add(buildOnOffValue(mutes[zone]));
1256 return buildAsciiResponse(KEY_MUTE, sj.toString());
1258 return buildAsciiResponse(KEY_MUTE, mutes[0]);
1262 private String buildBassAsciiResponse() {
1263 if (model.getNumberOfZones() > 1) {
1264 StringJoiner sj = new StringJoiner(",");
1265 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1266 sj.add(buildBassTrebleValue(basses[zone]));
1268 return buildAsciiResponse(KEY_BASS, sj.toString());
1270 return buildAsciiResponse(KEY_BASS, buildBassTrebleValue(basses[0]));
1274 private String buildTrebleAsciiResponse() {
1275 if (model.getNumberOfZones() > 1) {
1276 StringJoiner sj = new StringJoiner(",");
1277 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1278 sj.add(buildBassTrebleValue(trebles[zone]));
1280 return buildAsciiResponse(KEY_TREBLE, sj.toString());
1282 return buildAsciiResponse(KEY_TREBLE, buildBassTrebleValue(trebles[0]));
1286 private String buildBassTrebleValue(int value) {
1287 if (tcbypass || value == 0) {
1289 } else if (value > 0) {
1290 return String.format("+%02d", value);
1292 return String.format("-%02d", -value);
1296 private String buildBalanceAsciiResponse() {
1297 if (model.getNumberOfZones() > 1) {
1298 StringJoiner sj = new StringJoiner(",");
1299 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1300 sj.add(buildBalanceValue(balances[zone]));
1302 return buildAsciiResponse(KEY_BALANCE, sj.toString());
1304 return buildAsciiResponse(KEY_BALANCE, buildBalanceValue(balances[0]));
1308 private String buildBalanceValue(int value) {
1311 } else if (value > 0) {
1312 return String.format("r%02d", value);
1314 return String.format("l%02d", -value);
1318 private String buildSpeakerAsciiResponse() {
1320 if (speakerA && speakerB) {
1321 value = MSG_VALUE_SPEAKER_AB;
1322 } else if (speakerA && !speakerB) {
1323 value = MSG_VALUE_SPEAKER_A;
1324 } else if (!speakerA && speakerB) {
1325 value = MSG_VALUE_SPEAKER_B;
1327 value = MSG_VALUE_OFF;
1329 return buildAsciiResponse(KEY_SPEAKER, value);
1332 private String buildPlayStatusAsciiResponse() {
1334 switch (playStatus) {
1345 return buildAsciiResponse(protocol == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_STATUS, status);
1348 private String buildTrackAsciiResponse() {
1349 return buildAsciiResponse(KEY_TRACK, String.format("%03d", track));
1352 private String buildRandomModeAsciiResponse() {
1353 return buildAsciiResponse(KEY_RANDOM, randomMode);
1356 private String buildRepeatModeAsciiResponse() {
1358 switch (repeatMode) {
1366 mode = MSG_VALUE_OFF;
1369 return buildAsciiResponse(KEY_REPEAT, mode);
1372 private String buildSourceAsciiResponse() {
1373 if (model.getNumberOfZones() > 1) {
1374 StringJoiner sj = new StringJoiner(",");
1375 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1376 sj.add(buildZoneSourceValue(sources[zone]));
1378 return buildAsciiResponse(KEY_INPUT, sj.toString());
1380 return buildAsciiResponse(KEY_SOURCE, buildSourceValue(sources[0]));
1384 private String buildSourceValue(RotelSource source) {
1386 RotelCommand command = source.getCommand();
1387 if (command != null) {
1388 str = protocol == RotelProtocol.ASCII_V1 ? command.getAsciiCommandV1() : command.getAsciiCommandV2();
1390 return str == null ? "" : str;
1393 private String buildZoneSourceValue(RotelSource source) {
1394 String str = buildSourceValue(source);
1395 int idx = str.indexOf("input_");
1396 return idx < 0 ? str : str.substring(idx + 6);
1399 private String buildDspAsciiResponse() {
1400 return buildAsciiResponse(KEY_DSP_MODE, dsp.getFeedback());
1403 private String buildDecibelValue(double value) {
1407 return String.format("%+05.1fdb", value).replace(",", ".");
1411 private String buildSourceLine1Response() {
1415 } else if (mutes[0]) {
1418 text = getSourceLabel(sources[0], false) + " " + getSourceLabel(recordSource, true);
1423 private String buildSourceLine1LeftResponse() {
1428 text = getSourceLabel(sources[0], false);
1433 private String buildRecordResponse() {
1438 text = "REC " + getSourceLabel(recordSource, true);
1443 private String buildZonePowerResponse(int numZone) {
1446 zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1448 zone = String.format("ZONE%d", numZone);
1450 String state = powers[numZone] ? getSourceLabel(sources[numZone], true) : "OFF";
1451 return zone + " " + state;
1454 private String buildVolumeLine1Response() {
1456 if (volumes[0] == minVolume) {
1457 text = " VOLUME MIN ";
1458 } else if (volumes[0] == maxVolume) {
1459 text = " VOLUME MAX ";
1461 text = String.format(" VOLUME %02d ", volumes[0]);
1466 private String buildVolumeLine1RightResponse() {
1470 } else if (mutes[0]) {
1472 } else if (volumes[0] == minVolume) {
1474 } else if (volumes[0] == maxVolume) {
1477 text = String.format("VOL %02d", volumes[0]);
1482 private String buildZoneVolumeResponse(int numZone) {
1485 zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1487 zone = String.format("ZONE%d", numZone);
1490 if (mutes[numZone]) {
1491 text = zone + " MUTE ON";
1492 } else if (volumes[numZone] == minVolume) {
1493 text = zone + " VOL MIN";
1494 } else if (volumes[numZone] == maxVolume) {
1495 text = zone + " VOL MAX";
1497 text = String.format("%s VOL %02d", zone, volumes[numZone]);
1502 private String buildBassLine1Response() {
1504 if (basses[0] == minToneLevel) {
1505 text = " BASS MIN ";
1506 } else if (basses[0] == maxToneLevel) {
1507 text = " BASS MAX ";
1508 } else if (basses[0] == 0) {
1510 } else if (basses[0] > 0) {
1511 text = String.format(" BASS +%02d ", basses[0]);
1513 text = String.format(" BASS -%02d ", -basses[0]);
1518 private String buildBassLine1RightResponse() {
1520 if (basses[0] == minToneLevel) {
1522 } else if (basses[0] == maxToneLevel) {
1524 } else if (basses[0] == 0) {
1526 } else if (basses[0] > 0) {
1527 text = String.format("LF + %02d", basses[0]);
1529 text = String.format("LF - %02d", -basses[0]);
1534 private String buildTrebleLine1Response() {
1536 if (trebles[0] == minToneLevel) {
1537 text = " TREBLE MIN ";
1538 } else if (trebles[0] == maxToneLevel) {
1539 text = " TREBLE MAX ";
1540 } else if (trebles[0] == 0) {
1541 text = " TREBLE 0 ";
1542 } else if (trebles[0] > 0) {
1543 text = String.format(" TREBLE +%02d ", trebles[0]);
1545 text = String.format(" TREBLE -%02d ", -trebles[0]);
1550 private String buildTrebleLine1RightResponse() {
1552 if (trebles[0] == minToneLevel) {
1554 } else if (trebles[0] == maxToneLevel) {
1556 } else if (trebles[0] == 0) {
1558 } else if (trebles[0] > 0) {
1559 text = String.format("HF + %02d", trebles[0]);
1561 text = String.format("HF - %02d", -trebles[0]);
1566 private String getSourceLabel(RotelSource source, boolean considerFollowMain) {
1568 if (considerFollowMain && source.getName().equals(RotelSource.CAT1_FOLLOW_MAIN.getName())) {
1571 label = Objects.requireNonNullElse(sourcesLabels.get(source), source.getLabel());