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.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.i18n.CommunicationException;
32 import org.openhab.core.i18n.ConnectionException;
33 import org.openhab.core.util.HexUtils;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
38 * Class for communicating with Sony Projectors through an IP connection
39 * using Pj Talk service (SDCP protocol)
41 * @author Markus Wehrle - Initial contribution
42 * @author Laurent Garnier - Refactoring to consider SonyProjectorConnector and add a full check of responses
45 public class SonyProjectorSdcpConnector extends SonyProjectorConnector {
47 private final Logger logger = LoggerFactory.getLogger(SonyProjectorSdcpConnector.class);
49 private static final int DEFAULT_PORT = 53484;
50 private static final String DEFAULT_COMMUNITY = "SONY";
51 private static final long READ_TIMEOUT_MS = TimeUnit.MILLISECONDS.toMillis(3500);
52 private static final int MSG_MIN_SIZE = 10;
53 private static final int MSG_MAX_SIZE = 34;
55 protected static final byte[] HEADER = new byte[] { 0x02, 0x0A };
56 private static final byte SET = 0x00;
57 private static final byte GET = 0x01;
58 protected static final byte OK = 0x01;
60 private String address;
62 private String community;
64 private @Nullable Socket clientSocket;
69 * @param address the IP address of the projector
70 * @param port the TCP port to be used
71 * @param community the community name of the equipment
72 * @param model the projector model in use
74 public SonyProjectorSdcpConnector(String address, @Nullable Integer port, @Nullable String community,
75 SonyProjectorModel model) {
76 this(address, port, community, model, false);
82 * @param address the IP address of the projector
83 * @param port the TCP port to be used
84 * @param community the community name of the equipment
85 * @param model the projector model in use
86 * @param simu whether the communication is simulated or real
88 protected SonyProjectorSdcpConnector(String address, @Nullable Integer port, @Nullable String community,
89 SonyProjectorModel model, boolean simu) {
92 this.address = address;
95 if (port != null && port > 0) {
98 this.port = DEFAULT_PORT;
102 if (community != null && !community.isEmpty() && community.length() == 4) {
103 this.community = community;
105 this.community = DEFAULT_COMMUNITY;
110 * Get the community name of the equipment
112 * @return the community name of the equipment
114 public String getCommunity() {
119 public synchronized void open() throws ConnectionException {
121 logger.debug("Opening SDCP connection IP {} port {}", this.address, this.port);
123 Socket clientSocket = new Socket(this.address, this.port);
124 clientSocket.setSoTimeout(200);
126 dataOut = new DataOutputStream(clientSocket.getOutputStream());
127 dataIn = new DataInputStream(clientSocket.getInputStream());
129 this.clientSocket = clientSocket;
133 logger.debug("SDCP connection opened");
134 } catch (IOException | SecurityException | IllegalArgumentException e) {
135 throw new ConnectionException("@text/exception.opening-sdcp-connection-failed", e);
141 public synchronized void close() {
143 logger.debug("closing SDCP connection");
145 Socket clientSocket = this.clientSocket;
146 if (clientSocket != null) {
148 clientSocket.close();
149 } catch (IOException e) {
151 this.clientSocket = null;
158 protected byte[] buildMessage(SonyProjectorItem item, boolean getCommand, byte[] data) {
159 byte[] communityData = community.getBytes();
160 byte[] message = new byte[10 + data.length];
161 message[0] = HEADER[0];
162 message[1] = HEADER[1];
163 message[2] = communityData[0];
164 message[3] = communityData[1];
165 message[4] = communityData[2];
166 message[5] = communityData[3];
167 message[6] = getCommand ? GET : SET;
168 message[7] = item.getCode()[0];
169 message[8] = item.getCode()[1];
170 message[9] = getCommand ? 0 : (byte) data.length;
172 System.arraycopy(data, 0, message, 10, data.length);
178 * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
179 * actually read is returned as an integer.
180 * In case of socket timeout, the returned value is 0.
182 * @param dataBuffer the buffer into which the data is read.
183 * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
184 * stream has been reached.
185 * @throws CommunicationException if the input stream is null, if the first byte cannot be read for any reason
186 * other than the end of the file, if the input stream has been closed, or if some other I/O error
190 protected int readInput(byte[] dataBuffer) throws CommunicationException {
191 InputStream dataIn = this.dataIn;
192 if (dataIn == null) {
193 throw new CommunicationException("readInput failed: input stream is null");
196 return dataIn.read(dataBuffer);
197 } catch (SocketTimeoutException e) {
199 } catch (IOException e) {
200 logger.debug("readInput failed: {}", e.getMessage());
201 throw new CommunicationException("readInput failed", e);
206 protected synchronized byte[] readResponse() throws CommunicationException {
207 logger.debug("readResponse (timeout = {} ms)...", READ_TIMEOUT_MS);
208 byte[] message = new byte[MSG_MAX_SIZE];
209 boolean timeout = false;
210 byte[] dataBuffer = new byte[MSG_MAX_SIZE];
212 long startTimeRead = System.currentTimeMillis();
213 while ((count < MSG_MIN_SIZE) && !timeout) {
214 logger.trace("readResponse readInput...");
215 int len = readInput(dataBuffer);
216 logger.trace("readResponse readInput {} => {}", len, HexUtils.bytesToHex(dataBuffer));
218 int oldCount = count;
219 count = ((oldCount + len) > MSG_MAX_SIZE) ? MSG_MAX_SIZE : (oldCount + len);
220 System.arraycopy(dataBuffer, 0, message, oldCount, count - oldCount);
222 timeout = (System.currentTimeMillis() - startTimeRead) > READ_TIMEOUT_MS;
224 if ((count < MSG_MIN_SIZE) && timeout) {
225 logger.debug("readResponse timeout: only {} bytes read after {} ms", count, READ_TIMEOUT_MS);
226 throw new CommunicationException("readResponse failed: timeout");
228 logger.debug("readResponse: {}", HexUtils.bytesToHex(message));
229 if (count < MSG_MIN_SIZE) {
230 logger.debug("readResponse: unexpected response data length: {}", count);
231 throw new CommunicationException("Unexpected response data length");
237 protected void validateResponse(byte[] responseMessage, SonyProjectorItem item) throws CommunicationException {
238 // Check response size
239 if (responseMessage.length < MSG_MIN_SIZE) {
240 logger.debug("Unexpected response data length: {}", responseMessage.length);
241 throw new CommunicationException("Unexpected response data length");
244 // Header should be a sony projector header
245 byte[] headerMsg = Arrays.copyOf(responseMessage, HEADER.length);
246 if (!Arrays.equals(headerMsg, HEADER)) {
247 logger.debug("Unexpected HEADER in response: {} rather than {}", HexUtils.bytesToHex(headerMsg),
248 HexUtils.bytesToHex(HEADER));
249 throw new CommunicationException("Unexpected HEADER in response");
252 // Community should be the same as used for sending
253 byte[] communityResponseMsg = Arrays.copyOfRange(responseMessage, 2, 6);
254 if (!Arrays.equals(communityResponseMsg, community.getBytes())) {
255 logger.debug("Unexpected community in response: {} rather than {}",
256 HexUtils.bytesToHex(communityResponseMsg), HexUtils.bytesToHex(community.getBytes()));
257 throw new CommunicationException("Unexpected community in response");
260 // Item number should be the same as used for sending
261 byte[] itemResponseMsg = Arrays.copyOfRange(responseMessage, 7, 9);
262 if (!Arrays.equals(itemResponseMsg, item.getCode())) {
263 logger.debug("Unexpected item number in response: {} rather than {}", HexUtils.bytesToHex(itemResponseMsg),
264 HexUtils.bytesToHex(item.getCode()));
265 throw new CommunicationException("Unexpected item number in response");
268 // Check response size
269 int dataLength = responseMessage[9] & 0x000000FF;
270 if (responseMessage.length < (10 + dataLength)) {
271 logger.debug("Unexpected response data length: {}", dataLength);
272 throw new CommunicationException("Unexpected response data length");
275 // byte 7 is expected to be 1, which indicates that the request was successful
276 if (responseMessage[6] != OK) {
278 if (dataLength == 12) {
279 byte[] errorCode = Arrays.copyOfRange(responseMessage, 10, 12);
281 SonyProjectorSdcpError error = SonyProjectorSdcpError.getFromDataCode(errorCode);
282 msg = error.getMessage();
283 } catch (CommunicationException e) {
286 logger.debug("{} received in response", msg);
287 throw new CommunicationException(msg + " received in response");
292 protected byte[] getResponseData(byte[] responseMessage) {
293 // Data length is in 10th byte of message
294 int dataLength = responseMessage[9] & 0x000000FF;
295 if (dataLength > 0) {
296 return Arrays.copyOfRange(responseMessage, 10, 10 + dataLength);
298 return new byte[] { (byte) 0xFF };
303 * Request the model name
305 * @return the model name
307 * @throws SonyProjectorException in case of any problem
309 public String getModelName() throws SonyProjectorException {
310 return new String(getSetting(SonyProjectorItem.MODEL_NAME), StandardCharsets.UTF_8);