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