]> git.basschouten.com Git - openhab-addons.git/blob
f986e47df28b0e7ef4b47654c7f8fe3eb1ca9341
[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.onkyo.internal.eiscp;
14
15 import java.io.DataInputStream;
16 import java.io.IOException;
17 import java.util.Arrays;
18
19 import org.openhab.core.util.HexUtils;
20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
22
23 /**
24  * Class to handle Onkyo eISCP protocol.
25  *
26  * @author Pauli Anttila - Initial contribution
27  */
28 public class EiscpProtocol {
29
30     private static final Logger LOGGER = LoggerFactory.getLogger(EiscpProtocol.class);
31
32     /**
33      * Wraps a command in an eISCP data message (data characters).
34      *
35      * @param msg
36      *            eISCP command.
37      * @return String holding the full eISCP message packet
38      */
39     public static String createEiscpPdu(EiscpMessage msg) {
40         String data = msg.getCommand() + msg.getValue();
41         StringBuilder sb = new StringBuilder();
42         int eiscpDataSize = 2 + data.length() + 1; // this is the eISCP data size
43
44         /*
45          * This is where I construct the entire message character by character.
46          * Each char is represented by a 2 digit hex value
47          */
48         sb.append("ISCP");
49         // the following are all in HEX representing one char
50
51         // 4 char Big Endian Header
52         sb.append((char) 0x00);
53         sb.append((char) 0x00);
54         sb.append((char) 0x00);
55         sb.append((char) 0x10);
56
57         // 4 char Big Endian data size
58         sb.append((char) ((eiscpDataSize >> 24) & 0xFF));
59         sb.append((char) ((eiscpDataSize >> 16) & 0xFF));
60         sb.append((char) ((eiscpDataSize >> 8) & 0xFF));
61         sb.append((char) (eiscpDataSize & 0xFF));
62
63         // eISCP version = "01";
64         sb.append((char) 0x01);
65
66         // 3 chars reserved = "00"+"00"+"00";
67         sb.append((char) 0x00);
68         sb.append((char) 0x00);
69         sb.append((char) 0x00);
70
71         // eISCP data
72
73         // Start Character
74         sb.append("!");
75
76         // eISCP data - unit type char '1' is receiver
77         sb.append("1");
78
79         // eISCP data - 3 char command and param ie PWR01
80         sb.append(data);
81
82         // msg end - EOF
83         sb.append((char) 0x0D);
84
85         if (LOGGER.isTraceEnabled()) {
86             String d = sb.toString();
87             LOGGER.trace("Created eISCP message: {} -> {}", HexUtils.bytesToHex(d.getBytes()), toPrintable(d));
88         }
89
90         return sb.toString();
91     }
92
93     /**
94      * Method to read eISCP message from input stream.
95      *
96      * @return message
97      *
98      * @throws IOException
99      * @throws InterruptedException
100      * @throws EiscpException
101      */
102     public static EiscpMessage getNextMessage(DataInputStream stream)
103             throws IOException, InterruptedException, EiscpException {
104         while (true) {
105             // 1st 4 chars are the lead in
106
107             byte firstByte = stream.readByte();
108             if (firstByte != 'I') {
109                 LOGGER.trace("Expected character 'I', received '{}'",
110                         toPrintable(new String(new byte[] { firstByte })));
111                 continue;
112             }
113             if (stream.readByte() != 'S') {
114                 continue;
115             }
116             if (stream.readByte() != 'C') {
117                 continue;
118             }
119             if (stream.readByte() != 'P') {
120                 continue;
121             }
122
123             // header size
124             final int headerSize = (stream.readByte() & 0xFF) << 24 | (stream.readByte() & 0xFF) << 16
125                     | (stream.readByte() & 0xFF) << 8 | (stream.readByte() & 0xFF);
126
127             if (headerSize != 16) {
128                 throw new EiscpException("Unsupported header size: " + headerSize);
129             }
130
131             // data size
132             final int dataSize = (stream.readByte() & 0xFF) << 24 | (stream.readByte() & 0xFF) << 16
133                     | (stream.readByte() & 0xFF) << 8 | (stream.readByte() & 0xFF);
134
135             LOGGER.trace("Data size: {}", dataSize);
136
137             // version
138             final byte versionChar = stream.readByte();
139             if (versionChar != 1) {
140                 throw new EiscpException("Unsupported version " + versionChar);
141             }
142
143             // skip 3 reserved bytes
144             byte b1 = stream.readByte();
145             byte b2 = stream.readByte();
146             byte b3 = stream.readByte();
147
148             byte[] data = new byte[dataSize];
149             int bytesReceived = 0;
150
151             try {
152                 while (bytesReceived < dataSize) {
153                     bytesReceived = bytesReceived + stream.read(data, bytesReceived, data.length - bytesReceived);
154
155                     if (LOGGER.isTraceEnabled()) {
156                         // create header for debugging purposes
157                         final StringBuilder sb = new StringBuilder();
158                         sb.append("ISCP");
159                         sb.append((char) 0x00);
160                         sb.append((char) 0x00);
161                         sb.append((char) 0x00);
162                         sb.append((char) 0x10);
163                         // 4 char Big Endian data size
164                         sb.append((char) ((dataSize >> 24) & 0xFF));
165                         sb.append((char) ((dataSize >> 16) & 0xFF));
166                         sb.append((char) ((dataSize >> 8) & 0xFF));
167                         sb.append((char) (dataSize & 0xFF));
168                         // eiscp version;
169                         sb.append((char) versionChar);
170                         // reserved bytes
171                         sb.append((char) b1).append((char) b2).append((char) b3);
172                         // data
173                         sb.append(new String(data, "UTF-8"));
174                         LOGGER.trace("Received eISCP message, {} -> {}", HexUtils.bytesToHex(sb.toString().getBytes()),
175                                 toPrintable(sb.toString()));
176                     }
177                 }
178             } catch (IOException t) {
179                 if (bytesReceived != dataSize) {
180                     LOGGER.debug("Received bad data: '{}'", toPrintable(new String(data, "UTF-8")));
181                     throw new EiscpException(
182                             "Data missing, expected + " + dataSize + " received " + bytesReceived + " bytes");
183                 } else {
184                     throw t;
185                 }
186             }
187
188             // start char
189             final byte startChar = data[0];
190
191             if (startChar != '!') {
192                 throw new EiscpException("Illegal start char " + startChar);
193             }
194
195             // unit type
196             @SuppressWarnings("unused")
197             final byte unitType = data[1];
198
199             // data should be end to "[EOF]" or "[EOF][CR]" or "[EOF][CR][LF]" characters depend on model
200             // [EOF] End of File ASCII Code 0x1A
201             // [CR] Carriage Return ASCII Code 0x0D (\r)
202             // [LF] Line Feed ASCII Code 0x0A (\n)
203
204             int endBytes = 0;
205
206             // TODO: Simplify this by implementation, which find [EOF] character and ignore rest of the bytes after
207             // that. But before that, proper junit test should be implement to be sure that it does not broke
208             // anything.
209
210             if (data[dataSize - 5] == (byte) 0x1A && data[dataSize - 4] == '\n' && data[dataSize - 3] == '\n'
211                     && data[dataSize - 2] == '\r' && data[dataSize - 1] == '\n') {
212                 // skip "[EOF][LF][LF][CR][LF]"
213                 endBytes = 5;
214             } else if (data[dataSize - 4] == (byte) 0x1A && data[dataSize - 3] == '\r' && data[dataSize - 2] == '\n'
215                     && data[dataSize - 1] == 0x00) {
216                 // skip "[EOF][CR][LF][NULL]"
217                 endBytes = 4;
218             } else if (data[dataSize - 3] == (byte) 0x1A && data[dataSize - 2] == '\r' && data[dataSize - 1] == '\n') {
219                 // skip "[EOF][CR][LF]"
220                 endBytes = 3;
221             } else if (data[dataSize - 2] == (byte) 0x1A && data[dataSize - 1] == '\r') {
222                 // "[EOF][CR]"
223                 endBytes = 2;
224             } else if (data[dataSize - 1] == (byte) 0x1A) {
225                 // "[EOF]"
226                 endBytes = 1;
227             } else {
228                 throw new EiscpException("Illegal end of message");
229             }
230
231             try {
232                 String command = new String(Arrays.copyOfRange(data, 2, 5));
233                 String value = new String(Arrays.copyOfRange(data, 5, data.length - endBytes));
234                 return new EiscpMessage.MessageBuilder().command(command).value(value).build();
235             } catch (Exception e) {
236                 throw new EiscpException("Fatal error occurred when parsing eISCP message, cause=" + e.getCause());
237             }
238         }
239     }
240
241     public static String toPrintable(final String rawData) {
242         final StringBuilder sb = new StringBuilder();
243
244         if (rawData == null) {
245             return "";
246         }
247
248         for (final char c : rawData.toCharArray()) {
249             if (c <= 31 || c == 127) {
250                 switch (c) {
251                     case '\r':
252                         sb.append("[CR]");
253                         break;
254                     case '\n':
255                         sb.append("[LF]");
256                         break;
257                     case (byte) 0x1A:
258                         sb.append("[EOF]");
259                         break;
260                     default:
261                         sb.append(String.format("[%02X]", (int) c));
262                 }
263             } else {
264                 sb.append(c);
265             }
266         }
267
268         return sb.toString();
269     }
270 }