]> git.basschouten.com Git - openhab-addons.git/blob
2fee5cf2d496e2c010641d7fa6a445ead6e95059
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.rotel.internal.communication;
14
15 import static org.openhab.binding.rotel.internal.RotelBindingConstants.*;
16 import static org.openhab.binding.rotel.internal.protocol.hex.RotelHexProtocolHandler.START;
17
18 import java.io.InterruptedIOException;
19 import java.nio.charset.StandardCharsets;
20 import java.util.Arrays;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Objects;
24 import java.util.StringJoiner;
25
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;
37
38 /**
39  * Class for simulating the communication with the Rotel device
40  *
41  * @author Laurent Garnier - Initial contribution
42  */
43 @NonNullByDefault
44 public class RotelSimuConnector extends RotelConnector {
45
46     private static final int STEP_TONE_LEVEL = 1;
47
48     private final Logger logger = LoggerFactory.getLogger(RotelSimuConnector.class);
49
50     private final RotelModel model;
51     private final RotelProtocol protocol;
52     private final Map<RotelSource, String> sourcesLabels;
53
54     private Object lock = new Object();
55
56     private byte[] feedbackMsg = new byte[1];
57     private int idxInFeedbackMsg = feedbackMsg.length;
58
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;
79     private int showZone;
80     private int dimmer;
81
82     private int minVolume;
83     private int maxVolume;
84     private int minToneLevel;
85     private int maxToneLevel;
86     private int minBalance;
87     private int maxBalance;
88
89     /**
90      * Constructor
91      *
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
96      */
97     public RotelSimuConnector(RotelModel model, RotelAbstractProtocolHandler protocolHandler,
98             Map<RotelSource, String> sourcesLabels, String readerThreadName) {
99         super(protocolHandler, true, readerThreadName);
100         this.model = model;
101         this.protocol = protocolHandler.getProtocol();
102         this.sourcesLabels = sourcesLabels;
103         this.minVolume = 0;
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;
113     }
114
115     @Override
116     public synchronized void open() throws RotelException {
117         logger.debug("Opening simulated connection");
118         readerThread.start();
119         setConnected(true);
120         logger.debug("Simulated connection opened");
121     }
122
123     @Override
124     public synchronized void close() {
125         logger.debug("Closing simulated connection");
126         super.cleanup();
127         setConnected(false);
128         logger.debug("Simulated connection closed");
129     }
130
131     @Override
132     protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
133         synchronized (lock) {
134             int len = feedbackMsg.length - idxInFeedbackMsg;
135             if (len > 0) {
136                 if (len > dataBuffer.length) {
137                     len = dataBuffer.length;
138                 }
139                 System.arraycopy(feedbackMsg, idxInFeedbackMsg, dataBuffer, 0, len);
140                 idxInFeedbackMsg += len;
141                 return len;
142             }
143         }
144         // Give more chance to someone else than the reader thread to get the lock
145         try {
146             Thread.sleep(20);
147         } catch (InterruptedException e) {
148             Thread.currentThread().interrupt();
149         }
150         return 0;
151     }
152
153     /**
154      * Built the simulated feedback message for a sent command
155      *
156      * @param cmd the sent command
157      * @param value the integer value considered in the sent command for volume, bass or treble adjustment
158      */
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;
167         int numZone = 0;
168         switch (cmd) {
169             case ZONE1_VOLUME_UP:
170             case ZONE1_VOLUME_DOWN:
171             case ZONE1_VOLUME_SET:
172             case ZONE1_MUTE_TOGGLE:
173             case ZONE1_MUTE_ON:
174             case ZONE1_MUTE_OFF:
175             case ZONE1_BASS_UP:
176             case ZONE1_BASS_DOWN:
177             case ZONE1_BASS_SET:
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:
184                 numZone = 1;
185                 break;
186             case ZONE2_POWER_OFF:
187             case ZONE2_POWER_ON:
188             case ZONE2_VOLUME_UP:
189             case ZONE2_VOLUME_DOWN:
190             case ZONE2_VOLUME_SET:
191             case ZONE2_MUTE_TOGGLE:
192             case ZONE2_MUTE_ON:
193             case ZONE2_MUTE_OFF:
194             case ZONE2_BASS_UP:
195             case ZONE2_BASS_DOWN:
196             case ZONE2_BASS_SET:
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:
203                 numZone = 2;
204                 break;
205             case ZONE3_POWER_OFF:
206             case ZONE3_POWER_ON:
207             case ZONE3_VOLUME_UP:
208             case ZONE3_VOLUME_DOWN:
209             case ZONE3_VOLUME_SET:
210             case ZONE3_MUTE_TOGGLE:
211             case ZONE3_MUTE_ON:
212             case ZONE3_MUTE_OFF:
213             case ZONE3_BASS_UP:
214             case ZONE3_BASS_DOWN:
215             case ZONE3_BASS_SET:
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:
222                 numZone = 3;
223                 break;
224             case ZONE4_POWER_OFF:
225             case ZONE4_POWER_ON:
226             case ZONE4_VOLUME_UP:
227             case ZONE4_VOLUME_DOWN:
228             case ZONE4_VOLUME_SET:
229             case ZONE4_MUTE_TOGGLE:
230             case ZONE4_MUTE_ON:
231             case ZONE4_MUTE_OFF:
232             case ZONE4_BASS_UP:
233             case ZONE4_BASS_DOWN:
234             case ZONE4_BASS_SET:
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:
241                 numZone = 4;
242                 break;
243             default:
244                 break;
245         }
246         switch (cmd) {
247             case DISPLAY_REFRESH:
248                 break;
249             case POWER_OFF:
250             case MAIN_ZONE_POWER_OFF:
251                 powers[0] = false;
252                 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
253                     for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
254                         powers[zone] = false;
255                     }
256                 }
257                 text = buildSourceLine1Response();
258                 textLine1Left = buildSourceLine1LeftResponse();
259                 textLine1Right = buildVolumeLine1RightResponse();
260                 textAscii = buildPowerAsciiResponse();
261                 break;
262             case POWER_ON:
263             case MAIN_ZONE_POWER_ON:
264                 powers[0] = true;
265                 if (model.getNumberOfZones() > 1 && !model.hasPowerControlPerZone()) {
266                     for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
267                         powers[zone] = true;
268                     }
269                 }
270                 text = buildSourceLine1Response();
271                 textLine1Left = buildSourceLine1LeftResponse();
272                 textLine1Right = buildVolumeLine1RightResponse();
273                 textAscii = buildPowerAsciiResponse();
274                 break;
275             case POWER:
276                 textAscii = buildPowerAsciiResponse();
277                 break;
278             case ZONE2_POWER_OFF:
279             case ZONE3_POWER_OFF:
280             case ZONE4_POWER_OFF:
281                 powers[numZone] = false;
282                 text = textLine2 = buildZonePowerResponse(numZone);
283                 showZone = numZone;
284                 resetZone = false;
285                 break;
286             case ZONE2_POWER_ON:
287             case ZONE3_POWER_ON:
288             case ZONE4_POWER_ON:
289                 powers[numZone] = true;
290                 text = textLine2 = buildZonePowerResponse(numZone);
291                 showZone = numZone;
292                 resetZone = false;
293                 break;
294             case RECORD_FONCTION_SELECT:
295                 if (model.getNumberOfZones() > 1 && model.getZoneSelectCmd() == cmd) {
296                     showZone++;
297                     if (showZone >= model.getNumberOfZones()) {
298                         showZone = 1;
299                         if (!powers[0]) {
300                             showZone++;
301                         }
302                     }
303                 } else {
304                     showZone = 1;
305                 }
306                 if (showZone == 1) {
307                     selectingRecord = powers[0];
308                     showTreble = false;
309                     textLine2 = buildRecordResponse();
310                 } else if (showZone >= 2 && showZone <= 4) {
311                     selectingRecord = false;
312                     text = textLine2 = buildZonePowerResponse(showZone);
313                 }
314                 resetZone = false;
315                 break;
316             case ZONE_SELECT:
317                 if (model.getNumberOfZones() == 1 || (model.getNumberOfZones() > 2 && model.getZoneSelectCmd() == cmd)
318                         || (showZone == 1 && model.getZoneSelectCmd() != cmd)) {
319                     accepted = false;
320                 } else {
321                     if (model.getZoneSelectCmd() == cmd) {
322                         if (!powers[0] && !powers[2]) {
323                             showZone = 2;
324                             powers[2] = true;
325                         } else if (showZone == 2) {
326                             powers[2] = !powers[2];
327                         } else {
328                             showZone = 2;
329                         }
330                     } else if (showZone >= 2 && showZone <= 4) {
331                         powers[showZone] = !powers[showZone];
332                     }
333                     if (showZone >= 2 && showZone <= 4) {
334                         text = textLine2 = buildZonePowerResponse(showZone);
335                     }
336                     resetZone = false;
337                 }
338                 break;
339             default:
340                 accepted = false;
341                 break;
342         }
343         if (!accepted && numZone > 0 && powers[numZone]) {
344             accepted = true;
345             switch (cmd) {
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) {
351                         volumes[numZone]++;
352                     }
353                     text = textLine2 = buildZoneVolumeResponse(numZone);
354                     textAscii = buildVolumeAsciiResponse();
355                     break;
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) {
361                         volumes[numZone]--;
362                     }
363                     text = textLine2 = buildZoneVolumeResponse(numZone);
364                     textAscii = buildVolumeAsciiResponse();
365                     break;
366                 case ZONE1_VOLUME_SET:
367                 case ZONE2_VOLUME_SET:
368                 case ZONE3_VOLUME_SET:
369                 case ZONE4_VOLUME_SET:
370                     if (value != null) {
371                         volumes[numZone] = value;
372                     }
373                     text = textLine2 = buildZoneVolumeResponse(numZone);
374                     textAscii = buildVolumeAsciiResponse();
375                     break;
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();
383                     break;
384                 case ZONE1_MUTE_ON:
385                 case ZONE2_MUTE_ON:
386                 case ZONE3_MUTE_ON:
387                 case ZONE4_MUTE_ON:
388                     mutes[numZone] = true;
389                     text = textLine2 = buildZoneVolumeResponse(numZone);
390                     textAscii = buildMuteAsciiResponse();
391                     break;
392                 case ZONE1_MUTE_OFF:
393                 case ZONE2_MUTE_OFF:
394                 case ZONE3_MUTE_OFF:
395                 case ZONE4_MUTE_OFF:
396                     mutes[numZone] = false;
397                     text = textLine2 = buildZoneVolumeResponse(numZone);
398                     textAscii = buildMuteAsciiResponse();
399                     break;
400                 case ZONE1_BASS_UP:
401                 case ZONE2_BASS_UP:
402                 case ZONE3_BASS_UP:
403                 case ZONE4_BASS_UP:
404                     if (!tcbypass && basses[numZone] < maxToneLevel) {
405                         basses[numZone] += STEP_TONE_LEVEL;
406                     }
407                     textAscii = buildBassAsciiResponse();
408                     break;
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;
415                     }
416                     textAscii = buildBassAsciiResponse();
417                     break;
418                 case ZONE1_BASS_SET:
419                 case ZONE2_BASS_SET:
420                 case ZONE3_BASS_SET:
421                 case ZONE4_BASS_SET:
422                     if (!tcbypass && value != null) {
423                         basses[numZone] = value;
424                     }
425                     textAscii = buildBassAsciiResponse();
426                     break;
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;
433                     }
434                     textAscii = buildTrebleAsciiResponse();
435                     break;
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;
442                     }
443                     textAscii = buildTrebleAsciiResponse();
444                     break;
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;
451                     }
452                     textAscii = buildTrebleAsciiResponse();
453                     break;
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) {
459                         balances[numZone]--;
460                     }
461                     textAscii = buildBalanceAsciiResponse();
462                     break;
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) {
468                         balances[numZone]++;
469                     }
470                     textAscii = buildBalanceAsciiResponse();
471                     break;
472                 case ZONE1_BALANCE_SET:
473                 case ZONE2_BALANCE_SET:
474                 case ZONE3_BALANCE_SET:
475                 case ZONE4_BALANCE_SET:
476                     if (value != null) {
477                         balances[numZone] = value;
478                     }
479                     textAscii = buildBalanceAsciiResponse();
480                     break;
481                 default:
482                     accepted = false;
483                     break;
484             }
485         }
486         if (!accepted) {
487             // Check if command is a change of source input for a zone
488             for (int zone = 1; zone <= model.getNumberOfZones(); zone++) {
489                 if (powers[zone]) {
490                     try {
491                         sources[zone] = model.getZoneSourceFromCommand(cmd, zone);
492                         text = textLine2 = buildZonePowerResponse(zone);
493                         textAscii = buildSourceAsciiResponse();
494                         mutes[zone] = false;
495                         accepted = true;
496                         showZone = zone;
497                         resetZone = false;
498                         break;
499                     } catch (RotelException e) {
500                     }
501                 }
502             }
503         }
504         if (!accepted && powers[2] && !model.hasZoneCommands(2) && model.getNumberOfZones() > 1 && showZone == 2) {
505             accepted = true;
506             switch (cmd) {
507                 case VOLUME_UP:
508                     if (volumes[2] < maxVolume) {
509                         volumes[2]++;
510                     }
511                     text = textLine2 = buildZoneVolumeResponse(2);
512                     resetZone = false;
513                     break;
514                 case VOLUME_DOWN:
515                     if (volumes[2] > minVolume) {
516                         volumes[2]--;
517                     }
518                     text = textLine2 = buildZoneVolumeResponse(2);
519                     resetZone = false;
520                     break;
521                 case VOLUME_SET:
522                     if (value != null) {
523                         volumes[2] = value;
524                     }
525                     text = textLine2 = buildZoneVolumeResponse(2);
526                     resetZone = false;
527                     break;
528                 default:
529                     accepted = false;
530                     break;
531             }
532             if (!accepted) {
533                 try {
534                     sources[2] = model.getSourceFromCommand(cmd);
535                     text = textLine2 = buildZonePowerResponse(2);
536                     mutes[2] = false;
537                     accepted = true;
538                     resetZone = false;
539                 } catch (RotelException e) {
540                 }
541             }
542         }
543         if (!accepted && powers[0]) {
544             accepted = true;
545             switch (cmd) {
546                 case UPDATE_AUTO:
547                     textAscii = buildAsciiResponse(
548                             protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, AUTO);
549                     break;
550                 case UPDATE_MANUAL:
551                     textAscii = buildAsciiResponse(
552                             protocol == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, MANUAL);
553                     break;
554                 case VOLUME_GET_MIN:
555                     textAscii = buildAsciiResponse(KEY_VOLUME_MIN, minVolume);
556                     break;
557                 case VOLUME_GET_MAX:
558                     textAscii = buildAsciiResponse(KEY_VOLUME_MAX, maxVolume);
559                     break;
560                 case VOLUME_UP:
561                 case MAIN_ZONE_VOLUME_UP:
562                     if (volumes[0] < maxVolume) {
563                         volumes[0]++;
564                     }
565                     text = buildVolumeLine1Response();
566                     textLine1Right = buildVolumeLine1RightResponse();
567                     textAscii = buildVolumeAsciiResponse();
568                     break;
569                 case VOLUME_DOWN:
570                 case MAIN_ZONE_VOLUME_DOWN:
571                     if (volumes[0] > minVolume) {
572                         volumes[0]--;
573                     }
574                     text = buildVolumeLine1Response();
575                     textLine1Right = buildVolumeLine1RightResponse();
576                     textAscii = buildVolumeAsciiResponse();
577                     break;
578                 case VOLUME_SET:
579                     if (value != null) {
580                         volumes[0] = value;
581                     }
582                     text = buildVolumeLine1Response();
583                     textLine1Right = buildVolumeLine1RightResponse();
584                     textAscii = buildVolumeAsciiResponse();
585                     break;
586                 case VOLUME_GET:
587                     textAscii = buildVolumeAsciiResponse();
588                     break;
589                 case MUTE_TOGGLE:
590                 case MAIN_ZONE_MUTE_TOGGLE:
591                     mutes[0] = !mutes[0];
592                     text = buildSourceLine1Response();
593                     textLine1Right = buildVolumeLine1RightResponse();
594                     textAscii = buildMuteAsciiResponse();
595                     break;
596                 case MUTE_ON:
597                 case MAIN_ZONE_MUTE_ON:
598                     mutes[0] = true;
599                     text = buildSourceLine1Response();
600                     textLine1Right = buildVolumeLine1RightResponse();
601                     textAscii = buildMuteAsciiResponse();
602                     break;
603                 case MUTE_OFF:
604                 case MAIN_ZONE_MUTE_OFF:
605                     mutes[0] = false;
606                     text = buildSourceLine1Response();
607                     textLine1Right = buildVolumeLine1RightResponse();
608                     textAscii = buildMuteAsciiResponse();
609                     break;
610                 case MUTE:
611                     textAscii = buildMuteAsciiResponse();
612                     break;
613                 case TONE_MAX:
614                     textAscii = buildAsciiResponse(KEY_TONE_MAX, String.format("%02d", maxToneLevel));
615                     break;
616                 case TONE_CONTROLS_ON:
617                     tcbypass = false;
618                     textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
619                     break;
620                 case TONE_CONTROLS_OFF:
621                     tcbypass = true;
622                     textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
623                     break;
624                 case TONE_CONTROLS:
625                     textAscii = buildAsciiResponse(KEY_TONE, !tcbypass);
626                     break;
627                 case TCBYPASS_ON:
628                     tcbypass = true;
629                     textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
630                     break;
631                 case TCBYPASS_OFF:
632                     tcbypass = false;
633                     textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
634                     break;
635                 case TCBYPASS:
636                     textAscii = buildAsciiResponse(KEY_TCBYPASS, tcbypass);
637                     break;
638                 case BASS_UP:
639                     if (!tcbypass && basses[0] < maxToneLevel) {
640                         basses[0] += STEP_TONE_LEVEL;
641                     }
642                     text = buildBassLine1Response();
643                     textLine1Right = buildBassLine1RightResponse();
644                     textAscii = buildBassAsciiResponse();
645                     break;
646                 case BASS_DOWN:
647                     if (!tcbypass && basses[0] > minToneLevel) {
648                         basses[0] -= STEP_TONE_LEVEL;
649                     }
650                     text = buildBassLine1Response();
651                     textLine1Right = buildBassLine1RightResponse();
652                     textAscii = buildBassAsciiResponse();
653                     break;
654                 case BASS_SET:
655                     if (!tcbypass && value != null) {
656                         basses[0] = value;
657                     }
658                     text = buildBassLine1Response();
659                     textLine1Right = buildBassLine1RightResponse();
660                     textAscii = buildBassAsciiResponse();
661                     break;
662                 case BASS:
663                     textAscii = buildBassAsciiResponse();
664                     break;
665                 case TREBLE_UP:
666                     if (!tcbypass && trebles[0] < maxToneLevel) {
667                         trebles[0] += STEP_TONE_LEVEL;
668                     }
669                     text = buildTrebleLine1Response();
670                     textLine1Right = buildTrebleLine1RightResponse();
671                     textAscii = buildTrebleAsciiResponse();
672                     break;
673                 case TREBLE_DOWN:
674                     if (!tcbypass && trebles[0] > minToneLevel) {
675                         trebles[0] -= STEP_TONE_LEVEL;
676                     }
677                     text = buildTrebleLine1Response();
678                     textLine1Right = buildTrebleLine1RightResponse();
679                     textAscii = buildTrebleAsciiResponse();
680                     break;
681                 case TREBLE_SET:
682                     if (!tcbypass && value != null) {
683                         trebles[0] = value;
684                     }
685                     text = buildTrebleLine1Response();
686                     textLine1Right = buildTrebleLine1RightResponse();
687                     textAscii = buildTrebleAsciiResponse();
688                     break;
689                 case TREBLE:
690                     textAscii = buildTrebleAsciiResponse();
691                     break;
692                 case TONE_CONTROL_SELECT:
693                     showTreble = !showTreble;
694                     if (showTreble) {
695                         text = buildTrebleLine1Response();
696                         textLine1Right = buildTrebleLine1RightResponse();
697                     } else {
698                         text = buildBassLine1Response();
699                         textLine1Right = buildBassLine1RightResponse();
700                     }
701                     break;
702                 case BALANCE_LEFT:
703                     if (balances[0] > minBalance) {
704                         balances[0]--;
705                     }
706                     textAscii = buildBalanceAsciiResponse();
707                     break;
708                 case BALANCE_RIGHT:
709                     if (balances[0] < maxBalance) {
710                         balances[0]++;
711                     }
712                     textAscii = buildBalanceAsciiResponse();
713                     break;
714                 case BALANCE_SET:
715                     if (value != null) {
716                         balances[0] = value;
717                     }
718                     textAscii = buildBalanceAsciiResponse();
719                     break;
720                 case BALANCE:
721                     textAscii = buildBalanceAsciiResponse();
722                     break;
723                 case SPEAKER_A_TOGGLE:
724                     speakerA = !speakerA;
725                     textAscii = buildSpeakerAsciiResponse();
726                     break;
727                 case SPEAKER_A_ON:
728                     speakerA = true;
729                     textAscii = buildSpeakerAsciiResponse();
730                     break;
731                 case SPEAKER_A_OFF:
732                     speakerA = false;
733                     textAscii = buildSpeakerAsciiResponse();
734                     break;
735                 case SPEAKER_B_TOGGLE:
736                     speakerB = !speakerB;
737                     textAscii = buildSpeakerAsciiResponse();
738                     break;
739                 case SPEAKER_B_ON:
740                     speakerB = true;
741                     textAscii = buildSpeakerAsciiResponse();
742                     break;
743                 case SPEAKER_B_OFF:
744                     speakerB = false;
745                     textAscii = buildSpeakerAsciiResponse();
746                     break;
747                 case SPEAKER:
748                     textAscii = buildSpeakerAsciiResponse();
749                     break;
750                 case PLAY:
751                     playStatus = RotelPlayStatus.PLAYING;
752                     textAscii = buildPlayStatusAsciiResponse();
753                     break;
754                 case STOP:
755                     playStatus = RotelPlayStatus.STOPPED;
756                     textAscii = buildPlayStatusAsciiResponse();
757                     break;
758                 case PAUSE:
759                     switch (playStatus) {
760                         case PLAYING:
761                             playStatus = RotelPlayStatus.PAUSED;
762                             break;
763                         case PAUSED:
764                         case STOPPED:
765                             playStatus = RotelPlayStatus.PLAYING;
766                             break;
767                     }
768                     textAscii = buildPlayStatusAsciiResponse();
769                     break;
770                 case CD_PLAY_STATUS:
771                 case PLAY_STATUS:
772                     textAscii = buildPlayStatusAsciiResponse();
773                     break;
774                 case TRACK_FWD:
775                     track++;
776                     textAscii = buildTrackAsciiResponse();
777                     break;
778                 case TRACK_BACK:
779                     if (track > 1) {
780                         track--;
781                     }
782                     textAscii = buildTrackAsciiResponse();
783                     break;
784                 case TRACK:
785                     textAscii = buildTrackAsciiResponse();
786                     break;
787                 case RANDOM_TOGGLE:
788                     randomMode = !randomMode;
789                     textAscii = buildRandomModeAsciiResponse();
790                     break;
791                 case RANDOM_MODE:
792                     textAscii = buildRandomModeAsciiResponse();
793                     break;
794                 case REPEAT_TOGGLE:
795                     switch (repeatMode) {
796                         case TRACK:
797                             repeatMode = RotelRepeatMode.DISC;
798                             break;
799                         case DISC:
800                             repeatMode = RotelRepeatMode.OFF;
801                             break;
802                         case OFF:
803                             repeatMode = RotelRepeatMode.TRACK;
804                             break;
805                     }
806                     textAscii = buildRepeatModeAsciiResponse();
807                     break;
808                 case REPEAT_MODE:
809                     textAscii = buildRepeatModeAsciiResponse();
810                     break;
811                 case SOURCE_MULTI_INPUT:
812                     multiinput = !multiinput;
813                     text = "MULTI IN " + (multiinput ? "ON" : "OFF");
814                     try {
815                         sources[0] = model.getSourceFromCommand(cmd);
816                         textLine1Left = buildSourceLine1LeftResponse();
817                         textAscii = buildSourceAsciiResponse();
818                         mutes[0] = false;
819                     } catch (RotelException e) {
820                     }
821                     break;
822                 case SOURCE:
823                 case INPUT:
824                     textAscii = buildSourceAsciiResponse();
825                     break;
826                 case STEREO:
827                     dsp = RotelDsp.CAT4_NONE;
828                     textLine2 = bypass ? "BYPASS" : "STEREO";
829                     textAscii = buildDspAsciiResponse();
830                     break;
831                 case STEREO3:
832                     dsp = RotelDsp.CAT4_STEREO3;
833                     textLine2 = "DOLBY 3 STEREO";
834                     textAscii = buildDspAsciiResponse();
835                     break;
836                 case STEREO5:
837                     dsp = RotelDsp.CAT4_STEREO5;
838                     textLine2 = "5CH STEREO";
839                     textAscii = buildDspAsciiResponse();
840                     break;
841                 case STEREO7:
842                     dsp = RotelDsp.CAT4_STEREO7;
843                     textLine2 = "7CH STEREO";
844                     textAscii = buildDspAsciiResponse();
845                     break;
846                 case STEREO9:
847                     dsp = RotelDsp.CAT5_STEREO9;
848                     textAscii = buildDspAsciiResponse();
849                     break;
850                 case STEREO11:
851                     dsp = RotelDsp.CAT5_STEREO11;
852                     textAscii = buildDspAsciiResponse();
853                     break;
854                 case DSP1:
855                     dsp = RotelDsp.CAT4_DSP1;
856                     textLine2 = "DSP 1";
857                     textAscii = buildDspAsciiResponse();
858                     break;
859                 case DSP2:
860                     dsp = RotelDsp.CAT4_DSP2;
861                     textLine2 = "DSP 2";
862                     textAscii = buildDspAsciiResponse();
863                     break;
864                 case DSP3:
865                     dsp = RotelDsp.CAT4_DSP3;
866                     textLine2 = "DSP 3";
867                     textAscii = buildDspAsciiResponse();
868                     break;
869                 case DSP4:
870                     dsp = RotelDsp.CAT4_DSP4;
871                     textLine2 = "DSP 4";
872                     textAscii = buildDspAsciiResponse();
873                     break;
874                 case PROLOGIC:
875                     dsp = RotelDsp.CAT4_PROLOGIC;
876                     textLine2 = "DOLBY PRO LOGIC";
877                     textAscii = buildDspAsciiResponse();
878                     break;
879                 case PLII_CINEMA:
880                     dsp = RotelDsp.CAT4_PLII_CINEMA;
881                     textLine2 = "DOLBY PL  C";
882                     textAscii = buildDspAsciiResponse();
883                     break;
884                 case PLII_MUSIC:
885                     dsp = RotelDsp.CAT4_PLII_MUSIC;
886                     textLine2 = "DOLBY PL  M";
887                     textAscii = buildDspAsciiResponse();
888                     break;
889                 case PLII_GAME:
890                     dsp = RotelDsp.CAT4_PLII_GAME;
891                     textLine2 = "DOLBY PL  G";
892                     textAscii = buildDspAsciiResponse();
893                     break;
894                 case PLIIZ:
895                     dsp = RotelDsp.CAT4_PLIIZ;
896                     textLine2 = "DOLBY PL z";
897                     textAscii = buildDspAsciiResponse();
898                     break;
899                 case NEO6_MUSIC:
900                     dsp = RotelDsp.CAT4_NEO6_MUSIC;
901                     textLine2 = "DTS Neo:6 M";
902                     textAscii = buildDspAsciiResponse();
903                     break;
904                 case NEO6_CINEMA:
905                     dsp = RotelDsp.CAT4_NEO6_CINEMA;
906                     textLine2 = "DTS Neo:6 C";
907                     textAscii = buildDspAsciiResponse();
908                     break;
909                 case ATMOS:
910                     dsp = RotelDsp.CAT5_ATMOS;
911                     textAscii = buildDspAsciiResponse();
912                     break;
913                 case NEURAL_X:
914                     dsp = RotelDsp.CAT5_NEURAL_X;
915                     textAscii = buildDspAsciiResponse();
916                     break;
917                 case BYPASS:
918                     dsp = RotelDsp.CAT5_BYPASS;
919                     textAscii = buildDspAsciiResponse();
920                     break;
921                 case DSP_MODE:
922                     textAscii = buildDspAsciiResponse();
923                     break;
924                 case STEREO_BYPASS_TOGGLE:
925                     bypass = !bypass;
926                     textLine2 = bypass ? "BYPASS" : "STEREO";
927                     break;
928                 case FREQUENCY:
929                     textAscii = model.getNumberOfZones() > 1 ? buildAsciiResponse(KEY_FREQ, "44.1,48,none,176.4")
930                             : buildAsciiResponse(KEY_FREQ, "44.1");
931                     break;
932                 case DIMMER_LEVEL_SET:
933                     if (value != null) {
934                         dimmer = value;
935                     }
936                     textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
937                     break;
938                 case DIMMER_LEVEL_GET:
939                     textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
940                     break;
941                 case MODEL:
942                     textAscii = buildAsciiResponse(KEY_MODEL, model.getName());
943                     break;
944                 case VERSION:
945                     textAscii = buildAsciiResponse(KEY_VERSION, "1.00");
946                     break;
947                 default:
948                     accepted = false;
949                     break;
950             }
951             if (!accepted) {
952                 // Check if command is a change of source input for the main zone
953                 try {
954                     sources[0] = model.getZoneSourceFromCommand(cmd, 1);
955                     text = buildSourceLine1Response();
956                     textLine1Left = buildSourceLine1LeftResponse();
957                     textAscii = buildSourceAsciiResponse();
958                     accepted = true;
959                 } catch (RotelException e) {
960                 }
961             }
962             if (!accepted) {
963                 // Check if command is a change of source input
964                 try {
965                     if (selectingRecord && !model.hasOtherThanPrimaryCommands()) {
966                         recordSource = model.getSourceFromCommand(cmd);
967                     } else {
968                         sources[0] = model.getSourceFromCommand(cmd);
969                     }
970                     text = buildSourceLine1Response();
971                     textLine1Left = buildSourceLine1LeftResponse();
972                     textAscii = buildSourceAsciiResponse();
973                     mutes[0] = false;
974                     accepted = true;
975                 } catch (RotelException e) {
976                 }
977             }
978             if (!accepted) {
979                 // Check if command is a change of record source
980                 try {
981                     recordSource = model.getRecordSourceFromCommand(cmd);
982                     text = buildSourceLine1Response();
983                     textLine2 = buildRecordResponse();
984                     accepted = true;
985                 } catch (RotelException e) {
986                 }
987             }
988         }
989
990         if (!accepted) {
991             return;
992         }
993
994         if (cmd != RotelCommand.RECORD_FONCTION_SELECT) {
995             selectingRecord = false;
996         }
997         if (resetZone) {
998             showZone = 0;
999         }
1000
1001         if (model.getRespNbChars() == 42) {
1002             while (textLine1Left.length() < 14) {
1003                 textLine1Left += " ";
1004             }
1005             while (textLine1Right.length() < 7) {
1006                 textLine1Right += " ";
1007             }
1008             while (textLine2.length() < 21) {
1009                 textLine2 += " ";
1010             }
1011             text = textLine1Left + textLine1Right + textLine2;
1012         }
1013
1014         if (protocol == RotelProtocol.HEX) {
1015             byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), model.getRespNbChars());
1016             byte[] flags = new byte[model.getRespNbFlags()];
1017             try {
1018                 model.setMultiInput(flags, multiinput);
1019             } catch (RotelException e) {
1020             }
1021             try {
1022                 model.setZone2(flags, powers[2]);
1023             } catch (RotelException e) {
1024             }
1025             try {
1026                 model.setZone3(flags, powers[3]);
1027             } catch (RotelException e) {
1028             }
1029             try {
1030                 model.setZone4(flags, powers[4]);
1031             } catch (RotelException e) {
1032             }
1033             int size = 6 + model.getRespNbChars() + model.getRespNbFlags();
1034             byte[] dataBuffer = new byte[size];
1035             int idx = 0;
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();
1045             } else {
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();
1050             }
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;
1058             } else {
1059                 dataBuffer[idx++] = checksum;
1060             }
1061             synchronized (lock) {
1062                 feedbackMsg = Arrays.copyOf(dataBuffer, idx);
1063                 idxInFeedbackMsg = 0;
1064             }
1065         } else {
1066             String command = textAscii + (protocol == RotelProtocol.ASCII_V1 ? "!" : "$");
1067             synchronized (lock) {
1068                 feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
1069                 idxInFeedbackMsg = 0;
1070             }
1071         }
1072     }
1073
1074     private String buildAsciiResponse(String key, String value) {
1075         return String.format("%s=%s", key, value);
1076     }
1077
1078     private String buildAsciiResponse(String key, int value) {
1079         return String.format("%s=%d", key, value);
1080     }
1081
1082     private String buildAsciiResponse(String key, boolean value) {
1083         return buildAsciiResponse(key, buildOnOffValue(value));
1084     }
1085
1086     private String buildOnOffValue(boolean on) {
1087         return on ? MSG_VALUE_ON : MSG_VALUE_OFF;
1088     }
1089
1090     private String buildPowerAsciiResponse() {
1091         return buildAsciiResponse(KEY_POWER, powers[0] ? POWER_ON : STANDBY);
1092     }
1093
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]));
1099             }
1100             return buildAsciiResponse(KEY_VOLUME, sj.toString());
1101         } else {
1102             return buildAsciiResponse(KEY_VOLUME, String.format("%02d", volumes[0]));
1103         }
1104     }
1105
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]));
1111             }
1112             return buildAsciiResponse(KEY_MUTE, sj.toString());
1113         } else {
1114             return buildAsciiResponse(KEY_MUTE, mutes[0]);
1115         }
1116     }
1117
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]));
1123             }
1124             return buildAsciiResponse(KEY_BASS, sj.toString());
1125         } else {
1126             return buildAsciiResponse(KEY_BASS, buildBassTrebleValue(basses[0]));
1127         }
1128     }
1129
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]));
1135             }
1136             return buildAsciiResponse(KEY_TREBLE, sj.toString());
1137         } else {
1138             return buildAsciiResponse(KEY_TREBLE, buildBassTrebleValue(trebles[0]));
1139         }
1140     }
1141
1142     private String buildBassTrebleValue(int value) {
1143         if (tcbypass || value == 0) {
1144             return "000";
1145         } else if (value > 0) {
1146             return String.format("+%02d", value);
1147         } else {
1148             return String.format("-%02d", -value);
1149         }
1150     }
1151
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]));
1157             }
1158             return buildAsciiResponse(KEY_BALANCE, sj.toString());
1159         } else {
1160             return buildAsciiResponse(KEY_BALANCE, buildBalanceValue(balances[0]));
1161         }
1162     }
1163
1164     private String buildBalanceValue(int value) {
1165         if (value == 0) {
1166             return "000";
1167         } else if (value > 0) {
1168             return String.format("r%02d", value);
1169         } else {
1170             return String.format("l%02d", -value);
1171         }
1172     }
1173
1174     private String buildSpeakerAsciiResponse() {
1175         String value;
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;
1182         } else {
1183             value = MSG_VALUE_OFF;
1184         }
1185         return buildAsciiResponse(KEY_SPEAKER, value);
1186     }
1187
1188     private String buildPlayStatusAsciiResponse() {
1189         String status = "";
1190         switch (playStatus) {
1191             case PLAYING:
1192                 status = PLAY;
1193                 break;
1194             case PAUSED:
1195                 status = PAUSE;
1196                 break;
1197             case STOPPED:
1198                 status = STOP;
1199                 break;
1200         }
1201         return buildAsciiResponse(protocol == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_STATUS, status);
1202     }
1203
1204     private String buildTrackAsciiResponse() {
1205         return buildAsciiResponse(KEY_TRACK, String.format("%03d", track));
1206     }
1207
1208     private String buildRandomModeAsciiResponse() {
1209         return buildAsciiResponse(KEY_RANDOM, randomMode);
1210     }
1211
1212     private String buildRepeatModeAsciiResponse() {
1213         String mode = "";
1214         switch (repeatMode) {
1215             case TRACK:
1216                 mode = TRACK;
1217                 break;
1218             case DISC:
1219                 mode = DISC;
1220                 break;
1221             case OFF:
1222                 mode = MSG_VALUE_OFF;
1223                 break;
1224         }
1225         return buildAsciiResponse(KEY_REPEAT, mode);
1226     }
1227
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]));
1233             }
1234             return buildAsciiResponse(KEY_INPUT, sj.toString());
1235         } else {
1236             return buildAsciiResponse(KEY_SOURCE, buildSourceValue(sources[0]));
1237         }
1238     }
1239
1240     private String buildSourceValue(RotelSource source) {
1241         String str = null;
1242         RotelCommand command = source.getCommand();
1243         if (command != null) {
1244             str = protocol == RotelProtocol.ASCII_V1 ? command.getAsciiCommandV1() : command.getAsciiCommandV2();
1245         }
1246         return str == null ? "" : str;
1247     }
1248
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);
1253     }
1254
1255     private String buildDspAsciiResponse() {
1256         return buildAsciiResponse(KEY_DSP_MODE, dsp.getFeedback());
1257     }
1258
1259     private String buildSourceLine1Response() {
1260         String text;
1261         if (!powers[0]) {
1262             text = "";
1263         } else if (mutes[0]) {
1264             text = "MUTE ON";
1265         } else {
1266             text = getSourceLabel(sources[0], false) + " " + getSourceLabel(recordSource, true);
1267         }
1268         return text;
1269     }
1270
1271     private String buildSourceLine1LeftResponse() {
1272         String text;
1273         if (!powers[0]) {
1274             text = "";
1275         } else {
1276             text = getSourceLabel(sources[0], false);
1277         }
1278         return text;
1279     }
1280
1281     private String buildRecordResponse() {
1282         String text;
1283         if (!powers[0]) {
1284             text = "";
1285         } else {
1286             text = "REC " + getSourceLabel(recordSource, true);
1287         }
1288         return text;
1289     }
1290
1291     private String buildZonePowerResponse(int numZone) {
1292         String zone;
1293         if (numZone == 2) {
1294             zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1295         } else {
1296             zone = String.format("ZONE%d", numZone);
1297         }
1298         String state = powers[numZone] ? getSourceLabel(sources[numZone], true) : "OFF";
1299         return zone + " " + state;
1300     }
1301
1302     private String buildVolumeLine1Response() {
1303         String text;
1304         if (volumes[0] == minVolume) {
1305             text = " VOLUME  MIN ";
1306         } else if (volumes[0] == maxVolume) {
1307             text = " VOLUME  MAX ";
1308         } else {
1309             text = String.format(" VOLUME   %02d ", volumes[0]);
1310         }
1311         return text;
1312     }
1313
1314     private String buildVolumeLine1RightResponse() {
1315         String text;
1316         if (!powers[0]) {
1317             text = "";
1318         } else if (mutes[0]) {
1319             text = "MUTE ON";
1320         } else if (volumes[0] == minVolume) {
1321             text = "VOL MIN";
1322         } else if (volumes[0] == maxVolume) {
1323             text = "VOL MAX";
1324         } else {
1325             text = String.format("VOL  %02d", volumes[0]);
1326         }
1327         return text;
1328     }
1329
1330     private String buildZoneVolumeResponse(int numZone) {
1331         String zone;
1332         if (numZone == 2) {
1333             zone = model.getNumberOfZones() > 2 ? "ZONE2" : "ZONE";
1334         } else {
1335             zone = String.format("ZONE%d", numZone);
1336         }
1337         String text;
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";
1344         } else {
1345             text = String.format("%s VOL %02d", zone, volumes[numZone]);
1346         }
1347         return text;
1348     }
1349
1350     private String buildBassLine1Response() {
1351         String text;
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) {
1357             text = "   BASS    0 ";
1358         } else if (basses[0] > 0) {
1359             text = String.format("   BASS  +%02d ", basses[0]);
1360         } else {
1361             text = String.format("   BASS  -%02d ", -basses[0]);
1362         }
1363         return text;
1364     }
1365
1366     private String buildBassLine1RightResponse() {
1367         String text;
1368         if (basses[0] == minToneLevel) {
1369             text = "LF  MIN";
1370         } else if (basses[0] == maxToneLevel) {
1371             text = "LF  MAX";
1372         } else if (basses[0] == 0) {
1373             text = "LF    0";
1374         } else if (basses[0] > 0) {
1375             text = String.format("LF + %02d", basses[0]);
1376         } else {
1377             text = String.format("LF - %02d", -basses[0]);
1378         }
1379         return text;
1380     }
1381
1382     private String buildTrebleLine1Response() {
1383         String text;
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]);
1392         } else {
1393             text = String.format(" TREBLE  -%02d ", -trebles[0]);
1394         }
1395         return text;
1396     }
1397
1398     private String buildTrebleLine1RightResponse() {
1399         String text;
1400         if (trebles[0] == minToneLevel) {
1401             text = "HF  MIN";
1402         } else if (trebles[0] == maxToneLevel) {
1403             text = "HF  MAX";
1404         } else if (trebles[0] == 0) {
1405             text = "HF    0";
1406         } else if (trebles[0] > 0) {
1407             text = String.format("HF + %02d", trebles[0]);
1408         } else {
1409             text = String.format("HF - %02d", -trebles[0]);
1410         }
1411         return text;
1412     }
1413
1414     private String getSourceLabel(RotelSource source, boolean considerFollowMain) {
1415         String label;
1416         if (considerFollowMain && source.getName().equals(RotelSource.CAT1_FOLLOW_MAIN.getName())) {
1417             label = "SOURCE";
1418         } else {
1419             label = Objects.requireNonNullElse(sourcesLabels.get(source), source.getLabel());
1420         }
1421
1422         return label;
1423     }
1424 }