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