2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.onkyo.internal.eiscp;
15 import java.io.DataInputStream;
16 import java.io.IOException;
17 import java.util.Arrays;
19 import org.openhab.core.util.HexUtils;
20 import org.slf4j.Logger;
21 import org.slf4j.LoggerFactory;
24 * Class to handle Onkyo eISCP protocol.
26 * @author Pauli Anttila - Initial contribution
28 public class EiscpProtocol {
30 private static final Logger LOGGER = LoggerFactory.getLogger(EiscpProtocol.class);
33 * Wraps a command in an eISCP data message (data characters).
37 * @return String holding the full eISCP message packet
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
45 * This is where I construct the entire message character by character.
46 * Each char is represented by a 2 digit hex value
49 // the following are all in HEX representing one char
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);
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));
63 // eISCP version = "01";
64 sb.append((char) 0x01);
66 // 3 chars reserved = "00"+"00"+"00";
67 sb.append((char) 0x00);
68 sb.append((char) 0x00);
69 sb.append((char) 0x00);
76 // eISCP data - unit type char '1' is receiver
79 // eISCP data - 3 char command and param ie PWR01
83 sb.append((char) 0x0D);
85 if (LOGGER.isTraceEnabled()) {
86 String d = sb.toString();
87 LOGGER.trace("Created eISCP message: {} -> {}", HexUtils.bytesToHex(d.getBytes()), toPrintable(d));
94 * Method to read eISCP message from input stream.
99 * @throws InterruptedException
100 * @throws EiscpException
102 public static EiscpMessage getNextMessage(DataInputStream stream)
103 throws IOException, InterruptedException, EiscpException {
105 // 1st 4 chars are the lead in
107 byte firstByte = stream.readByte();
108 if (firstByte != 'I') {
109 LOGGER.trace("Expected character 'I', received '{}'",
110 toPrintable(new String(new byte[] { firstByte })));
113 if (stream.readByte() != 'S') {
116 if (stream.readByte() != 'C') {
119 if (stream.readByte() != 'P') {
124 final int headerSize = (stream.readByte() & 0xFF) << 24 | (stream.readByte() & 0xFF) << 16
125 | (stream.readByte() & 0xFF) << 8 | (stream.readByte() & 0xFF);
127 if (headerSize != 16) {
128 throw new EiscpException("Unsupported header size: " + headerSize);
132 final int dataSize = (stream.readByte() & 0xFF) << 24 | (stream.readByte() & 0xFF) << 16
133 | (stream.readByte() & 0xFF) << 8 | (stream.readByte() & 0xFF);
135 LOGGER.trace("Data size: {}", dataSize);
138 final byte versionChar = stream.readByte();
139 if (versionChar != 1) {
140 throw new EiscpException("Unsupported version " + String.valueOf(versionChar));
143 // skip 3 reserved bytes
144 byte b1 = stream.readByte();
145 byte b2 = stream.readByte();
146 byte b3 = stream.readByte();
148 byte[] data = new byte[dataSize];
149 int bytesReceived = 0;
152 while (bytesReceived < dataSize) {
153 bytesReceived = bytesReceived + stream.read(data, bytesReceived, data.length - bytesReceived);
155 if (LOGGER.isTraceEnabled()) {
156 // create header for debugging purposes
157 final StringBuilder sb = new StringBuilder();
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));
169 sb.append((char) versionChar);
171 sb.append((char) b1).append((char) b2).append((char) b3);
173 sb.append(new String(data, "UTF-8"));
174 LOGGER.trace("Received eISCP message, {} -> {}", HexUtils.bytesToHex(sb.toString().getBytes()),
175 toPrintable(sb.toString()));
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");
189 final byte startChar = data[0];
191 if (startChar != '!') {
192 throw new EiscpException("Illegal start char " + startChar);
196 @SuppressWarnings("unused")
197 final byte unitType = data[1];
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)
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
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]"
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]"
218 } else if (data[dataSize - 3] == (byte) 0x1A && data[dataSize - 2] == '\r' && data[dataSize - 1] == '\n') {
219 // skip "[EOF][CR][LF]"
221 } else if (data[dataSize - 2] == (byte) 0x1A && data[dataSize - 1] == '\r') {
224 } else if (data[dataSize - 1] == (byte) 0x1A) {
228 throw new EiscpException("Illegal end of message");
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());
241 public static String toPrintable(final String rawData) {
242 final StringBuilder sb = new StringBuilder();
244 if (rawData == null) {
248 for (final char c : rawData.toCharArray()) {
249 if (c <= 31 || c == 127) {
261 sb.append(String.format("[%02X]", (int) c));
268 return sb.toString();