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