2 * Copyright (c) 2010-2023 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 int fmPreset = 5;
82 private int dabPreset = 15;
83 private int iradioPreset = 25;
84 private boolean selectingRecord;
87 private int pcUsbClass = 1;
88 private double subLevel;
89 private double centerLevel;
90 private double surroundRightLevel;
91 private double surroundLefLevel;
92 private double centerBackRightLevel;
93 private double centerBackLefLevel;
94 private double ceilingFrontRightLevel;
95 private double ceilingFrontLefLevel;
96 private double ceilingRearRightLevel;
97 private double ceilingRearLefLevel;
99 private int minVolume;
100 private int maxVolume;
101 private int minToneLevel;
102 private int maxToneLevel;
103 private int minBalance;
104 private int maxBalance;
109 * @param model the projector model in use
110 * @param protocolHandler the protocol handler
111 * @param sourcesLabels the custom labels for sources
112 * @param readerThreadName the name of thread to be created
114 public RotelSimuConnector(RotelModel model, RotelAbstractProtocolHandler protocolHandler,
115 Map<RotelSource, String> sourcesLabels, String readerThreadName) {
116 super(protocolHandler, true, readerThreadName);
118 this.protocol = protocolHandler.getProtocol();
119 this.sourcesLabels = sourcesLabels;
121 this.maxVolume = model.hasVolumeControl() ? model.getVolumeMax() : 0;
122 this.maxToneLevel = model.hasToneControl() ? model.getToneLevelMax() : 0;
123 this.minToneLevel = -this.maxToneLevel;
124 this.maxBalance = model.hasBalanceControl() ? model.getBalanceLevelMax() : 0;
125 this.minBalance = -this.maxBalance;
126 List<RotelSource> modelSources = model.getSources();
127 RotelSource source = modelSources.isEmpty() ? RotelSource.CAT0_CD : modelSources.get(0);
128 sources = new RotelSource[] { source, source, source, source, source };
129 recordSource = source;
133 public synchronized void open() throws RotelException {
134 logger.debug("Opening simulated connection");
137 logger.debug("Simulated connection opened");
141 public synchronized void close() {
142 logger.debug("Closing simulated connection");
145 logger.debug("Simulated connection closed");
149 protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
150 synchronized (lock) {
151 int len = feedbackMsg.length - idxInFeedbackMsg;
153 if (len > dataBuffer.length) {
154 len = dataBuffer.length;
156 System.arraycopy(feedbackMsg, idxInFeedbackMsg, dataBuffer, 0, len);
157 idxInFeedbackMsg += len;
161 // Give more chance to someone else than the reader thread to get the lock
164 } catch (InterruptedException e) {
165 Thread.currentThread().interrupt();
171 * Built the simulated feedback message for a sent command
173 * @param cmd the sent command
174 * @param value the integer value considered in the sent command for volume, bass or treble adjustment
176 public void buildFeedbackMessage(RotelCommand cmd, @Nullable Integer value) {
177 String text = buildSourceLine1Response();
178 String textLine1Left = buildSourceLine1LeftResponse();
179 String textLine1Right = buildVolumeLine1RightResponse();
180 String textLine2 = "";
181 String textAscii = "";
182 boolean variableLength = false;
183 boolean accepted = true;
184 boolean resetZone = true;
187 case ZONE1_VOLUME_UP:
188 case ZONE1_VOLUME_DOWN:
189 case ZONE1_VOLUME_SET:
190 case ZONE1_MUTE_TOGGLE:
194 case ZONE1_BASS_DOWN:
196 case ZONE1_TREBLE_UP:
197 case ZONE1_TREBLE_DOWN:
198 case ZONE1_TREBLE_SET:
199 case ZONE1_BALANCE_LEFT:
200 case ZONE1_BALANCE_RIGHT:
201 case ZONE1_BALANCE_SET:
204 case ZONE2_POWER_OFF:
206 case ZONE2_VOLUME_UP:
207 case ZONE2_VOLUME_DOWN:
208 case ZONE2_VOLUME_SET:
209 case ZONE2_MUTE_TOGGLE:
213 case ZONE2_BASS_DOWN:
215 case ZONE2_TREBLE_UP:
216 case ZONE2_TREBLE_DOWN:
217 case ZONE2_TREBLE_SET:
218 case ZONE2_BALANCE_LEFT:
219 case ZONE2_BALANCE_RIGHT:
220 case ZONE2_BALANCE_SET:
223 case ZONE3_POWER_OFF:
225 case ZONE3_VOLUME_UP:
226 case ZONE3_VOLUME_DOWN:
227 case ZONE3_VOLUME_SET:
228 case ZONE3_MUTE_TOGGLE:
232 case ZONE3_BASS_DOWN:
234 case ZONE3_TREBLE_UP:
235 case ZONE3_TREBLE_DOWN:
236 case ZONE3_TREBLE_SET:
237 case ZONE3_BALANCE_LEFT:
238 case ZONE3_BALANCE_RIGHT:
239 case ZONE3_BALANCE_SET:
242 case ZONE4_POWER_OFF:
244 case ZONE4_VOLUME_UP:
245 case ZONE4_VOLUME_DOWN:
246 case ZONE4_VOLUME_SET:
247 case ZONE4_MUTE_TOGGLE:
251 case ZONE4_BASS_DOWN:
253 case ZONE4_TREBLE_UP:
254 case ZONE4_TREBLE_DOWN:
255 case ZONE4_TREBLE_SET:
256 case ZONE4_BALANCE_LEFT:
257 case ZONE4_BALANCE_RIGHT:
258 case ZONE4_BALANCE_SET:
265 case DISPLAY_REFRESH:
268 case MAIN_ZONE_POWER_OFF:
270 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
271 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
272 powers[zone] = false;
275 text = buildSourceLine1Response();
276 textLine1Left = buildSourceLine1LeftResponse();
277 textLine1Right = buildVolumeLine1RightResponse();
278 textAscii = buildPowerAsciiResponse();
281 case MAIN_ZONE_POWER_ON:
283 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
284 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
288 text = buildSourceLine1Response();
289 textLine1Left = buildSourceLine1LeftResponse();
290 textLine1Right = buildVolumeLine1RightResponse();
291 textAscii = buildPowerAsciiResponse();
294 textAscii = buildPowerAsciiResponse();
296 case ZONE2_POWER_OFF:
297 case ZONE3_POWER_OFF:
298 case ZONE4_POWER_OFF:
299 powers[numZone] = false;
300 text = textLine2 = buildZonePowerResponse(numZone);
307 powers[numZone] = true;
308 text = textLine2 = buildZonePowerResponse(numZone);
312 case RECORD_FONCTION_SELECT:
313 if (model.getNumberOfZones() > 1 && model.getZoneSelectCmd() == cmd) {
315 if (showZone >= model.getNumberOfZones()) {
325 selectingRecord = powers[0];
327 textLine2 = buildRecordResponse();
328 } else if (showZone >= 2 && showZone <= 4) {
329 selectingRecord = false;
330 text = textLine2 = buildZonePowerResponse(showZone);
335 if (model.getNumberOfZones() == 1 || (model.getNumberOfZones() > 2 && model.getZoneSelectCmd() == cmd)
336 || (showZone == 1 && model.getZoneSelectCmd() != cmd)) {
339 if (model.getZoneSelectCmd() == cmd) {
340 if (!powers[0] && !powers[2]) {
343 } else if (showZone == 2) {
344 powers[2] = !powers[2];
348 } else if (showZone >= 2 && showZone <= 4) {
349 powers[showZone] = !powers[showZone];
351 if (showZone >= 2 && showZone <= 4) {
352 text = textLine2 = buildZonePowerResponse(showZone);
361 if (!accepted && numZone > 0 && powers[numZone]) {
364 case ZONE1_VOLUME_UP:
365 case ZONE2_VOLUME_UP:
366 case ZONE3_VOLUME_UP:
367 case ZONE4_VOLUME_UP:
368 if (volumes[numZone] < maxVolume) {
371 text = textLine2 = buildZoneVolumeResponse(numZone);
372 textAscii = buildVolumeAsciiResponse();
374 case ZONE1_VOLUME_DOWN:
375 case ZONE2_VOLUME_DOWN:
376 case ZONE3_VOLUME_DOWN:
377 case ZONE4_VOLUME_DOWN:
378 if (volumes[numZone] > minVolume) {
381 text = textLine2 = buildZoneVolumeResponse(numZone);
382 textAscii = buildVolumeAsciiResponse();
384 case ZONE1_VOLUME_SET:
385 case ZONE2_VOLUME_SET:
386 case ZONE3_VOLUME_SET:
387 case ZONE4_VOLUME_SET:
389 volumes[numZone] = value;
391 text = textLine2 = buildZoneVolumeResponse(numZone);
392 textAscii = buildVolumeAsciiResponse();
394 case ZONE1_MUTE_TOGGLE:
395 case ZONE2_MUTE_TOGGLE:
396 case ZONE3_MUTE_TOGGLE:
397 case ZONE4_MUTE_TOGGLE:
398 mutes[numZone] = !mutes[numZone];
399 text = textLine2 = buildZoneVolumeResponse(numZone);
400 textAscii = buildMuteAsciiResponse();
406 mutes[numZone] = true;
407 text = textLine2 = buildZoneVolumeResponse(numZone);
408 textAscii = buildMuteAsciiResponse();
414 mutes[numZone] = false;
415 text = textLine2 = buildZoneVolumeResponse(numZone);
416 textAscii = buildMuteAsciiResponse();
422 if (!tcbypass && basses[numZone] < maxToneLevel) {
423 basses[numZone] += STEP_TONE_LEVEL;
425 textAscii = buildBassAsciiResponse();
427 case ZONE1_BASS_DOWN:
428 case ZONE2_BASS_DOWN:
429 case ZONE3_BASS_DOWN:
430 case ZONE4_BASS_DOWN:
431 if (!tcbypass && basses[numZone] > minToneLevel) {
432 basses[numZone] -= STEP_TONE_LEVEL;
434 textAscii = buildBassAsciiResponse();
440 if (!tcbypass && value != null) {
441 basses[numZone] = value;
443 textAscii = buildBassAsciiResponse();
445 case ZONE1_TREBLE_UP:
446 case ZONE2_TREBLE_UP:
447 case ZONE3_TREBLE_UP:
448 case ZONE4_TREBLE_UP:
449 if (!tcbypass && trebles[numZone] < maxToneLevel) {
450 trebles[numZone] += STEP_TONE_LEVEL;
452 textAscii = buildTrebleAsciiResponse();
454 case ZONE1_TREBLE_DOWN:
455 case ZONE2_TREBLE_DOWN:
456 case ZONE3_TREBLE_DOWN:
457 case ZONE4_TREBLE_DOWN:
458 if (!tcbypass && trebles[numZone] > minToneLevel) {
459 trebles[numZone] -= STEP_TONE_LEVEL;
461 textAscii = buildTrebleAsciiResponse();
463 case ZONE1_TREBLE_SET:
464 case ZONE2_TREBLE_SET:
465 case ZONE3_TREBLE_SET:
466 case ZONE4_TREBLE_SET:
467 if (!tcbypass && value != null) {
468 trebles[numZone] = value;
470 textAscii = buildTrebleAsciiResponse();
472 case ZONE1_BALANCE_LEFT:
473 case ZONE2_BALANCE_LEFT:
474 case ZONE3_BALANCE_LEFT:
475 case ZONE4_BALANCE_LEFT:
476 if (balances[numZone] > minBalance) {
479 textAscii = buildBalanceAsciiResponse();
481 case ZONE1_BALANCE_RIGHT:
482 case ZONE2_BALANCE_RIGHT:
483 case ZONE3_BALANCE_RIGHT:
484 case ZONE4_BALANCE_RIGHT:
485 if (balances[numZone] < maxBalance) {
488 textAscii = buildBalanceAsciiResponse();
490 case ZONE1_BALANCE_SET:
491 case ZONE2_BALANCE_SET:
492 case ZONE3_BALANCE_SET:
493 case ZONE4_BALANCE_SET:
495 balances[numZone] = value;
497 textAscii = buildBalanceAsciiResponse();
505 // Check if command is a change of source input for a zone
506 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
509 sources[zone] = model.getZoneSourceFromCommand(cmd, zone);
510 text = textLine2 = buildZonePowerResponse(zone);
511 textAscii = buildSourceAsciiResponse();
517 } catch (RotelException e) {
522 if (!accepted && powers[2] && !model.hasZoneCommands(2) && model.getNumberOfZones() > 1 && showZone == 2) {
526 if (volumes[2] < maxVolume) {
529 text = textLine2 = buildZoneVolumeResponse(2);
533 if (volumes[2] > minVolume) {
536 text = textLine2 = buildZoneVolumeResponse(2);
543 text = textLine2 = buildZoneVolumeResponse(2);
552 sources[2] = model.getSourceFromCommand(cmd);
553 text = textLine2 = buildZonePowerResponse(2);
557 } catch (RotelException e) {
561 if (!accepted && powers[0]) {
565 textAscii = buildAsciiResponse(
566 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, AUTO);
569 textAscii = buildAsciiResponse(
570 protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, MANUAL);
572 case POWER_MODE_QUICK:
573 powerMode = POWER_QUICK;
574 textAscii = buildAsciiResponse(KEY_POWER_MODE, powerMode);
576 case POWER_MODE_NORMAL:
577 powerMode = POWER_NORMAL;
578 textAscii = buildAsciiResponse(KEY_POWER_MODE, powerMode);
581 textAscii = buildAsciiResponse(KEY_POWER_MODE, powerMode);
584 textAscii = buildAsciiResponse(KEY_VOLUME_MIN, minVolume);
587 textAscii = buildAsciiResponse(KEY_VOLUME_MAX, maxVolume);
590 case MAIN_ZONE_VOLUME_UP:
591 if (volumes[0] < maxVolume) {
594 text = buildVolumeLine1Response();
595 textLine1Right = buildVolumeLine1RightResponse();
596 textAscii = buildVolumeAsciiResponse();
599 case MAIN_ZONE_VOLUME_DOWN:
600 if (volumes[0] > minVolume) {
603 text = buildVolumeLine1Response();
604 textLine1Right = buildVolumeLine1RightResponse();
605 textAscii = buildVolumeAsciiResponse();
611 text = buildVolumeLine1Response();
612 textLine1Right = buildVolumeLine1RightResponse();
613 textAscii = buildVolumeAsciiResponse();
616 textAscii = buildVolumeAsciiResponse();
619 case MAIN_ZONE_MUTE_TOGGLE:
620 mutes[0] = !mutes[0];
621 text = buildSourceLine1Response();
622 textLine1Right = buildVolumeLine1RightResponse();
623 textAscii = buildMuteAsciiResponse();
626 case MAIN_ZONE_MUTE_ON:
628 text = buildSourceLine1Response();
629 textLine1Right = buildVolumeLine1RightResponse();
630 textAscii = buildMuteAsciiResponse();
633 case MAIN_ZONE_MUTE_OFF:
635 text = buildSourceLine1Response();
636 textLine1Right = buildVolumeLine1RightResponse();
637 textAscii = buildMuteAsciiResponse();
640 textAscii = buildMuteAsciiResponse();
643 textAscii = buildAsciiResponse(KEY_TONE_MAX, String.format("%02d", maxToneLevel));
645 case TONE_CONTROLS_ON:
647 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
649 case TONE_CONTROLS_OFF:
651 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
654 textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
658 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
662 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
665 textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
668 if (!tcbypass && basses[0] < maxToneLevel) {
669 basses[0] += STEP_TONE_LEVEL;
671 text = buildBassLine1Response();
672 textLine1Right = buildBassLine1RightResponse();
673 textAscii = buildBassAsciiResponse();
676 if (!tcbypass && basses[0] > minToneLevel) {
677 basses[0] -= STEP_TONE_LEVEL;
679 text = buildBassLine1Response();
680 textLine1Right = buildBassLine1RightResponse();
681 textAscii = buildBassAsciiResponse();
684 if (!tcbypass && value != null) {
687 text = buildBassLine1Response();
688 textLine1Right = buildBassLine1RightResponse();
689 textAscii = buildBassAsciiResponse();
692 textAscii = buildBassAsciiResponse();
695 if (!tcbypass && trebles[0] < maxToneLevel) {
696 trebles[0] += STEP_TONE_LEVEL;
698 text = buildTrebleLine1Response();
699 textLine1Right = buildTrebleLine1RightResponse();
700 textAscii = buildTrebleAsciiResponse();
703 if (!tcbypass && trebles[0] > minToneLevel) {
704 trebles[0] -= STEP_TONE_LEVEL;
706 text = buildTrebleLine1Response();
707 textLine1Right = buildTrebleLine1RightResponse();
708 textAscii = buildTrebleAsciiResponse();
711 if (!tcbypass && value != null) {
714 text = buildTrebleLine1Response();
715 textLine1Right = buildTrebleLine1RightResponse();
716 textAscii = buildTrebleAsciiResponse();
719 textAscii = buildTrebleAsciiResponse();
721 case TONE_CONTROL_SELECT:
722 showTreble = !showTreble;
724 text = buildTrebleLine1Response();
725 textLine1Right = buildTrebleLine1RightResponse();
727 text = buildBassLine1Response();
728 textLine1Right = buildBassLine1RightResponse();
732 if (balances[0] > minBalance) {
735 textAscii = buildBalanceAsciiResponse();
738 if (balances[0] < maxBalance) {
741 textAscii = buildBalanceAsciiResponse();
747 textAscii = buildBalanceAsciiResponse();
750 textAscii = buildBalanceAsciiResponse();
752 case SPEAKER_A_TOGGLE:
753 speakerA = !speakerA;
754 textAscii = buildSpeakerAsciiResponse();
758 textAscii = buildSpeakerAsciiResponse();
762 textAscii = buildSpeakerAsciiResponse();
764 case SPEAKER_B_TOGGLE:
765 speakerB = !speakerB;
766 textAscii = buildSpeakerAsciiResponse();
770 textAscii = buildSpeakerAsciiResponse();
774 textAscii = buildSpeakerAsciiResponse();
777 textAscii = buildSpeakerAsciiResponse();
780 playStatus = RotelPlayStatus.PLAYING;
781 textAscii = buildPlayStatusAsciiResponse();
784 playStatus = RotelPlayStatus.STOPPED;
785 textAscii = buildPlayStatusAsciiResponse();
788 switch (playStatus) {
790 playStatus = RotelPlayStatus.PAUSED;
794 playStatus = RotelPlayStatus.PLAYING;
797 textAscii = buildPlayStatusAsciiResponse();
801 textAscii = buildPlayStatusAsciiResponse();
805 textAscii = buildTrackAsciiResponse();
811 textAscii = buildTrackAsciiResponse();
814 textAscii = buildTrackAsciiResponse();
817 randomMode = !randomMode;
818 textAscii = buildRandomModeAsciiResponse();
821 textAscii = buildRandomModeAsciiResponse();
824 switch (repeatMode) {
826 repeatMode = RotelRepeatMode.DISC;
829 repeatMode = RotelRepeatMode.OFF;
832 repeatMode = RotelRepeatMode.TRACK;
835 textAscii = buildRepeatModeAsciiResponse();
838 textAscii = buildRepeatModeAsciiResponse();
842 fmPreset = value.intValue();
843 if (protocol == RotelProtocol.ASCII_V1) {
844 variableLength = true;
845 textAscii = buildAsciiResponse(String.format("%s%d", KEY_FM_PRESET, fmPreset),
854 case CALL_DAB_PRESET:
856 dabPreset = value.intValue();
857 if (protocol == RotelProtocol.ASCII_V1) {
858 variableLength = true;
859 textAscii = buildAsciiResponse(String.format("%s%d", KEY_DAB_PRESET, dabPreset),
868 case CALL_IRADIO_PRESET:
870 iradioPreset = value.intValue();
871 variableLength = true;
872 textAscii = buildAsciiResponse(String.format("%s%d", KEY_IRADIO_PRESET, iradioPreset),
879 if ("FM".equals(sources[0].getName())) {
880 textAscii = buildAsciiResponse(KEY_PRESET_FM, fmPreset);
881 } else if ("DAB".equals(sources[0].getName())) {
882 textAscii = buildAsciiResponse(KEY_PRESET_DAB, dabPreset);
883 } else if ("IRADIO".equals(sources[0].getName())) {
884 textAscii = buildAsciiResponse(KEY_PRESET_IRADIO, iradioPreset);
886 textAscii = buildAsciiResponse(KEY_PRESET_FM, 0);
890 if ("FM".equals(sources[0].getName())) {
891 textAscii = buildAsciiResponse(KEY_FM, String.format("%02d", fmPreset));
893 textAscii = buildAsciiResponse(KEY_FM, "00");
897 if ("DAB".equals(sources[0].getName())) {
898 textAscii = buildAsciiResponse(KEY_DAB, String.format("%02d", dabPreset));
900 textAscii = buildAsciiResponse(KEY_DAB, "00");
903 case SOURCE_MULTI_INPUT:
904 multiinput = !multiinput;
905 text = "MULTI IN " + (multiinput ? "ON" : "OFF");
907 sources[0] = model.getSourceFromCommand(cmd);
908 textLine1Left = buildSourceLine1LeftResponse();
909 textAscii = buildSourceAsciiResponse();
911 } catch (RotelException e) {
916 textAscii = buildSourceAsciiResponse();
919 dsp = RotelDsp.CAT4_NONE;
920 textLine2 = bypass ? "BYPASS" : "STEREO";
921 textAscii = buildDspAsciiResponse();
924 dsp = RotelDsp.CAT4_STEREO3;
925 textLine2 = "DOLBY 3 STEREO";
926 textAscii = buildDspAsciiResponse();
929 dsp = RotelDsp.CAT4_STEREO5;
930 textLine2 = "5CH STEREO";
931 textAscii = buildDspAsciiResponse();
934 dsp = RotelDsp.CAT4_STEREO7;
935 textLine2 = "7CH STEREO";
936 textAscii = buildDspAsciiResponse();
939 dsp = RotelDsp.CAT5_STEREO9;
940 textAscii = buildDspAsciiResponse();
943 dsp = RotelDsp.CAT5_STEREO11;
944 textAscii = buildDspAsciiResponse();
947 dsp = RotelDsp.CAT4_DSP1;
949 textAscii = buildDspAsciiResponse();
952 dsp = RotelDsp.CAT4_DSP2;
954 textAscii = buildDspAsciiResponse();
957 dsp = RotelDsp.CAT4_DSP3;
959 textAscii = buildDspAsciiResponse();
962 dsp = RotelDsp.CAT4_DSP4;
964 textAscii = buildDspAsciiResponse();
967 dsp = RotelDsp.CAT4_PROLOGIC;
968 textLine2 = "DOLBY PRO LOGIC";
969 textAscii = buildDspAsciiResponse();
972 dsp = RotelDsp.CAT4_PLII_CINEMA;
973 textLine2 = "DOLBY PL C";
974 textAscii = buildDspAsciiResponse();
977 dsp = RotelDsp.CAT4_PLII_MUSIC;
978 textLine2 = "DOLBY PL M";
979 textAscii = buildDspAsciiResponse();
982 dsp = RotelDsp.CAT4_PLII_GAME;
983 textLine2 = "DOLBY PL G";
984 textAscii = buildDspAsciiResponse();
987 dsp = RotelDsp.CAT4_PLIIZ;
988 textLine2 = "DOLBY PL z";
989 textAscii = buildDspAsciiResponse();
992 dsp = RotelDsp.CAT4_NEO6_MUSIC;
993 textLine2 = "DTS Neo:6 M";
994 textAscii = buildDspAsciiResponse();
997 dsp = RotelDsp.CAT4_NEO6_CINEMA;
998 textLine2 = "DTS Neo:6 C";
999 textAscii = buildDspAsciiResponse();
1002 dsp = RotelDsp.CAT5_ATMOS;
1003 textAscii = buildDspAsciiResponse();
1006 dsp = RotelDsp.CAT5_NEURAL_X;
1007 textAscii = buildDspAsciiResponse();
1010 dsp = RotelDsp.CAT5_BYPASS;
1011 textAscii = buildDspAsciiResponse();
1014 textAscii = buildDspAsciiResponse();
1016 case STEREO_BYPASS_TOGGLE:
1018 textLine2 = bypass ? "BYPASS" : "STEREO";
1021 textAscii = model.getNumberOfZones() > 1 ? buildAsciiResponse(KEY_FREQ, "44.1,48,none,176.4")
1022 : buildAsciiResponse(KEY_FREQ, "44.1");
1025 subLevel += STEP_DECIBEL;
1026 textAscii = buildAsciiResponse(KEY_SUB_LEVEL, buildDecibelValue(subLevel));
1028 case SUB_LEVEL_DOWN:
1029 subLevel -= STEP_DECIBEL;
1030 textAscii = buildAsciiResponse(KEY_SUB_LEVEL, buildDecibelValue(subLevel));
1033 centerLevel += STEP_DECIBEL;
1034 textAscii = buildAsciiResponse(KEY_CENTER_LEVEL, buildDecibelValue(centerLevel));
1037 centerLevel -= STEP_DECIBEL;
1038 textAscii = buildAsciiResponse(KEY_CENTER_LEVEL, buildDecibelValue(centerLevel));
1041 surroundRightLevel += STEP_DECIBEL;
1042 textAscii = buildAsciiResponse(KEY_SURROUND_RIGHT_LEVEL, buildDecibelValue(surroundRightLevel));
1045 surroundRightLevel -= STEP_DECIBEL;
1046 textAscii = buildAsciiResponse(KEY_SURROUND_RIGHT_LEVEL, buildDecibelValue(surroundRightLevel));
1049 surroundLefLevel += STEP_DECIBEL;
1050 textAscii = buildAsciiResponse(KEY_SURROUND_LEFT_LEVEL, buildDecibelValue(surroundLefLevel));
1053 surroundLefLevel -= STEP_DECIBEL;
1054 textAscii = buildAsciiResponse(KEY_SURROUND_LEFT_LEVEL, buildDecibelValue(surroundLefLevel));
1057 centerBackRightLevel += STEP_DECIBEL;
1058 textAscii = buildAsciiResponse(KEY_CENTER_BACK_RIGHT_LEVEL,
1059 buildDecibelValue(centerBackRightLevel));
1061 case CBR_LEVEL_DOWN:
1062 centerBackRightLevel -= STEP_DECIBEL;
1063 textAscii = buildAsciiResponse(KEY_CENTER_BACK_RIGHT_LEVEL,
1064 buildDecibelValue(centerBackRightLevel));
1067 centerBackLefLevel += STEP_DECIBEL;
1068 textAscii = buildAsciiResponse(KEY_CENTER_BACK_LEFT_LEVEL, buildDecibelValue(centerBackLefLevel));
1070 case CBL_LEVEL_DOWN:
1071 centerBackLefLevel -= STEP_DECIBEL;
1072 textAscii = buildAsciiResponse(KEY_CENTER_BACK_LEFT_LEVEL, buildDecibelValue(centerBackLefLevel));
1075 ceilingFrontRightLevel += STEP_DECIBEL;
1076 textAscii = buildAsciiResponse(KEY_CEILING_FRONT_RIGHT_LEVEL,
1077 buildDecibelValue(ceilingFrontRightLevel));
1079 case CFR_LEVEL_DOWN:
1080 ceilingFrontRightLevel -= STEP_DECIBEL;
1081 textAscii = buildAsciiResponse(KEY_CEILING_FRONT_RIGHT_LEVEL,
1082 buildDecibelValue(ceilingFrontRightLevel));
1085 ceilingFrontLefLevel += STEP_DECIBEL;
1086 textAscii = buildAsciiResponse(KEY_CEILING_FRONT_LEFT_LEVEL,
1087 buildDecibelValue(ceilingFrontLefLevel));
1089 case CFL_LEVEL_DOWN:
1090 ceilingFrontLefLevel -= STEP_DECIBEL;
1091 textAscii = buildAsciiResponse(KEY_CEILING_FRONT_LEFT_LEVEL,
1092 buildDecibelValue(ceilingFrontLefLevel));
1095 ceilingRearRightLevel += STEP_DECIBEL;
1096 textAscii = buildAsciiResponse(KEY_CEILING_REAR_RIGHT_LEVEL,
1097 buildDecibelValue(ceilingRearRightLevel));
1099 case CRR_LEVEL_DOWN:
1100 ceilingRearRightLevel -= STEP_DECIBEL;
1101 textAscii = buildAsciiResponse(KEY_CEILING_REAR_RIGHT_LEVEL,
1102 buildDecibelValue(ceilingRearRightLevel));
1105 ceilingRearLefLevel += STEP_DECIBEL;
1106 textAscii = buildAsciiResponse(KEY_CEILING_REAR_LEFT_LEVEL, buildDecibelValue(ceilingRearLefLevel));
1108 case CRL_LEVEL_DOWN:
1109 ceilingRearLefLevel -= STEP_DECIBEL;
1110 textAscii = buildAsciiResponse(KEY_CEILING_REAR_LEFT_LEVEL, buildDecibelValue(ceilingRearLefLevel));
1112 case DIMMER_LEVEL_SET:
1113 if (value != null) {
1116 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
1118 case DIMMER_LEVEL_GET:
1119 textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
1123 textAscii = buildAsciiResponse(KEY_PCUSB_CLASS, pcUsbClass);
1127 textAscii = buildAsciiResponse(KEY_PCUSB_CLASS, pcUsbClass);
1130 textAscii = buildAsciiResponse(KEY_PCUSB_CLASS, pcUsbClass);
1133 if (protocol == RotelProtocol.ASCII_V1) {
1134 variableLength = true;
1135 textAscii = buildAsciiResponse(KEY_PRODUCT_TYPE,
1136 String.format("%d,%s", model.getName().length(), model.getName()));
1138 textAscii = buildAsciiResponse(KEY_MODEL, model.getName());
1142 if (protocol == RotelProtocol.ASCII_V1) {
1143 variableLength = true;
1144 textAscii = buildAsciiResponse(KEY_PRODUCT_VERSION,
1145 String.format("%d,%s", FIRMWARE.length(), FIRMWARE));
1147 textAscii = buildAsciiResponse(KEY_VERSION, FIRMWARE);
1155 // Check if command is a change of source input for the main zone
1157 sources[0] = model.getZoneSourceFromCommand(cmd, 1);
1158 text = buildSourceLine1Response();
1159 textLine1Left = buildSourceLine1LeftResponse();
1160 textAscii = buildSourceAsciiResponse();
1162 } catch (RotelException e) {
1166 // Check if command is a change of source input
1168 if (selectingRecord && !model.hasOtherThanPrimaryCommands()) {
1169 recordSource = model.getSourceFromCommand(cmd);
1171 sources[0] = model.getSourceFromCommand(cmd);
1173 text = buildSourceLine1Response();
1174 textLine1Left = buildSourceLine1LeftResponse();
1175 textAscii = buildSourceAsciiResponse();
1178 } catch (RotelException e) {
1182 // Check if command is a change of record source
1184 recordSource = model.getRecordSourceFromCommand(cmd);
1185 text = buildSourceLine1Response();
1186 textLine2 = buildRecordResponse();
1188 } catch (RotelException e) {
1197 if (cmd != RotelCommand.RECORD_FONCTION_SELECT) {
1198 selectingRecord = false;
1204 if (model.getRespNbChars() == 42) {
1205 while (textLine1Left.length() < 14) {
1206 textLine1Left += " ";
1208 while (textLine1Right.length() < 7) {
1209 textLine1Right += " ";
1211 while (textLine2.length() < 21) {
1214 text = textLine1Left + textLine1Right + textLine2;
1217 if (protocol == RotelProtocol.HEX) {
1218 byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), model.getRespNbChars());
1219 byte[] flags = new byte[model.getRespNbFlags()];
1221 model.setMultiInput(flags, multiinput);
1222 } catch (RotelException e) {
1225 model.setZone2(flags, powers[2]);
1226 } catch (RotelException e) {
1229 model.setZone3(flags, powers[3]);
1230 } catch (RotelException e) {
1233 model.setZone4(flags, powers[4]);
1234 } catch (RotelException e) {
1236 int size = 6 + model.getRespNbChars() + model.getRespNbFlags();
1237 byte[] dataBuffer = new byte[size];
1239 dataBuffer[idx++] = START;
1240 dataBuffer[idx++] = (byte) (size - 4);
1241 dataBuffer[idx++] = model.getDeviceId();
1242 dataBuffer[idx++] = STANDARD_RESPONSE;
1243 if (model.isCharsBeforeFlags()) {
1244 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1245 idx += model.getRespNbChars();
1246 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1247 idx += model.getRespNbFlags();
1249 System.arraycopy(flags, 0, dataBuffer, idx, model.getRespNbFlags());
1250 idx += model.getRespNbFlags();
1251 System.arraycopy(chars, 0, dataBuffer, idx, model.getRespNbChars());
1252 idx += model.getRespNbChars();
1254 byte checksum = RotelHexProtocolHandler.computeCheckSum(dataBuffer, idx - 1);
1255 if ((checksum & 0x000000FF) == 0x000000FD) {
1256 dataBuffer[idx++] = (byte) 0xFD;
1257 dataBuffer[idx++] = 0;
1258 } else if ((checksum & 0x000000FF) == 0x000000FE) {
1259 dataBuffer[idx++] = (byte) 0xFD;
1260 dataBuffer[idx++] = 1;
1262 dataBuffer[idx++] = checksum;
1264 synchronized (lock) {
1265 feedbackMsg = Arrays.copyOf(dataBuffer, idx);
1266 idxInFeedbackMsg = 0;
1269 String command = textAscii;
1270 if (protocol == RotelProtocol.ASCII_V1 && !variableLength) {
1272 } else if (protocol == RotelProtocol.ASCII_V2 && !variableLength) {
1274 } else if (protocol == RotelProtocol.ASCII_V2 && variableLength) {
1277 synchronized (lock) {
1278 feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
1279 idxInFeedbackMsg = 0;
1284 private String buildAsciiResponse(String key, String value) {
1285 return String.format("%s=%s", key, value);
1288 private String buildAsciiResponse(String key, int value) {
1289 return String.format("%s=%d", key, value);
1292 private String buildAsciiResponse(String key, boolean value) {
1293 return buildAsciiResponse(key, buildOnOffValue(value));
1296 private String buildOnOffValue(boolean on) {
1297 return on ? MSG_VALUE_ON : MSG_VALUE_OFF;
1300 private String buildPowerAsciiResponse() {
1301 return buildAsciiResponse(KEY_POWER, powers[0] ? POWER_ON : STANDBY);
1304 private String buildVolumeAsciiResponse() {
1305 if (model.getNumberOfZones() > 1) {
1306 StringJoiner sj = new StringJoiner(",");
1307 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1308 sj.add(String.format("%02d", volumes[zone]));
1310 return buildAsciiResponse(KEY_VOLUME, sj.toString());
1312 return buildAsciiResponse(KEY_VOLUME, String.format("%02d", volumes[0]));
1316 private String buildMuteAsciiResponse() {
1317 if (model.getNumberOfZones() > 1) {
1318 StringJoiner sj = new StringJoiner(",");
1319 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1320 sj.add(buildOnOffValue(mutes[zone]));
1322 return buildAsciiResponse(KEY_MUTE, sj.toString());
1324 return buildAsciiResponse(KEY_MUTE, mutes[0]);
1328 private String buildBassAsciiResponse() {
1329 if (model.getNumberOfZones() > 1) {
1330 StringJoiner sj = new StringJoiner(",");
1331 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1332 sj.add(buildBassTrebleValue(basses[zone]));
1334 return buildAsciiResponse(KEY_BASS, sj.toString());
1336 return buildAsciiResponse(KEY_BASS, buildBassTrebleValue(basses[0]));
1340 private String buildTrebleAsciiResponse() {
1341 if (model.getNumberOfZones() > 1) {
1342 StringJoiner sj = new StringJoiner(",");
1343 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1344 sj.add(buildBassTrebleValue(trebles[zone]));
1346 return buildAsciiResponse(KEY_TREBLE, sj.toString());
1348 return buildAsciiResponse(KEY_TREBLE, buildBassTrebleValue(trebles[0]));
1352 private String buildBassTrebleValue(int value) {
1353 if (tcbypass || value == 0) {
1355 } else if (value > 0) {
1356 return String.format("+%02d", value);
1358 return String.format("-%02d", -value);
1362 private String buildBalanceAsciiResponse() {
1363 if (model.getNumberOfZones() > 1) {
1364 StringJoiner sj = new StringJoiner(",");
1365 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1366 sj.add(buildBalanceValue(balances[zone]));
1368 return buildAsciiResponse(KEY_BALANCE, sj.toString());
1370 return buildAsciiResponse(KEY_BALANCE, buildBalanceValue(balances[0]));
1374 private String buildBalanceValue(int value) {
1377 } else if (value > 0) {
1378 return String.format("r%02d", value);
1380 return String.format("l%02d", -value);
1384 private String buildSpeakerAsciiResponse() {
1386 if (speakerA && speakerB) {
1387 value = MSG_VALUE_SPEAKER_AB;
1388 } else if (speakerA && !speakerB) {
1389 value = MSG_VALUE_SPEAKER_A;
1390 } else if (!speakerA && speakerB) {
1391 value = MSG_VALUE_SPEAKER_B;
1393 value = MSG_VALUE_OFF;
1395 return buildAsciiResponse(KEY_SPEAKER, value);
1398 private String buildPlayStatusAsciiResponse() {
1400 switch (playStatus) {
1411 return buildAsciiResponse(protocol == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_STATUS, status);
1414 private String buildTrackAsciiResponse() {
1415 return buildAsciiResponse(KEY_TRACK, String.format("%03d", track));
1418 private String buildRandomModeAsciiResponse() {
1419 return buildAsciiResponse(KEY_RANDOM, randomMode);
1422 private String buildRepeatModeAsciiResponse() {
1424 switch (repeatMode) {
1432 mode = MSG_VALUE_OFF;
1435 return buildAsciiResponse(KEY_REPEAT, mode);
1438 private String buildSourceAsciiResponse() {
1439 if (model.getNumberOfZones() > 1) {
1440 StringJoiner sj = new StringJoiner(",");
1441 for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
1442 sj.add(buildZoneSourceValue(sources[zone]));
1444 return buildAsciiResponse(KEY_INPUT, sj.toString());
1446 return buildAsciiResponse(KEY_SOURCE, buildSourceValue(sources[0]));
1450 private String buildSourceValue(RotelSource source) {
1452 RotelCommand command = source.getCommand();
1453 if (command != null) {
1454 str = protocol == RotelProtocol.ASCII_V1 ? command.getAsciiCommandV1() : command.getAsciiCommandV2();
1456 return str == null ? "" : str;
1459 private String buildZoneSourceValue(RotelSource source) {
1460 String str = buildSourceValue(source);
1461 int idx = str.indexOf("input_");
1462 return idx < 0 ? str : str.substring(idx + 6);
1465 private String buildDspAsciiResponse() {
1466 return buildAsciiResponse(KEY_DSP_MODE, dsp.getFeedback());
1469 private String buildDecibelValue(double value) {
1473 return String.format("%+05.1fdb", value).replace(",", ".");
1477 private String buildSourceLine1Response() {
1481 } else if (mutes[0]) {
1484 text = getSourceLabel(sources[0], false) + " " + getSourceLabel(recordSource, true);
1489 private String buildSourceLine1LeftResponse() {
1494 text = getSourceLabel(sources[0], false);
1499 private String buildRecordResponse() {
1504 text = "REC " + getSourceLabel(recordSource, true);
1509 private String buildZonePowerResponse(int numZone) {
1512 zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1514 zone = String.format("ZONE%d", numZone);
1516 String state = powers[numZone] ? getSourceLabel(sources[numZone], true) : "OFF";
1517 return zone + " " + state;
1520 private String buildVolumeLine1Response() {
1522 if (volumes[0] == minVolume) {
1523 text = " VOLUME MIN ";
1524 } else if (volumes[0] == maxVolume) {
1525 text = " VOLUME MAX ";
1527 text = String.format(" VOLUME %02d ", volumes[0]);
1532 private String buildVolumeLine1RightResponse() {
1536 } else if (mutes[0]) {
1538 } else if (volumes[0] == minVolume) {
1540 } else if (volumes[0] == maxVolume) {
1543 text = String.format("VOL %02d", volumes[0]);
1548 private String buildZoneVolumeResponse(int numZone) {
1551 zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1553 zone = String.format("ZONE%d", numZone);
1556 if (mutes[numZone]) {
1557 text = zone + " MUTE ON";
1558 } else if (volumes[numZone] == minVolume) {
1559 text = zone + " VOL MIN";
1560 } else if (volumes[numZone] == maxVolume) {
1561 text = zone + " VOL MAX";
1563 text = String.format("%s VOL %02d", zone, volumes[numZone]);
1568 private String buildBassLine1Response() {
1570 if (basses[0] == minToneLevel) {
1571 text = " BASS MIN ";
1572 } else if (basses[0] == maxToneLevel) {
1573 text = " BASS MAX ";
1574 } else if (basses[0] == 0) {
1576 } else if (basses[0] > 0) {
1577 text = String.format(" BASS +%02d ", basses[0]);
1579 text = String.format(" BASS -%02d ", -basses[0]);
1584 private String buildBassLine1RightResponse() {
1586 if (basses[0] == minToneLevel) {
1588 } else if (basses[0] == maxToneLevel) {
1590 } else if (basses[0] == 0) {
1592 } else if (basses[0] > 0) {
1593 text = String.format("LF + %02d", basses[0]);
1595 text = String.format("LF - %02d", -basses[0]);
1600 private String buildTrebleLine1Response() {
1602 if (trebles[0] == minToneLevel) {
1603 text = " TREBLE MIN ";
1604 } else if (trebles[0] == maxToneLevel) {
1605 text = " TREBLE MAX ";
1606 } else if (trebles[0] == 0) {
1607 text = " TREBLE 0 ";
1608 } else if (trebles[0] > 0) {
1609 text = String.format(" TREBLE +%02d ", trebles[0]);
1611 text = String.format(" TREBLE -%02d ", -trebles[0]);
1616 private String buildTrebleLine1RightResponse() {
1618 if (trebles[0] == minToneLevel) {
1620 } else if (trebles[0] == maxToneLevel) {
1622 } else if (trebles[0] == 0) {
1624 } else if (trebles[0] > 0) {
1625 text = String.format("HF + %02d", trebles[0]);
1627 text = String.format("HF - %02d", -trebles[0]);
1632 private String getSourceLabel(RotelSource source, boolean considerFollowMain) {
1634 if (considerFollowMain && source.getName().equals(RotelSource.CAT1_FOLLOW_MAIN.getName())) {
1637 label = Objects.requireNonNullElse(sourcesLabels.get(source), source.getLabel());