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