]> git.basschouten.com Git - openhab-addons.git/blob
30a4d4768b828dc18c1600e3dc5d21a352448b82
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.epsonprojector.internal;
14
15 import java.time.Duration;
16 import java.util.concurrent.ScheduledExecutorService;
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
19
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.epsonprojector.internal.configuration.EpsonProjectorConfiguration;
23 import org.openhab.binding.epsonprojector.internal.connector.EpsonProjectorConnector;
24 import org.openhab.binding.epsonprojector.internal.connector.EpsonProjectorSerialConnector;
25 import org.openhab.binding.epsonprojector.internal.connector.EpsonProjectorTcpConnector;
26 import org.openhab.binding.epsonprojector.internal.enums.AspectRatio;
27 import org.openhab.binding.epsonprojector.internal.enums.Background;
28 import org.openhab.binding.epsonprojector.internal.enums.ColorMode;
29 import org.openhab.binding.epsonprojector.internal.enums.ErrorMessage;
30 import org.openhab.binding.epsonprojector.internal.enums.Gamma;
31 import org.openhab.binding.epsonprojector.internal.enums.Luminance;
32 import org.openhab.binding.epsonprojector.internal.enums.PowerStatus;
33 import org.openhab.binding.epsonprojector.internal.enums.Switch;
34 import org.openhab.core.cache.ExpiringCache;
35 import org.openhab.core.io.transport.serial.SerialPortManager;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 /**
40  * Provide high level interface to Epson projector.
41  *
42  * @author Pauli Anttila - Initial contribution
43  * @author Yannick Schaus - Refactoring
44  * @author Michael Lobstein - Improvements for OH3
45  */
46 @NonNullByDefault
47 public class EpsonProjectorDevice {
48     private static final int[] MAP64 = new int[] { 0, 3, 7, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 51, 55, 59, 63, 66,
49             70, 74, 78, 82, 86, 90, 94, 98, 102, 106, 110, 114, 118, 122, 126, 129, 133, 137, 141, 145, 149, 153, 157,
50             161, 165, 169, 173, 177, 181, 185, 189, 192, 196, 200, 204, 208, 212, 216, 220, 224, 228, 232, 236, 240,
51             244, 248, 252 };
52
53     private static final int[] MAP60 = new int[] { 0, 4, 8, 12, 16, 20, 25, 29, 33, 37, 41, 46, 50, 54, 58, 62, 67, 71,
54             75, 79, 83, 88, 92, 96, 100, 104, 109, 113, 117, 121, 125, 130, 134, 138, 142, 146, 151, 155, 159, 163, 167,
55             172, 176, 180, 184, 188, 193, 197, 201, 205, 209, 214, 218, 222, 226, 230, 235, 239, 243, 247, 251 };
56
57     private static final int[] MAP49 = new int[] { 0, 5, 10, 15, 20, 25, 30, 35, 40, 46, 51, 56, 61, 66, 71, 76, 81, 87,
58             92, 97, 102, 107, 112, 117, 122, 128, 133, 138, 143, 148, 153, 158, 163, 168, 174, 179, 184, 189, 194, 199,
59             204, 209, 215, 220, 225, 230, 235, 240, 245, 250 };
60
61     private static final int[] MAP48 = new int[] { 0, 5, 10, 15, 20, 26, 31, 36, 41, 47, 52, 57, 62, 67, 73, 78, 83, 88,
62             94, 99, 104, 109, 114, 120, 125, 130, 135, 141, 146, 151, 156, 161, 167, 172, 177, 182, 188, 193, 198, 203,
63             208, 214, 219, 224, 229, 235, 240, 245, 250 };
64
65     private static final int[] MAP40 = new int[] { 0, 6, 12, 18, 24, 31, 37, 43, 49, 56, 62, 68, 74, 81, 87, 93, 99,
66             106, 112, 118, 124, 131, 137, 143, 149, 156, 162, 168, 174, 181, 187, 193, 199, 206, 212, 218, 224, 231,
67             237, 243, 249 };
68
69     private static final int[] MAP20 = new int[] { 0, 12, 24, 36, 48, 60, 73, 85, 97, 109, 121, 134, 146, 158, 170, 182,
70             195, 207, 219, 231, 243 };
71
72     private static final int[] MAP18 = new int[] { 0, 13, 26, 40, 53, 67, 80, 94, 107, 121, 134, 148, 161, 175, 188,
73             202, 215, 229, 242 };
74
75     private static final int[] MAP_COLOR_TEMP = new int[] { 0, 25, 51, 76, 102, 128, 153, 179, 204, 230 };
76     private static final int[] MAP_FLESH_COLOR = new int[] { 0, 36, 73, 109, 146, 182, 219 };
77
78     private static final int DEFAULT_TIMEOUT = 5 * 1000;
79     private static final int POWER_ON_TIMEOUT = 100 * 1000;
80     private static final int POWER_OFF_TIMEOUT = 130 * 1000;
81     private static final int LAMP_REFRESH_WAIT_MINUTES = 5;
82
83     private static final String ON = "ON";
84     private static final String ERR = "ERR";
85     private static final String IMEVENT = "IMEVENT";
86
87     private final Logger logger = LoggerFactory.getLogger(EpsonProjectorDevice.class);
88
89     private @Nullable ScheduledExecutorService scheduler = null;
90     private @Nullable ScheduledFuture<?> timeoutJob;
91
92     private EpsonProjectorConnector connection;
93     private ExpiringCache<Integer> cachedLampHours = new ExpiringCache<>(Duration.ofMinutes(LAMP_REFRESH_WAIT_MINUTES),
94             this::queryLamp);
95     private boolean connected = false;
96     private boolean ready = true;
97
98     public EpsonProjectorDevice(SerialPortManager serialPortManager, EpsonProjectorConfiguration config) {
99         connection = new EpsonProjectorSerialConnector(serialPortManager, config.serialPort);
100     }
101
102     public EpsonProjectorDevice(EpsonProjectorConfiguration config) {
103         connection = new EpsonProjectorTcpConnector(config.host, config.port);
104     }
105
106     public boolean isReady() {
107         return ready;
108     }
109
110     public void setScheduler(ScheduledExecutorService scheduler) {
111         this.scheduler = scheduler;
112     }
113
114     private synchronized @Nullable String sendQuery(String query, int timeout)
115             throws EpsonProjectorCommandException, EpsonProjectorException {
116         logger.debug("Query: '{}'", query);
117         String response = connection.sendMessage(query, timeout);
118
119         if (response.length() == 0) {
120             throw new EpsonProjectorException("No response received");
121         }
122
123         response = response.replace("\r:", "");
124         logger.debug("Response: '{}'", response);
125
126         if (ERR.equals(response) || response.startsWith(IMEVENT)) {
127             throw new EpsonProjectorCommandException("Error response received for command: " + query);
128         }
129
130         if ("PWR OFF".equals(query) && ":".equals(response)) {
131             // When PWR OFF command is sent, next command can be sent 10 seconds after the colon is received
132             logger.debug("Refusing further commands for 10 seconds to power OFF completion");
133             ready = false;
134             ScheduledExecutorService scheduler = this.scheduler;
135             if (scheduler != null) {
136                 timeoutJob = scheduler.schedule(() -> {
137                     ready = true;
138                 }, 10, TimeUnit.SECONDS);
139             }
140         }
141
142         return response;
143     }
144
145     private String splitResponse(@Nullable String response)
146             throws EpsonProjectorCommandException, EpsonProjectorException {
147         if (response != null && !"".equals(response)) {
148             String[] pieces = response.split("=");
149
150             if (pieces.length < 2) {
151                 throw new EpsonProjectorCommandException("Invalid response from projector: " + response);
152             }
153
154             return pieces[1].trim();
155         } else {
156             throw new EpsonProjectorException("No response received");
157         }
158     }
159
160     protected void sendCommand(String command, int timeout)
161             throws EpsonProjectorCommandException, EpsonProjectorException {
162         sendQuery(command, timeout);
163     }
164
165     protected void sendCommand(String command) throws EpsonProjectorCommandException, EpsonProjectorException {
166         sendCommand(command, DEFAULT_TIMEOUT);
167     }
168
169     protected int queryInt(String query, int timeout, int radix)
170             throws EpsonProjectorCommandException, EpsonProjectorException {
171         String response = sendQuery(query, timeout);
172
173         String str = splitResponse(response);
174
175         // if the response has two number groups, get the first one (Aspect Ratio does this)
176         if (str.contains(" ")) {
177             String[] subStr = str.split(" ");
178             str = subStr[0];
179         }
180
181         try {
182             return Integer.parseInt(str, radix);
183         } catch (NumberFormatException nfe) {
184             throw new EpsonProjectorCommandException(
185                     "Unable to parse response '" + str + "' as Integer for command: " + query);
186         }
187     }
188
189     protected int queryInt(String query, int timeout) throws EpsonProjectorCommandException, EpsonProjectorException {
190         return queryInt(query, timeout, 10);
191     }
192
193     protected int queryInt(String query) throws EpsonProjectorCommandException, EpsonProjectorException {
194         return queryInt(query, DEFAULT_TIMEOUT, 10);
195     }
196
197     protected int queryHexInt(String query, int timeout)
198             throws EpsonProjectorCommandException, EpsonProjectorException {
199         return queryInt(query, timeout, 16);
200     }
201
202     protected int queryHexInt(String query) throws EpsonProjectorCommandException, EpsonProjectorException {
203         return queryInt(query, DEFAULT_TIMEOUT, 16);
204     }
205
206     protected String queryString(String query) throws EpsonProjectorCommandException, EpsonProjectorException {
207         String response = sendQuery(query, DEFAULT_TIMEOUT);
208         return splitResponse(response);
209     }
210
211     public void connect() throws EpsonProjectorException {
212         connection.connect();
213         connected = true;
214     }
215
216     public void disconnect() throws EpsonProjectorException {
217         connection.disconnect();
218         connected = false;
219         ready = true;
220         ScheduledFuture<?> timeoutJob = this.timeoutJob;
221         if (timeoutJob != null) {
222             timeoutJob.cancel(true);
223             this.timeoutJob = null;
224         }
225     }
226
227     public boolean isConnected() {
228         return connected;
229     }
230
231     /*
232      * Power
233      */
234     public PowerStatus getPowerStatus() throws EpsonProjectorCommandException, EpsonProjectorException {
235         int val = queryInt("PWR?");
236         return PowerStatus.forValue(val);
237     }
238
239     public void setPower(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
240         sendCommand(String.format("PWR %s", value.name()), value == Switch.ON ? POWER_ON_TIMEOUT : POWER_OFF_TIMEOUT);
241     }
242
243     /*
244      * Key code
245      */
246     public void sendKeyCode(String value) throws EpsonProjectorCommandException, EpsonProjectorException {
247         sendCommand(String.format("KEY %s", value));
248     }
249
250     /*
251      * Vertical Keystone
252      */
253     public int getVerticalKeystone() throws EpsonProjectorCommandException, EpsonProjectorException {
254         int vkey = queryInt("VKEYSTONE?");
255         for (int i = 0; i < MAP60.length; i++) {
256             if (vkey == MAP60[i]) {
257                 return i - 30;
258             }
259         }
260         return 0;
261     }
262
263     public void setVerticalKeystone(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
264         value = value + 30;
265         if (value >= 0 && value <= 60) {
266             sendCommand(String.format("VKEYSTONE %d", MAP60[value]));
267         }
268     }
269
270     /*
271      * Horizontal Keystone
272      */
273     public int getHorizontalKeystone() throws EpsonProjectorCommandException, EpsonProjectorException {
274         int hkey = queryInt("HKEYSTONE?");
275         for (int i = 0; i < MAP60.length; i++) {
276             if (hkey == MAP60[i]) {
277                 return i - 30;
278             }
279         }
280         return 0;
281     }
282
283     public void setHorizontalKeystone(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
284         value = value + 30;
285         if (value >= 0 && value <= 60) {
286             sendCommand(String.format("HKEYSTONE %d", MAP60[value]));
287         }
288     }
289
290     /*
291      * Auto Keystone
292      */
293
294     public Switch getAutoKeystone() throws EpsonProjectorCommandException, EpsonProjectorException {
295         String val = queryString("AUTOKEYSTONE?");
296         return val.equals(ON) ? Switch.ON : Switch.OFF;
297     }
298
299     public void setAutoKeystone(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
300         sendCommand(String.format("AUTOKEYSTONE %s", value.name()), DEFAULT_TIMEOUT);
301     }
302
303     /*
304      * Freeze
305      */
306     public Switch getFreeze() throws EpsonProjectorCommandException, EpsonProjectorException {
307         String val = queryString("FREEZE?");
308         return val.equals(ON) ? Switch.ON : Switch.OFF;
309     }
310
311     public void setFreeze(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
312         sendCommand(String.format("FREEZE %s", value.name()), DEFAULT_TIMEOUT);
313     }
314
315     /*
316      * Aspect Ratio
317      */
318     public AspectRatio getAspectRatio() throws EpsonProjectorCommandException, EpsonProjectorException {
319         int val = queryHexInt("ASPECT?");
320         return AspectRatio.forValue(val);
321     }
322
323     public void setAspectRatio(AspectRatio value) throws EpsonProjectorCommandException, EpsonProjectorException {
324         sendCommand(String.format("ASPECT %02X", value.toInt()));
325     }
326
327     /*
328      * Luminance
329      */
330     public Luminance getLuminance() throws EpsonProjectorCommandException, EpsonProjectorException {
331         int val = queryHexInt("LUMINANCE?");
332         return Luminance.forValue(val);
333     }
334
335     public void setLuminance(Luminance value) throws EpsonProjectorCommandException, EpsonProjectorException {
336         sendCommand(String.format("LUMINANCE %02X", value.toInt()));
337     }
338
339     /*
340      * Source
341      */
342     public String getSource() throws EpsonProjectorCommandException, EpsonProjectorException {
343         return queryString("SOURCE?");
344     }
345
346     public void setSource(String value) throws EpsonProjectorCommandException, EpsonProjectorException {
347         sendCommand(String.format("SOURCE %s", value));
348     }
349
350     /*
351      * Brightness
352      */
353     public int getBrightness() throws EpsonProjectorCommandException, EpsonProjectorException {
354         int brt = queryInt("BRIGHT?");
355         for (int i = 0; i < MAP48.length; i++) {
356             if (brt == MAP48[i]) {
357                 return i - 24;
358             }
359         }
360         return 0;
361     }
362
363     public void setBrightness(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
364         value = value + 24;
365         if (value >= 0 && value <= 48) {
366             sendCommand(String.format("BRIGHT %d", MAP48[value]));
367         }
368     }
369
370     /*
371      * Contrast
372      */
373     public int getContrast() throws EpsonProjectorCommandException, EpsonProjectorException {
374         int con = queryInt("CONTRAST?");
375         for (int i = 0; i < MAP48.length; i++) {
376             if (con == MAP48[i]) {
377                 return i - 24;
378             }
379         }
380         return 0;
381     }
382
383     public void setContrast(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
384         value = value + 24;
385         if (value >= 0 && value <= 48) {
386             sendCommand(String.format("CONTRAST %d", MAP48[value]));
387         }
388     }
389
390     /*
391      * Density
392      */
393     public int getDensity() throws EpsonProjectorCommandException, EpsonProjectorException {
394         int den = queryInt("DENSITY?");
395         for (int i = 0; i < MAP64.length; i++) {
396             if (den == MAP64[i]) {
397                 return i - 32;
398             }
399         }
400         return 0;
401     }
402
403     public void setDensity(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
404         value = value + 32;
405         if (value >= 0 && value <= 64) {
406             sendCommand(String.format("DENSITY %d", MAP64[value]));
407         }
408     }
409
410     /*
411      * Tint
412      */
413     public int getTint() throws EpsonProjectorCommandException, EpsonProjectorException {
414         int tint = queryInt("TINT?");
415         for (int i = 0; i < MAP64.length; i++) {
416             if (tint == MAP64[i]) {
417                 return i - 32;
418             }
419         }
420         return 0;
421     }
422
423     public void setTint(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
424         value = value + 32;
425         if (value >= 0 && value <= 64) {
426             sendCommand(String.format("TINT %d", MAP64[value]));
427         }
428     }
429
430     /*
431      * Color Temperature
432      */
433     public int getColorTemperature() throws EpsonProjectorCommandException, EpsonProjectorException {
434         int ctemp = queryInt("CTEMP?");
435         for (int i = 0; i < MAP_COLOR_TEMP.length; i++) {
436             if (ctemp == MAP_COLOR_TEMP[i]) {
437                 return i;
438             }
439         }
440         return 0;
441     }
442
443     public void setColorTemperature(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
444         if (value >= 0 && value <= 9) {
445             sendCommand(String.format("CTEMP %d", MAP_COLOR_TEMP[value]));
446         }
447     }
448
449     /*
450      * Flesh Color
451      */
452     public int getFleshColor() throws EpsonProjectorCommandException, EpsonProjectorException {
453         int fclr = queryInt("FCOLOR?");
454         for (int i = 0; i < MAP_FLESH_COLOR.length; i++) {
455             if (fclr == MAP_FLESH_COLOR[i]) {
456                 return i;
457             }
458         }
459         return 0;
460     }
461
462     public void setFleshColor(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
463         if (value >= 0 && value <= 6) {
464             sendCommand(String.format("FCOLOR %d", MAP_FLESH_COLOR[value]));
465         }
466     }
467
468     /*
469      * Color Mode
470      */
471     public ColorMode getColorMode() throws EpsonProjectorCommandException, EpsonProjectorException {
472         int val = queryHexInt("CMODE?");
473         return ColorMode.forValue(val);
474     }
475
476     public void setColorMode(ColorMode value) throws EpsonProjectorCommandException, EpsonProjectorException {
477         sendCommand(String.format("CMODE %02X", value.toInt()));
478     }
479
480     /*
481      * Horizontal Position
482      */
483     public int getHorizontalPosition() throws EpsonProjectorCommandException, EpsonProjectorException {
484         int hpos = queryInt("HPOS?");
485         for (int i = 0; i < MAP49.length; i++) {
486             if (hpos == MAP49[i]) {
487                 return i - 23;
488             }
489         }
490         return 0;
491     }
492
493     public void setHorizontalPosition(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
494         value = value + 23;
495         if (value >= 0 && value <= 49) {
496             sendCommand(String.format("HPOS %d", MAP49[value]));
497         }
498     }
499
500     /*
501      * Vertical Position
502      */
503     public int getVerticalPosition() throws EpsonProjectorCommandException, EpsonProjectorException {
504         int vpos = queryInt("VPOS?");
505         for (int i = 0; i < MAP18.length; i++) {
506             if (vpos == MAP18[i]) {
507                 return i - 8;
508             }
509         }
510         return 0;
511     }
512
513     public void setVerticalPosition(int value) throws EpsonProjectorCommandException, EpsonProjectorException {
514         value = value + 8;
515         if (value >= 0 && value <= 18) {
516             sendCommand(String.format("VPOS %d", MAP18[value]));
517         }
518     }
519
520     /*
521      * Gamma
522      */
523     public Gamma getGamma() throws EpsonProjectorCommandException, EpsonProjectorException {
524         int val = queryHexInt("GAMMA?");
525         return Gamma.forValue(val);
526     }
527
528     public void setGamma(Gamma value) throws EpsonProjectorCommandException, EpsonProjectorException {
529         sendCommand(String.format("GAMMA %02X", value.toInt()));
530     }
531
532     /*
533      * Volume
534      */
535     public int getVolume(int maxVolume) throws EpsonProjectorCommandException, EpsonProjectorException {
536         int vol = this.queryInt("VOL?");
537         switch (maxVolume) {
538             case 20:
539                 return this.getMappingValue(MAP20, vol);
540             case 40:
541                 return this.getMappingValue(MAP40, vol);
542         }
543         return 0;
544     }
545
546     private int getMappingValue(int[] map, int value) {
547         for (int i = 0; i < map.length; i++) {
548             if (value == map[i]) {
549                 return i;
550             }
551         }
552         return 0;
553     }
554
555     public void setVolume(int value, int maxVolume) throws EpsonProjectorCommandException, EpsonProjectorException {
556         if (value >= 0 && value <= maxVolume) {
557             switch (maxVolume) {
558                 case 20:
559                     this.sendCommand(String.format("VOL %d", MAP20[value]));
560                     return;
561                 case 40:
562                     this.sendCommand(String.format("VOL %d", MAP40[value]));
563                     return;
564             }
565         }
566     }
567
568     /*
569      * AV Mute
570      */
571     public Switch getMute() throws EpsonProjectorCommandException, EpsonProjectorException {
572         String val = queryString("MUTE?");
573         return val.equals(ON) ? Switch.ON : Switch.OFF;
574     }
575
576     public void setMute(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
577         sendCommand(String.format("MUTE %s", value.name()));
578     }
579
580     /*
581      * Horizontal Reverse
582      */
583     public Switch getHorizontalReverse() throws EpsonProjectorCommandException, EpsonProjectorException {
584         String val = queryString("HREVERSE?");
585         return val.equals(ON) ? Switch.ON : Switch.OFF;
586     }
587
588     public void setHorizontalReverse(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
589         sendCommand(String.format("HREVERSE %s", value.name()));
590     }
591
592     /*
593      * Vertical Reverse
594      */
595     public Switch getVerticalReverse() throws EpsonProjectorCommandException, EpsonProjectorException {
596         String val = queryString("VREVERSE?");
597         return val.equals(ON) ? Switch.ON : Switch.OFF;
598     }
599
600     public void setVerticalReverse(Switch value) throws EpsonProjectorCommandException, EpsonProjectorException {
601         sendCommand(String.format("VREVERSE %s", value.name()));
602     }
603
604     /*
605      * Background Select for AV Mute
606      */
607     public Background getBackground() throws EpsonProjectorCommandException, EpsonProjectorException {
608         int val = queryHexInt("MSEL?");
609         return Background.forValue(val);
610     }
611
612     public void setBackground(Background value) throws EpsonProjectorCommandException, EpsonProjectorException {
613         sendCommand(String.format("MSEL %02X", value.toInt()));
614     }
615
616     /*
617      * Lamp Time (hours) - get from cache
618      */
619     public int getLampTime() throws EpsonProjectorCommandException, EpsonProjectorException {
620         Integer lampHours = cachedLampHours.getValue();
621
622         if (lampHours != null) {
623             return lampHours.intValue();
624         } else {
625             throw new EpsonProjectorCommandException("cachedLampHours returned null");
626         }
627     }
628
629     /*
630      * Get Lamp Time
631      */
632     private @Nullable Integer queryLamp() {
633         try {
634             return Integer.valueOf(queryInt("LAMP?"));
635         } catch (EpsonProjectorCommandException | EpsonProjectorException e) {
636             logger.debug("Error executing command LAMP?", e);
637             return null;
638         }
639     }
640
641     /*
642      * Error Code
643      */
644     public int getError() throws EpsonProjectorCommandException, EpsonProjectorException {
645         return queryHexInt("ERR?");
646     }
647
648     /*
649      * Error Code Description
650      */
651     public String getErrorString() throws EpsonProjectorCommandException, EpsonProjectorException {
652         int err = queryHexInt("ERR?");
653         return ErrorMessage.forCode(err);
654     }
655 }