]> git.basschouten.com Git - openhab-addons.git/blob
4f3e543f58ba6602173d3a574def1b03ca5defbe
[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 java.io.InterruptedIOException;
16 import java.nio.charset.StandardCharsets;
17 import java.util.Arrays;
18 import java.util.Map;
19 import java.util.Objects;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.rotel.internal.RotelException;
24 import org.openhab.binding.rotel.internal.RotelModel;
25 import org.openhab.binding.rotel.internal.RotelPlayStatus;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28
29 /**
30  * Class for simulating the communication with the Rotel device
31  *
32  * @author Laurent Garnier - Initial contribution
33  */
34 @NonNullByDefault
35 public class RotelSimuConnector extends RotelConnector {
36
37     private final Logger logger = LoggerFactory.getLogger(RotelSimuConnector.class);
38
39     private static final int STEP_TONE_LEVEL = 1;
40
41     private Object lock = new Object();
42
43     private byte[] feedbackMsg = new byte[1];
44     private int idxInFeedbackMsg = feedbackMsg.length;
45
46     private boolean power;
47     private boolean powerZone2;
48     private boolean powerZone3;
49     private boolean powerZone4;
50     private RotelSource source = RotelSource.CAT0_CD;
51     private RotelSource recordSource = RotelSource.CAT1_CD;
52     private RotelSource sourceZone2 = RotelSource.CAT1_CD;
53     private RotelSource sourceZone3 = RotelSource.CAT1_CD;
54     private RotelSource sourceZone4 = RotelSource.CAT1_CD;
55     private boolean multiinput;
56     private RotelDsp dsp = RotelDsp.CAT4_NONE;
57     private int volume = 50;
58     private boolean mute;
59     private int volumeZone2 = 20;
60     private boolean muteZone2;
61     private int volumeZone3 = 30;
62     private boolean muteZone3;
63     private int volumeZone4 = 40;
64     private boolean muteZone4;
65     private int bass;
66     private int treble;
67     private boolean showTreble;
68     private RotelPlayStatus playStatus = RotelPlayStatus.STOPPED;
69     private int track = 1;
70     private boolean selectingRecord;
71     private int showZone;
72     private int dimmer;
73
74     private int minVolume;
75     private int maxVolume;
76     private int minToneLevel;
77     private int maxToneLevel;
78
79     /**
80      * Constructor
81      *
82      * @param model the projector model in use
83      * @param protocol the protocol to be used
84      * @param readerThreadName the name of thread to be created
85      */
86     public RotelSimuConnector(RotelModel model, RotelProtocol protocol, Map<RotelSource, String> sourcesLabels,
87             String readerThreadName) {
88         super(model, protocol, sourcesLabels, true, readerThreadName);
89         this.minVolume = 0;
90         this.maxVolume = model.hasVolumeControl() ? model.getVolumeMax() : 0;
91         this.maxToneLevel = model.hasToneControl() ? model.getToneLevelMax() : 0;
92         this.minToneLevel = -this.maxToneLevel;
93     }
94
95     @Override
96     public synchronized void open() throws RotelException {
97         logger.debug("Opening simulated connection");
98         Thread thread = new RotelReaderThread(this, readerThreadName);
99         setReaderThread(thread);
100         thread.start();
101         setConnected(true);
102         logger.debug("Simulated connection opened");
103     }
104
105     @Override
106     public synchronized void close() {
107         logger.debug("Closing simulated connection");
108         super.cleanup();
109         setConnected(false);
110         logger.debug("Simulated connection closed");
111     }
112
113     @Override
114     protected int readInput(byte[] dataBuffer) throws RotelException, InterruptedIOException {
115         synchronized (lock) {
116             int len = feedbackMsg.length - idxInFeedbackMsg;
117             if (len > 0) {
118                 if (len > dataBuffer.length) {
119                     len = dataBuffer.length;
120                 }
121                 System.arraycopy(feedbackMsg, idxInFeedbackMsg, dataBuffer, 0, len);
122                 idxInFeedbackMsg += len;
123                 return len;
124             }
125         }
126         // Give more chance to someone else than the reader thread to get the lock
127         try {
128             Thread.sleep(20);
129         } catch (InterruptedException e) {
130             Thread.currentThread().interrupt();
131         }
132         return 0;
133     }
134
135     @Override
136     public void sendCommand(RotelCommand cmd, @Nullable Integer value) throws RotelException {
137         super.sendCommand(cmd, value);
138         if ((getProtocol() == RotelProtocol.HEX && cmd.getHexType() != 0)
139                 || (getProtocol() == RotelProtocol.ASCII_V1 && cmd.getAsciiCommandV1() != null)
140                 || (getProtocol() == RotelProtocol.ASCII_V2 && cmd.getAsciiCommandV2() != null)) {
141             buildFeedbackMessage(cmd, value);
142         }
143     }
144
145     /**
146      * Built the simulated feedback message for a sent command
147      *
148      * @param cmd the sent command
149      * @param value the integer value considered in the sent command for volume, bass or treble adjustment
150      */
151     private void buildFeedbackMessage(RotelCommand cmd, @Nullable Integer value) {
152         String text = buildSourceLine1Response();
153         String textLine1Left = buildSourceLine1LeftResponse();
154         String textLine1Right = buildVolumeLine1RightResponse();
155         String textLine2 = "";
156         String textAscii = "";
157         boolean accepted = true;
158         boolean resetZone = true;
159         switch (cmd) {
160             case DISPLAY_REFRESH:
161                 break;
162             case POWER_OFF:
163             case MAIN_ZONE_POWER_OFF:
164                 power = false;
165                 text = buildSourceLine1Response();
166                 textLine1Left = buildSourceLine1LeftResponse();
167                 textLine1Right = buildVolumeLine1RightResponse();
168                 textAscii = buildPowerAsciiResponse();
169                 break;
170             case POWER_ON:
171             case MAIN_ZONE_POWER_ON:
172                 power = true;
173                 text = buildSourceLine1Response();
174                 textLine1Left = buildSourceLine1LeftResponse();
175                 textLine1Right = buildVolumeLine1RightResponse();
176                 textAscii = buildPowerAsciiResponse();
177                 break;
178             case POWER:
179                 textAscii = buildPowerAsciiResponse();
180                 break;
181             case ZONE2_POWER_OFF:
182                 powerZone2 = false;
183                 text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
184                         powerZone2, sourceZone2);
185                 showZone = 2;
186                 resetZone = false;
187                 break;
188             case ZONE2_POWER_ON:
189                 powerZone2 = true;
190                 text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
191                         powerZone2, sourceZone2);
192                 showZone = 2;
193                 resetZone = false;
194                 break;
195             case ZONE3_POWER_OFF:
196                 powerZone3 = false;
197                 text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
198                 showZone = 3;
199                 resetZone = false;
200                 break;
201             case ZONE3_POWER_ON:
202                 powerZone3 = true;
203                 text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
204                 showZone = 3;
205                 resetZone = false;
206                 break;
207             case ZONE4_POWER_OFF:
208                 powerZone4 = false;
209                 text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
210                 showZone = 4;
211                 resetZone = false;
212                 break;
213             case ZONE4_POWER_ON:
214                 powerZone4 = true;
215                 text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
216                 showZone = 4;
217                 resetZone = false;
218                 break;
219             case RECORD_FONCTION_SELECT:
220                 if (getModel().getNbAdditionalZones() >= 1 && getModel().getZoneSelectCmd() == cmd) {
221                     showZone++;
222                     if (showZone > getModel().getNbAdditionalZones()) {
223                         showZone = 1;
224                         if (!power) {
225                             showZone++;
226                         }
227                     }
228                 } else {
229                     showZone = 1;
230                 }
231                 if (showZone == 1) {
232                     selectingRecord = power;
233                     showTreble = false;
234                     textLine2 = buildRecordResponse();
235                 } else if (showZone == 2) {
236                     selectingRecord = false;
237                     text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
238                             powerZone2, sourceZone2);
239                 } else if (showZone == 3) {
240                     selectingRecord = false;
241                     text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
242                 } else if (showZone == 4) {
243                     selectingRecord = false;
244                     text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
245                 }
246                 resetZone = false;
247                 break;
248             case ZONE_SELECT:
249                 if (getModel().getNbAdditionalZones() == 0
250                         || (getModel().getNbAdditionalZones() > 1 && getModel().getZoneSelectCmd() == cmd)
251                         || (showZone == 1 && getModel().getZoneSelectCmd() != cmd)) {
252                     accepted = false;
253                 } else {
254                     if (getModel().getZoneSelectCmd() == cmd) {
255                         if (!power && !powerZone2) {
256                             showZone = 2;
257                             powerZone2 = true;
258                         } else if (showZone == 2) {
259                             powerZone2 = !powerZone2;
260                         } else {
261                             showZone = 2;
262                         }
263                     } else {
264                         if (showZone == 2) {
265                             powerZone2 = !powerZone2;
266                         } else if (showZone == 3) {
267                             powerZone3 = !powerZone3;
268                         } else if (showZone == 4) {
269                             powerZone4 = !powerZone4;
270                         }
271                     }
272                     if (showZone == 2) {
273                         text = textLine2 = buildZonePowerResponse(
274                                 getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE", powerZone2, sourceZone2);
275                     } else if (showZone == 3) {
276                         text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
277                     } else if (showZone == 4) {
278                         text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
279                     }
280                     resetZone = false;
281                 }
282                 break;
283             default:
284                 accepted = false;
285                 break;
286         }
287         if (!accepted && powerZone2) {
288             accepted = true;
289             switch (cmd) {
290                 case ZONE2_VOLUME_UP:
291                     if (volumeZone2 < maxVolume) {
292                         volumeZone2++;
293                     }
294                     text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
295                             muteZone2, volumeZone2);
296                     break;
297                 case ZONE2_VOLUME_DOWN:
298                     if (volumeZone2 > minVolume) {
299                         volumeZone2--;
300                     }
301                     text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
302                             muteZone2, volumeZone2);
303                     break;
304                 case ZONE2_VOLUME_SET:
305                     if (value != null) {
306                         volumeZone2 = value;
307                     }
308                     text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
309                             muteZone2, volumeZone2);
310                     break;
311                 case VOLUME_UP:
312                     if (!getModel().hasZone2Commands() && getModel().getNbAdditionalZones() >= 1 && showZone == 2) {
313                         if (volumeZone2 < maxVolume) {
314                             volumeZone2++;
315                         }
316                         text = textLine2 = buildZoneVolumeResponse(
317                                 getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE", muteZone2, volumeZone2);
318                         resetZone = false;
319                     } else {
320                         accepted = false;
321                     }
322                     break;
323                 case VOLUME_DOWN:
324                     if (!getModel().hasZone2Commands() && getModel().getNbAdditionalZones() >= 1 && showZone == 2) {
325                         if (volumeZone2 > minVolume) {
326                             volumeZone2--;
327                         }
328                         text = textLine2 = buildZoneVolumeResponse(
329                                 getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE", muteZone2, volumeZone2);
330                         resetZone = false;
331                     } else {
332                         accepted = false;
333                     }
334                     break;
335                 case VOLUME_SET:
336                     if (!getModel().hasZone2Commands() && getModel().getNbAdditionalZones() >= 1 && showZone == 2) {
337                         if (value != null) {
338                             volumeZone2 = value;
339                         }
340                         text = textLine2 = buildZoneVolumeResponse(
341                                 getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE", muteZone2, volumeZone2);
342                         resetZone = false;
343                     } else {
344                         accepted = false;
345                     }
346                     break;
347                 case ZONE2_MUTE_TOGGLE:
348                     muteZone2 = !muteZone2;
349                     text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
350                             muteZone2, volumeZone2);
351                     break;
352                 case ZONE2_MUTE_ON:
353                     muteZone2 = true;
354                     text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
355                             muteZone2, volumeZone2);
356                     break;
357                 case ZONE2_MUTE_OFF:
358                     muteZone2 = false;
359                     text = textLine2 = buildZoneVolumeResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
360                             muteZone2, volumeZone2);
361                     break;
362                 default:
363                     accepted = false;
364                     break;
365             }
366             if (!accepted) {
367                 try {
368                     sourceZone2 = getModel().getZone2SourceFromCommand(cmd);
369                     powerZone2 = true;
370                     text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
371                             powerZone2, sourceZone2);
372                     muteZone2 = false;
373                     accepted = true;
374                     showZone = 2;
375                     resetZone = false;
376                 } catch (RotelException e) {
377                 }
378             }
379             if (!accepted && !getModel().hasZone2Commands() && getModel().getNbAdditionalZones() >= 1
380                     && showZone == 2) {
381                 try {
382                     sourceZone2 = getModel().getSourceFromCommand(cmd);
383                     powerZone2 = true;
384                     text = textLine2 = buildZonePowerResponse(getModel().getNbAdditionalZones() > 1 ? "ZONE2" : "ZONE",
385                             powerZone2, sourceZone2);
386                     muteZone2 = false;
387                     accepted = true;
388                     resetZone = false;
389                 } catch (RotelException e) {
390                 }
391             }
392         }
393         if (!accepted && powerZone3) {
394             accepted = true;
395             switch (cmd) {
396                 case ZONE3_VOLUME_UP:
397                     if (volumeZone3 < maxVolume) {
398                         volumeZone3++;
399                     }
400                     text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
401                     break;
402                 case ZONE3_VOLUME_DOWN:
403                     if (volumeZone3 > minVolume) {
404                         volumeZone3--;
405                     }
406                     text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
407                     break;
408                 case ZONE3_VOLUME_SET:
409                     if (value != null) {
410                         volumeZone3 = value;
411                     }
412                     text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
413                     break;
414                 case ZONE3_MUTE_TOGGLE:
415                     muteZone3 = !muteZone3;
416                     text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
417                     break;
418                 case ZONE3_MUTE_ON:
419                     muteZone3 = true;
420                     text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
421                     break;
422                 case ZONE3_MUTE_OFF:
423                     muteZone3 = false;
424                     text = textLine2 = buildZoneVolumeResponse("ZONE3", muteZone3, volumeZone3);
425                     break;
426                 default:
427                     accepted = false;
428                     break;
429             }
430             if (!accepted) {
431                 try {
432                     sourceZone3 = getModel().getZone3SourceFromCommand(cmd);
433                     powerZone3 = true;
434                     text = textLine2 = buildZonePowerResponse("ZONE3", powerZone3, sourceZone3);
435                     muteZone3 = false;
436                     accepted = true;
437                     showZone = 3;
438                     resetZone = false;
439                 } catch (RotelException e) {
440                 }
441             }
442         }
443         if (!accepted && powerZone4) {
444             accepted = true;
445             switch (cmd) {
446                 case ZONE4_VOLUME_UP:
447                     if (volumeZone4 < maxVolume) {
448                         volumeZone4++;
449                     }
450                     text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
451                     break;
452                 case ZONE4_VOLUME_DOWN:
453                     if (volumeZone4 > minVolume) {
454                         volumeZone4--;
455                     }
456                     text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
457                     break;
458                 case ZONE4_VOLUME_SET:
459                     if (value != null) {
460                         volumeZone4 = value;
461                     }
462                     text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
463                     break;
464                 case ZONE4_MUTE_TOGGLE:
465                     muteZone4 = !muteZone4;
466                     text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
467                     break;
468                 case ZONE4_MUTE_ON:
469                     muteZone4 = true;
470                     text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
471                     break;
472                 case ZONE4_MUTE_OFF:
473                     muteZone4 = false;
474                     text = textLine2 = buildZoneVolumeResponse("ZONE4", muteZone4, volumeZone4);
475                     break;
476                 default:
477                     accepted = false;
478                     break;
479             }
480             if (!accepted) {
481                 try {
482                     sourceZone4 = getModel().getZone4SourceFromCommand(cmd);
483                     powerZone4 = true;
484                     text = textLine2 = buildZonePowerResponse("ZONE4", powerZone4, sourceZone4);
485                     muteZone4 = false;
486                     accepted = true;
487                     showZone = 4;
488                     resetZone = false;
489                 } catch (RotelException e) {
490                 }
491             }
492         }
493         if (!accepted && power) {
494             accepted = true;
495             switch (cmd) {
496                 case UPDATE_AUTO:
497                     textAscii = buildAsciiResponse(
498                             getProtocol() == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, "AUTO");
499                     break;
500                 case UPDATE_MANUAL:
501                     textAscii = buildAsciiResponse(
502                             getProtocol() == RotelProtocol.ASCII_V1 ? KEY_DISPLAY_UPDATE : KEY_UPDATE_MODE, "MANUAL");
503                     break;
504                 case VOLUME_GET_MIN:
505                     textAscii = buildAsciiResponse(KEY_VOLUME_MIN, minVolume);
506                     break;
507                 case VOLUME_GET_MAX:
508                     textAscii = buildAsciiResponse(KEY_VOLUME_MAX, maxVolume);
509                     break;
510                 case VOLUME_UP:
511                 case MAIN_ZONE_VOLUME_UP:
512                     if (volume < maxVolume) {
513                         volume++;
514                     }
515                     text = buildVolumeLine1Response();
516                     textLine1Right = buildVolumeLine1RightResponse();
517                     textAscii = buildVolumeAsciiResponse();
518                     break;
519                 case VOLUME_DOWN:
520                 case MAIN_ZONE_VOLUME_DOWN:
521                     if (volume > minVolume) {
522                         volume--;
523                     }
524                     text = buildVolumeLine1Response();
525                     textLine1Right = buildVolumeLine1RightResponse();
526                     textAscii = buildVolumeAsciiResponse();
527                     break;
528                 case VOLUME_SET:
529                     if (value != null) {
530                         volume = value;
531                     }
532                     text = buildVolumeLine1Response();
533                     textLine1Right = buildVolumeLine1RightResponse();
534                     textAscii = buildVolumeAsciiResponse();
535                     break;
536                 case VOLUME_GET:
537                     textAscii = buildVolumeAsciiResponse();
538                     break;
539                 case MUTE_TOGGLE:
540                 case MAIN_ZONE_MUTE_TOGGLE:
541                     mute = !mute;
542                     text = buildSourceLine1Response();
543                     textLine1Right = buildVolumeLine1RightResponse();
544                     textAscii = buildMuteAsciiResponse();
545                     break;
546                 case MUTE_ON:
547                 case MAIN_ZONE_MUTE_ON:
548                     mute = true;
549                     text = buildSourceLine1Response();
550                     textLine1Right = buildVolumeLine1RightResponse();
551                     textAscii = buildMuteAsciiResponse();
552                     break;
553                 case MUTE_OFF:
554                 case MAIN_ZONE_MUTE_OFF:
555                     mute = false;
556                     text = buildSourceLine1Response();
557                     textLine1Right = buildVolumeLine1RightResponse();
558                     textAscii = buildMuteAsciiResponse();
559                     break;
560                 case MUTE:
561                     textAscii = buildMuteAsciiResponse();
562                     break;
563                 case TONE_MAX:
564                     textAscii = buildAsciiResponse(KEY_TONE_MAX, "%02d", maxToneLevel);
565                     break;
566                 case BASS_UP:
567                     if (bass < maxToneLevel) {
568                         bass += STEP_TONE_LEVEL;
569                     }
570                     text = buildBassLine1Response();
571                     textLine1Right = buildBassLine1RightResponse();
572                     textAscii = buildBassAsciiResponse();
573                     break;
574                 case BASS_DOWN:
575                     if (bass > minToneLevel) {
576                         bass -= STEP_TONE_LEVEL;
577                     }
578                     text = buildBassLine1Response();
579                     textLine1Right = buildBassLine1RightResponse();
580                     textAscii = buildBassAsciiResponse();
581                     break;
582                 case BASS_SET:
583                     if (value != null) {
584                         bass = value;
585                     }
586                     text = buildBassLine1Response();
587                     textLine1Right = buildBassLine1RightResponse();
588                     textAscii = buildBassAsciiResponse();
589                     break;
590                 case BASS:
591                     textAscii = buildBassAsciiResponse();
592                     break;
593                 case TREBLE_UP:
594                     if (treble < maxToneLevel) {
595                         treble += STEP_TONE_LEVEL;
596                     }
597                     text = buildTrebleLine1Response();
598                     textLine1Right = buildTrebleLine1RightResponse();
599                     textAscii = buildTrebleAsciiResponse();
600                     break;
601                 case TREBLE_DOWN:
602                     if (treble > minToneLevel) {
603                         treble -= STEP_TONE_LEVEL;
604                     }
605                     text = buildTrebleLine1Response();
606                     textLine1Right = buildTrebleLine1RightResponse();
607                     textAscii = buildTrebleAsciiResponse();
608                     break;
609                 case TREBLE_SET:
610                     if (value != null) {
611                         treble = value;
612                     }
613                     text = buildTrebleLine1Response();
614                     textLine1Right = buildTrebleLine1RightResponse();
615                     textAscii = buildTrebleAsciiResponse();
616                     break;
617                 case TREBLE:
618                     textAscii = buildTrebleAsciiResponse();
619                     break;
620                 case TONE_CONTROL_SELECT:
621                     showTreble = !showTreble;
622                     if (showTreble) {
623                         text = buildTrebleLine1Response();
624                         textLine1Right = buildTrebleLine1RightResponse();
625                     } else {
626                         text = buildBassLine1Response();
627                         textLine1Right = buildBassLine1RightResponse();
628                     }
629                     break;
630                 case PLAY:
631                     playStatus = RotelPlayStatus.PLAYING;
632                     textAscii = buildPlayStatusAsciiResponse();
633                     break;
634                 case STOP:
635                     playStatus = RotelPlayStatus.STOPPED;
636                     textAscii = buildPlayStatusAsciiResponse();
637                     break;
638                 case PAUSE:
639                     switch (playStatus) {
640                         case PLAYING:
641                             playStatus = RotelPlayStatus.PAUSED;
642                             break;
643                         case PAUSED:
644                         case STOPPED:
645                             playStatus = RotelPlayStatus.PLAYING;
646                             break;
647                     }
648                     textAscii = buildPlayStatusAsciiResponse();
649                     break;
650                 case CD_PLAY_STATUS:
651                 case PLAY_STATUS:
652                     textAscii = buildPlayStatusAsciiResponse();
653                     break;
654                 case TRACK_FORWARD:
655                     track++;
656                     textAscii = buildTrackAsciiResponse();
657                     break;
658                 case TRACK_BACKWORD:
659                     if (track > 1) {
660                         track--;
661                     }
662                     textAscii = buildTrackAsciiResponse();
663                     break;
664                 case TRACK:
665                     textAscii = buildTrackAsciiResponse();
666                     break;
667                 case SOURCE_MULTI_INPUT:
668                     multiinput = !multiinput;
669                     text = "MULTI IN " + (multiinput ? "ON" : "OFF");
670                     try {
671                         source = getModel().getSourceFromCommand(cmd);
672                         textLine1Left = buildSourceLine1LeftResponse();
673                         textAscii = buildSourceAsciiResponse();
674                         mute = false;
675                     } catch (RotelException e) {
676                     }
677                     break;
678                 case SOURCE:
679                     textAscii = buildSourceAsciiResponse();
680                     break;
681                 case STEREO:
682                     dsp = RotelDsp.CAT4_NONE;
683                     textLine2 = "STEREO";
684                     textAscii = buildDspAsciiResponse();
685                     break;
686                 case STEREO3:
687                     dsp = RotelDsp.CAT4_STEREO3;
688                     textLine2 = "DOLBY 3 STEREO";
689                     textAscii = buildDspAsciiResponse();
690                     break;
691                 case STEREO5:
692                     dsp = RotelDsp.CAT4_STEREO5;
693                     textLine2 = "5CH STEREO";
694                     textAscii = buildDspAsciiResponse();
695                     break;
696                 case STEREO7:
697                     dsp = RotelDsp.CAT4_STEREO7;
698                     textLine2 = "7CH STEREO";
699                     textAscii = buildDspAsciiResponse();
700                     break;
701                 case STEREO9:
702                     dsp = RotelDsp.CAT5_STEREO9;
703                     textAscii = buildDspAsciiResponse();
704                     break;
705                 case STEREO11:
706                     dsp = RotelDsp.CAT5_STEREO11;
707                     textAscii = buildDspAsciiResponse();
708                     break;
709                 case DSP1:
710                     dsp = RotelDsp.CAT4_DSP1;
711                     textLine2 = "DSP 1";
712                     textAscii = buildDspAsciiResponse();
713                     break;
714                 case DSP2:
715                     dsp = RotelDsp.CAT4_DSP2;
716                     textLine2 = "DSP 2";
717                     textAscii = buildDspAsciiResponse();
718                     break;
719                 case DSP3:
720                     dsp = RotelDsp.CAT4_DSP3;
721                     textLine2 = "DSP 3";
722                     textAscii = buildDspAsciiResponse();
723                     break;
724                 case DSP4:
725                     dsp = RotelDsp.CAT4_DSP4;
726                     textLine2 = "DSP 4";
727                     textAscii = buildDspAsciiResponse();
728                     break;
729                 case PROLOGIC:
730                     dsp = RotelDsp.CAT4_PROLOGIC;
731                     textLine2 = "DOLBY PRO LOGIC";
732                     textAscii = buildDspAsciiResponse();
733                     break;
734                 case PLII_CINEMA:
735                     dsp = RotelDsp.CAT4_PLII_CINEMA;
736                     textLine2 = "DOLBY PL  C";
737                     textAscii = buildDspAsciiResponse();
738                     break;
739                 case PLII_MUSIC:
740                     dsp = RotelDsp.CAT4_PLII_MUSIC;
741                     textLine2 = "DOLBY PL  M";
742                     textAscii = buildDspAsciiResponse();
743                     break;
744                 case PLII_GAME:
745                     dsp = RotelDsp.CAT4_PLII_GAME;
746                     textLine2 = "DOLBY PL  G";
747                     textAscii = buildDspAsciiResponse();
748                     break;
749                 case PLIIZ:
750                     dsp = RotelDsp.CAT4_PLIIZ;
751                     textLine2 = "DOLBY PL z";
752                     textAscii = buildDspAsciiResponse();
753                     break;
754                 case NEO6_MUSIC:
755                     dsp = RotelDsp.CAT4_NEO6_MUSIC;
756                     textLine2 = "DTS Neo:6 M";
757                     textAscii = buildDspAsciiResponse();
758                     break;
759                 case NEO6_CINEMA:
760                     dsp = RotelDsp.CAT4_NEO6_CINEMA;
761                     textLine2 = "DTS Neo:6 C";
762                     textAscii = buildDspAsciiResponse();
763                     break;
764                 case ATMOS:
765                     dsp = RotelDsp.CAT5_ATMOS;
766                     textAscii = buildDspAsciiResponse();
767                     break;
768                 case NEURAL_X:
769                     dsp = RotelDsp.CAT5_NEURAL_X;
770                     textAscii = buildDspAsciiResponse();
771                     break;
772                 case BYPASS:
773                     dsp = RotelDsp.CAT4_BYPASS;
774                     textLine2 = "BYPASS";
775                     textAscii = buildDspAsciiResponse();
776                     break;
777                 case DSP_MODE:
778                     textAscii = buildDspAsciiResponse();
779                     break;
780                 case FREQUENCY:
781                     textAscii = buildAsciiResponse(KEY_FREQ, "44.1");
782                     break;
783                 case DIMMER_LEVEL_SET:
784                     if (value != null) {
785                         dimmer = value;
786                     }
787                     textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
788                     break;
789                 case DIMMER_LEVEL_GET:
790                     textAscii = buildAsciiResponse(KEY_DIMMER, dimmer);
791                     break;
792                 default:
793                     accepted = false;
794                     break;
795             }
796             if (!accepted) {
797                 try {
798                     source = getModel().getMainZoneSourceFromCommand(cmd);
799                     text = buildSourceLine1Response();
800                     textLine1Left = buildSourceLine1LeftResponse();
801                     textAscii = buildSourceAsciiResponse();
802                     accepted = true;
803                 } catch (RotelException e) {
804                 }
805             }
806             if (!accepted) {
807                 try {
808                     if (selectingRecord && !getModel().hasOtherThanPrimaryCommands()) {
809                         recordSource = getModel().getSourceFromCommand(cmd);
810                     } else {
811                         source = getModel().getSourceFromCommand(cmd);
812                     }
813                     text = buildSourceLine1Response();
814                     textLine1Left = buildSourceLine1LeftResponse();
815                     textAscii = buildSourceAsciiResponse();
816                     mute = false;
817                     accepted = true;
818                 } catch (RotelException e) {
819                 }
820             }
821             if (!accepted) {
822                 try {
823                     recordSource = getModel().getRecordSourceFromCommand(cmd);
824                     text = buildSourceLine1Response();
825                     textLine2 = buildRecordResponse();
826                     accepted = true;
827                 } catch (RotelException e) {
828                 }
829             }
830         }
831
832         if (!accepted) {
833             return;
834         }
835
836         if (cmd != RotelCommand.RECORD_FONCTION_SELECT) {
837             selectingRecord = false;
838         }
839         if (resetZone) {
840             showZone = 0;
841         }
842
843         if (getModel().getRespNbChars() == 42) {
844             while (textLine1Left.length() < 14) {
845                 textLine1Left += " ";
846             }
847             while (textLine1Right.length() < 7) {
848                 textLine1Right += " ";
849             }
850             while (textLine2.length() < 21) {
851                 textLine2 += " ";
852             }
853             text = textLine1Left + textLine1Right + textLine2;
854         }
855
856         if (getProtocol() == RotelProtocol.HEX) {
857             byte[] chars = Arrays.copyOf(text.getBytes(StandardCharsets.US_ASCII), getModel().getRespNbChars());
858             byte[] flags = new byte[getModel().getRespNbFlags()];
859             try {
860                 getModel().setMultiInput(flags, multiinput);
861             } catch (RotelException e) {
862             }
863             try {
864                 getModel().setZone2(flags, powerZone2);
865             } catch (RotelException e) {
866             }
867             try {
868                 getModel().setZone3(flags, powerZone3);
869             } catch (RotelException e) {
870             }
871             try {
872                 getModel().setZone4(flags, powerZone4);
873             } catch (RotelException e) {
874             }
875             int size = 6 + getModel().getRespNbChars() + getModel().getRespNbFlags();
876             byte[] dataBuffer = new byte[size];
877             int idx = 0;
878             dataBuffer[idx++] = START;
879             dataBuffer[idx++] = (byte) (size - 4);
880             dataBuffer[idx++] = getModel().getDeviceId();
881             dataBuffer[idx++] = STANDARD_RESPONSE;
882             if (getModel().isCharsBeforeFlags()) {
883                 System.arraycopy(chars, 0, dataBuffer, idx, getModel().getRespNbChars());
884                 idx += getModel().getRespNbChars();
885                 System.arraycopy(flags, 0, dataBuffer, idx, getModel().getRespNbFlags());
886                 idx += getModel().getRespNbFlags();
887             } else {
888                 System.arraycopy(flags, 0, dataBuffer, idx, getModel().getRespNbFlags());
889                 idx += getModel().getRespNbFlags();
890                 System.arraycopy(chars, 0, dataBuffer, idx, getModel().getRespNbChars());
891                 idx += getModel().getRespNbChars();
892             }
893             byte checksum = computeCheckSum(dataBuffer, idx - 1);
894             if ((checksum & 0x000000FF) == 0x000000FD) {
895                 dataBuffer[idx++] = (byte) 0xFD;
896                 dataBuffer[idx++] = 0;
897             } else if ((checksum & 0x000000FF) == 0x000000FE) {
898                 dataBuffer[idx++] = (byte) 0xFD;
899                 dataBuffer[idx++] = 1;
900             } else {
901                 dataBuffer[idx++] = checksum;
902             }
903             synchronized (lock) {
904                 feedbackMsg = Arrays.copyOf(dataBuffer, idx);
905                 idxInFeedbackMsg = 0;
906             }
907         } else {
908             String command = textAscii + (getProtocol() == RotelProtocol.ASCII_V1 ? "!" : "$");
909             synchronized (lock) {
910                 feedbackMsg = command.getBytes(StandardCharsets.US_ASCII);
911                 idxInFeedbackMsg = 0;
912             }
913         }
914     }
915
916     private String buildAsciiResponse(String key, String value) {
917         return String.format("%s=%s", key, value);
918     }
919
920     private String buildAsciiResponse(String key, int value) {
921         return buildAsciiResponse(key, "%d", value);
922     }
923
924     private String buildAsciiResponse(String key, String format, int value) {
925         return String.format("%s=" + format, key, value);
926     }
927
928     private String buildAsciiResponse(String key, boolean value) {
929         return buildAsciiResponse(key, value ? MSG_VALUE_ON : MSG_VALUE_OFF);
930     }
931
932     private String buildPowerAsciiResponse() {
933         return buildAsciiResponse(KEY_POWER, power ? POWER_ON : STANDBY);
934     }
935
936     private String buildVolumeAsciiResponse() {
937         return buildAsciiResponse(KEY_VOLUME, "%02d", volume);
938     }
939
940     private String buildMuteAsciiResponse() {
941         return buildAsciiResponse(KEY_MUTE, mute);
942     }
943
944     private String buildBassAsciiResponse() {
945         String result;
946         if (bass == 0) {
947             result = buildAsciiResponse(KEY_BASS, "000");
948         } else if (bass > 0) {
949             result = buildAsciiResponse(KEY_BASS, "+%02d", bass);
950         } else {
951             result = buildAsciiResponse(KEY_BASS, "-%02d", -bass);
952         }
953         return result;
954     }
955
956     private String buildTrebleAsciiResponse() {
957         String result;
958         if (treble == 0) {
959             result = buildAsciiResponse(KEY_TREBLE, "000");
960         } else if (treble > 0) {
961             result = buildAsciiResponse(KEY_TREBLE, "+%02d", treble);
962         } else {
963             result = buildAsciiResponse(KEY_TREBLE, "-%02d", -treble);
964         }
965         return result;
966     }
967
968     private String buildPlayStatusAsciiResponse() {
969         String status = "";
970         switch (playStatus) {
971             case PLAYING:
972                 status = PLAY;
973                 break;
974             case PAUSED:
975                 status = PAUSE;
976                 break;
977             case STOPPED:
978                 status = STOP;
979                 break;
980         }
981         return buildAsciiResponse(getProtocol() == RotelProtocol.ASCII_V1 ? KEY1_PLAY_STATUS : KEY2_PLAY_STATUS,
982                 status);
983     }
984
985     private String buildTrackAsciiResponse() {
986         return buildAsciiResponse(KEY_TRACK, "%03d", track);
987     }
988
989     private String buildSourceAsciiResponse() {
990         String str = null;
991         RotelCommand command = source.getCommand();
992         if (command != null) {
993             str = command.getAsciiCommandV2();
994         }
995         return buildAsciiResponse(KEY_SOURCE, (str == null) ? "" : str);
996     }
997
998     private String buildDspAsciiResponse() {
999         return buildAsciiResponse(KEY_DSP_MODE, dsp.getFeedback());
1000     }
1001
1002     private String buildSourceLine1Response() {
1003         String text;
1004         if (!power) {
1005             text = "";
1006         } else if (mute) {
1007             text = "MUTE ON";
1008         } else {
1009             text = getSourceLabel(source, false) + " " + getSourceLabel(recordSource, true);
1010         }
1011         return text;
1012     }
1013
1014     private String buildSourceLine1LeftResponse() {
1015         String text;
1016         if (!power) {
1017             text = "";
1018         } else {
1019             text = getSourceLabel(source, false);
1020         }
1021         return text;
1022     }
1023
1024     private String buildRecordResponse() {
1025         String text;
1026         if (!power) {
1027             text = "";
1028         } else {
1029             text = "REC " + getSourceLabel(recordSource, true);
1030         }
1031         return text;
1032     }
1033
1034     private String buildZonePowerResponse(String zone, boolean powerZone, RotelSource sourceZone) {
1035         String state = powerZone ? getSourceLabel(sourceZone, true) : "OFF";
1036         return zone + " " + state;
1037     }
1038
1039     private String buildVolumeLine1Response() {
1040         String text;
1041         if (volume == minVolume) {
1042             text = " VOLUME  MIN ";
1043         } else if (volume == maxVolume) {
1044             text = " VOLUME  MAX ";
1045         } else {
1046             text = String.format(" VOLUME   %02d ", volume);
1047         }
1048         return text;
1049     }
1050
1051     private String buildVolumeLine1RightResponse() {
1052         String text;
1053         if (!power) {
1054             text = "";
1055         } else if (mute) {
1056             text = "MUTE ON";
1057         } else if (volume == minVolume) {
1058             text = "VOL MIN";
1059         } else if (volume == maxVolume) {
1060             text = "VOL MAX";
1061         } else {
1062             text = String.format("VOL  %02d", volume);
1063         }
1064         return text;
1065     }
1066
1067     private String buildZoneVolumeResponse(String zone, boolean muted, int vol) {
1068         String text;
1069         if (muted) {
1070             text = zone + " MUTE ON";
1071         } else if (vol == minVolume) {
1072             text = zone + " VOL MIN";
1073         } else if (vol == maxVolume) {
1074             text = zone + " VOL MAX";
1075         } else {
1076             text = String.format("%s VOL %02d", zone, vol);
1077         }
1078         return text;
1079     }
1080
1081     private String buildBassLine1Response() {
1082         String text;
1083         if (bass == minToneLevel) {
1084             text = "   BASS  MIN ";
1085         } else if (bass == maxToneLevel) {
1086             text = "   BASS  MAX ";
1087         } else if (bass == 0) {
1088             text = "   BASS    0 ";
1089         } else if (bass > 0) {
1090             text = String.format("   BASS  +%02d ", bass);
1091         } else {
1092             text = String.format("   BASS  -%02d ", -bass);
1093         }
1094         return text;
1095     }
1096
1097     private String buildBassLine1RightResponse() {
1098         String text;
1099         if (bass == minToneLevel) {
1100             text = "LF  MIN";
1101         } else if (bass == maxToneLevel) {
1102             text = "LF  MAX";
1103         } else if (bass == 0) {
1104             text = "LF    0";
1105         } else if (bass > 0) {
1106             text = String.format("LF + %02d", bass);
1107         } else {
1108             text = String.format("LF - %02d", -bass);
1109         }
1110         return text;
1111     }
1112
1113     private String buildTrebleLine1Response() {
1114         String text;
1115         if (treble == minToneLevel) {
1116             text = " TREBLE  MIN ";
1117         } else if (treble == maxToneLevel) {
1118             text = " TREBLE  MAX ";
1119         } else if (treble == 0) {
1120             text = " TREBLE    0 ";
1121         } else if (treble > 0) {
1122             text = String.format(" TREBLE  +%02d ", treble);
1123         } else {
1124             text = String.format(" TREBLE  -%02d ", -treble);
1125         }
1126         return text;
1127     }
1128
1129     private String buildTrebleLine1RightResponse() {
1130         String text;
1131         if (treble == minToneLevel) {
1132             text = "HF  MIN";
1133         } else if (treble == maxToneLevel) {
1134             text = "HF  MAX";
1135         } else if (treble == 0) {
1136             text = "HF    0";
1137         } else if (treble > 0) {
1138             text = String.format("HF + %02d", treble);
1139         } else {
1140             text = String.format("HF - %02d", -treble);
1141         }
1142         return text;
1143     }
1144
1145     private String getSourceLabel(RotelSource source, boolean considerFollowMain) {
1146         String label;
1147         if (considerFollowMain && source.getName().equals(RotelSource.CAT1_FOLLOW_MAIN.getName())) {
1148             label = "SOURCE";
1149         } else {
1150             label = Objects.requireNonNullElse(sourcesLabels.get(source), source.getLabel());
1151         }
1152
1153         return label;
1154     }
1155 }