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