2 * Copyright (c) 2010-2021 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.sonyprojector.internal.communication.sdcp;
15 import java.io.DataInputStream;
16 import java.io.DataOutputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.net.Socket;
20 import java.net.SocketTimeoutException;
21 import java.nio.charset.StandardCharsets;
22 import java.util.Arrays;
23 import java.util.concurrent.TimeUnit;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.sonyprojector.internal.SonyProjectorException;
28 import org.openhab.binding.sonyprojector.internal.SonyProjectorModel;
29 import org.openhab.binding.sonyprojector.internal.communication.SonyProjectorConnector;
30 import org.openhab.binding.sonyprojector.internal.communication.SonyProjectorItem;
31 import org.openhab.core.util.HexUtils;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
36 * Class for communicating with Sony Projectors through an IP connection
37 * using Pj Talk service (SDCP protocol)
39 * @author Markus Wehrle - Initial contribution
40 * @author Laurent Garnier - Refactoring to consider SonyProjectorConnector and add a full check of responses
43 public class SonyProjectorSdcpConnector extends SonyProjectorConnector {
45 private final Logger logger = LoggerFactory.getLogger(SonyProjectorSdcpConnector.class);
47 private static final int DEFAULT_PORT = 53484;
48 private static final String DEFAULT_COMMUNITY = "SONY";
49 private static final long READ_TIMEOUT_MS = TimeUnit.MILLISECONDS.toMillis(3500);
50 private static final int MSG_MIN_SIZE = 10;
51 private static final int MSG_MAX_SIZE = 34;
53 protected static final byte[] HEADER = new byte[] { 0x02, 0x0A };
54 private static final byte SET = 0x00;
55 private static final byte GET = 0x01;
56 protected static final byte OK = 0x01;
58 private String address;
60 private String community;
62 private @Nullable Socket clientSocket;
67 * @param address the IP address of the projector
68 * @param port the TCP port to be used
69 * @param community the community name of the equipment
70 * @param model the projector model in use
72 public SonyProjectorSdcpConnector(String address, @Nullable Integer port, @Nullable String community,
73 SonyProjectorModel model) {
74 this(address, port, community, model, false);
80 * @param address the IP address of the projector
81 * @param port the TCP port to be used
82 * @param community the community name of the equipment
83 * @param model the projector model in use
84 * @param simu whether the communication is simulated or real
86 protected SonyProjectorSdcpConnector(String address, @Nullable Integer port, @Nullable String community,
87 SonyProjectorModel model, boolean simu) {
90 this.address = address;
93 if (port != null && port > 0) {
96 this.port = DEFAULT_PORT;
100 if (community != null && !community.isEmpty() && community.length() == 4) {
101 this.community = community;
103 this.community = DEFAULT_COMMUNITY;
108 * Get the community name of the equipment
110 * @return the community name of the equipment
112 public String getCommunity() {
117 public synchronized void open() throws SonyProjectorException {
119 logger.debug("Opening SDCP connection IP {} port {}", this.address, this.port);
121 Socket clientSocket = new Socket(this.address, this.port);
122 clientSocket.setSoTimeout(200);
124 dataOut = new DataOutputStream(clientSocket.getOutputStream());
125 dataIn = new DataInputStream(clientSocket.getInputStream());
127 this.clientSocket = clientSocket;
131 logger.debug("SDCP connection opened");
132 } catch (IOException | SecurityException | IllegalArgumentException e) {
133 logger.debug("Opening SDCP connection failed: {}", e.getMessage());
134 throw new SonyProjectorException("Opening SDCP connection failed: " + e.getMessage());
140 public synchronized void close() {
142 logger.debug("closing SDCP connection");
144 Socket clientSocket = this.clientSocket;
145 if (clientSocket != null) {
147 clientSocket.close();
148 } catch (IOException e) {
150 this.clientSocket = null;
157 protected byte[] buildMessage(SonyProjectorItem item, boolean getCommand, byte[] data) {
158 byte[] communityData = community.getBytes();
159 byte[] message = new byte[10 + data.length];
160 message[0] = HEADER[0];
161 message[1] = HEADER[1];
162 message[2] = communityData[0];
163 message[3] = communityData[1];
164 message[4] = communityData[2];
165 message[5] = communityData[3];
166 message[6] = getCommand ? GET : SET;
167 message[7] = item.getCode()[0];
168 message[8] = item.getCode()[1];
169 message[9] = getCommand ? 0 : (byte) data.length;
171 System.arraycopy(data, 0, message, 10, data.length);
177 * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
178 * actually read is returned as an integer.
179 * In case of socket timeout, the returned value is 0.
181 * @param dataBuffer the buffer into which the data is read.
182 * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
183 * stream has been reached.
184 * @throws SonyProjectorException - If the input stream is null, if the first byte cannot be read for any reason
185 * other than the end of the file, if the input stream has been closed, or if some other I/O error
189 protected int readInput(byte[] dataBuffer) throws SonyProjectorException {
190 InputStream dataIn = this.dataIn;
191 if (dataIn == null) {
192 throw new SonyProjectorException("readInput failed: input stream is null");
195 return dataIn.read(dataBuffer);
196 } catch (SocketTimeoutException e) {
198 } catch (IOException e) {
199 logger.debug("readInput failed: {}", e.getMessage());
200 throw new SonyProjectorException("readInput failed: " + e.getMessage());
205 protected synchronized byte[] readResponse() throws SonyProjectorException {
206 logger.debug("readResponse (timeout = {} ms)...", READ_TIMEOUT_MS);
207 byte[] message = new byte[MSG_MAX_SIZE];
208 boolean timeout = false;
209 byte[] dataBuffer = new byte[MSG_MAX_SIZE];
211 long startTimeRead = System.currentTimeMillis();
212 while ((count < MSG_MIN_SIZE) && !timeout) {
213 logger.trace("readResponse readInput...");
214 int len = readInput(dataBuffer);
215 logger.trace("readResponse readInput {} => {}", len, HexUtils.bytesToHex(dataBuffer));
217 int oldCount = count;
218 count = ((oldCount + len) > MSG_MAX_SIZE) ? MSG_MAX_SIZE : (oldCount + len);
219 System.arraycopy(dataBuffer, 0, message, oldCount, count - oldCount);
221 timeout = (System.currentTimeMillis() - startTimeRead) > READ_TIMEOUT_MS;
223 if ((count < MSG_MIN_SIZE) && timeout) {
224 logger.debug("readResponse timeout: only {} bytes read after {} ms", count, READ_TIMEOUT_MS);
225 throw new SonyProjectorException("readResponse failed: timeout");
227 logger.debug("readResponse: {}", HexUtils.bytesToHex(message));
228 if (count < MSG_MIN_SIZE) {
229 logger.debug("readResponse: unexpected response data length: {}", count);
230 throw new SonyProjectorException("Unexpected response data length");
236 protected void validateResponse(byte[] responseMessage, SonyProjectorItem item) throws SonyProjectorException {
237 // Check response size
238 if (responseMessage.length < MSG_MIN_SIZE) {
239 logger.debug("Unexpected response data length: {}", responseMessage.length);
240 throw new SonyProjectorException("Unexpected response data length");
243 // Header should be a sony projector header
244 byte[] headerMsg = Arrays.copyOf(responseMessage, HEADER.length);
245 if (!Arrays.equals(headerMsg, HEADER)) {
246 logger.debug("Unexpected HEADER in response: {} rather than {}", HexUtils.bytesToHex(headerMsg),
247 HexUtils.bytesToHex(HEADER));
248 throw new SonyProjectorException("Unexpected HEADER in response");
251 // Community should be the same as used for sending
252 byte[] communityResponseMsg = Arrays.copyOfRange(responseMessage, 2, 6);
253 if (!Arrays.equals(communityResponseMsg, community.getBytes())) {
254 logger.debug("Unexpected community in response: {} rather than {}",
255 HexUtils.bytesToHex(communityResponseMsg), HexUtils.bytesToHex(community.getBytes()));
256 throw new SonyProjectorException("Unexpected community in response");
259 // Item number should be the same as used for sending
260 byte[] itemResponseMsg = Arrays.copyOfRange(responseMessage, 7, 9);
261 if (!Arrays.equals(itemResponseMsg, item.getCode())) {
262 logger.debug("Unexpected item number in response: {} rather than {}", HexUtils.bytesToHex(itemResponseMsg),
263 HexUtils.bytesToHex(item.getCode()));
264 throw new SonyProjectorException("Unexpected item number in response");
267 // Check response size
268 int dataLength = responseMessage[9] & 0x000000FF;
269 if (responseMessage.length < (10 + dataLength)) {
270 logger.debug("Unexpected response data length: {}", dataLength);
271 throw new SonyProjectorException("Unexpected response data length");
274 // byte 7 is expected to be 1, which indicates that the request was successful
275 if (responseMessage[6] != OK) {
277 if (dataLength == 12) {
278 byte[] errorCode = Arrays.copyOfRange(responseMessage, 10, 12);
280 SonyProjectorSdcpError error = SonyProjectorSdcpError.getFromDataCode(errorCode);
281 msg = error.getMessage();
282 } catch (SonyProjectorException e) {
285 logger.debug("{} received in response", msg);
286 throw new SonyProjectorException(msg + " received in response");
291 protected byte[] getResponseData(byte[] responseMessage) {
292 // Data length is in 10th byte of message
293 int dataLength = responseMessage[9] & 0x000000FF;
294 if (dataLength > 0) {
295 return Arrays.copyOfRange(responseMessage, 10, 10 + dataLength);
297 return new byte[] { (byte) 0xFF };
302 * Request the model name
304 * @return the model name
306 * @throws SonyProjectorException - In case of any problem
308 public String getModelName() throws SonyProjectorException {
309 return new String(getSetting(SonyProjectorItem.MODEL_NAME), StandardCharsets.UTF_8);