2 * Copyright (c) 2010-2024 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.Objects;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.sonyprojector.internal.SonyProjectorException;
29 import org.openhab.binding.sonyprojector.internal.SonyProjectorModel;
30 import org.openhab.binding.sonyprojector.internal.communication.SonyProjectorConnector;
31 import org.openhab.binding.sonyprojector.internal.communication.SonyProjectorItem;
32 import org.openhab.core.i18n.CommunicationException;
33 import org.openhab.core.i18n.ConnectionException;
34 import org.openhab.core.util.HexUtils;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
39 * Class for communicating with Sony Projectors through an IP connection
40 * using Pj Talk service (SDCP protocol)
42 * @author Markus Wehrle - Initial contribution
43 * @author Laurent Garnier - Refactoring to consider SonyProjectorConnector and add a full check of responses
46 public class SonyProjectorSdcpConnector extends SonyProjectorConnector {
48 private final Logger logger = LoggerFactory.getLogger(SonyProjectorSdcpConnector.class);
50 private static final int DEFAULT_PORT = 53484;
51 private static final String DEFAULT_COMMUNITY = "SONY";
52 private static final long READ_TIMEOUT_MS = TimeUnit.MILLISECONDS.toMillis(3500);
53 private static final int MSG_MIN_SIZE = 10;
54 private static final int MSG_MAX_SIZE = 34;
56 protected static final byte[] HEADER = new byte[] { 0x02, 0x0A };
57 private static final byte SET = 0x00;
58 private static final byte GET = 0x01;
59 protected static final byte OK = 0x01;
61 private String address;
63 private String community;
65 private @Nullable Socket clientSocket;
70 * @param address the IP address of the projector
71 * @param port the TCP port to be used
72 * @param community the community name of the equipment
73 * @param model the projector model in use
75 public SonyProjectorSdcpConnector(String address, @Nullable Integer port, @Nullable String community,
76 SonyProjectorModel model) {
77 this(address, port, community, model, false);
83 * @param address the IP address of the projector
84 * @param port the TCP port to be used
85 * @param community the community name of the equipment
86 * @param model the projector model in use
87 * @param simu whether the communication is simulated or real
89 protected SonyProjectorSdcpConnector(String address, @Nullable Integer port, @Nullable String community,
90 SonyProjectorModel model, boolean simu) {
93 this.address = address;
96 if (port != null && port > 0) {
99 this.port = DEFAULT_PORT;
103 if (community != null && !community.isEmpty() && community.length() == 4) {
104 this.community = community;
106 this.community = DEFAULT_COMMUNITY;
111 * Get the community name of the equipment
113 * @return the community name of the equipment
115 public String getCommunity() {
120 public synchronized void open() throws ConnectionException {
122 logger.debug("Opening SDCP connection IP {} port {}", this.address, this.port);
124 Socket clientSocket = new Socket(this.address, this.port);
125 clientSocket.setSoTimeout(200);
127 dataOut = new DataOutputStream(clientSocket.getOutputStream());
128 dataIn = new DataInputStream(clientSocket.getInputStream());
130 this.clientSocket = clientSocket;
134 logger.debug("SDCP connection opened");
135 } catch (IOException | SecurityException | IllegalArgumentException e) {
136 throw new ConnectionException("@text/exception.opening-sdcp-connection-failed", e);
142 public synchronized void close() {
144 logger.debug("closing SDCP connection");
146 Socket clientSocket = this.clientSocket;
147 if (clientSocket != null) {
149 clientSocket.close();
150 } catch (IOException e) {
152 this.clientSocket = null;
159 protected byte[] buildMessage(byte[] itemCode, boolean getCommand, byte[] data) {
160 byte[] communityData = community.getBytes();
161 byte[] message = new byte[10 + data.length];
162 message[0] = HEADER[0];
163 message[1] = HEADER[1];
164 message[2] = communityData[0];
165 message[3] = communityData[1];
166 message[4] = communityData[2];
167 message[5] = communityData[3];
168 message[6] = getCommand ? GET : SET;
169 message[7] = itemCode[0];
170 message[8] = itemCode[1];
171 message[9] = getCommand ? 0 : (byte) data.length;
173 System.arraycopy(data, 0, message, 10, data.length);
179 * Reads some number of bytes from the input stream and stores them into the buffer array b. The number of bytes
180 * actually read is returned as an integer.
181 * In case of socket timeout, the returned value is 0.
183 * @param dataBuffer the buffer into which the data is read.
184 * @return the total number of bytes read into the buffer, or -1 if there is no more data because the end of the
185 * stream has been reached.
186 * @throws CommunicationException if the input stream is null, if the first byte cannot be read for any reason
187 * other than the end of the file, if the input stream has been closed, or if some other I/O error
191 protected int readInput(byte[] dataBuffer) throws CommunicationException {
192 InputStream dataIn = this.dataIn;
193 if (dataIn == null) {
194 throw new CommunicationException("readInput failed: input stream is null");
197 return dataIn.read(dataBuffer);
198 } catch (SocketTimeoutException e) {
200 } catch (IOException e) {
201 logger.debug("readInput failed: {}", e.getMessage());
202 throw new CommunicationException("readInput failed", e);
207 protected synchronized byte[] readResponse() throws CommunicationException {
208 logger.debug("readResponse (timeout = {} ms)...", READ_TIMEOUT_MS);
209 byte[] message = new byte[MSG_MAX_SIZE];
210 boolean timeout = false;
211 byte[] dataBuffer = new byte[MSG_MAX_SIZE];
213 long startTimeRead = System.currentTimeMillis();
214 while ((count < MSG_MIN_SIZE) && !timeout) {
215 logger.trace("readResponse readInput...");
216 int len = readInput(dataBuffer);
217 logger.trace("readResponse readInput {} => {}", len, HexUtils.bytesToHex(dataBuffer));
219 int oldCount = count;
220 count = ((oldCount + len) > MSG_MAX_SIZE) ? MSG_MAX_SIZE : (oldCount + len);
221 System.arraycopy(dataBuffer, 0, message, oldCount, count - oldCount);
223 timeout = (System.currentTimeMillis() - startTimeRead) > READ_TIMEOUT_MS;
225 if ((count < MSG_MIN_SIZE) && timeout) {
226 logger.debug("readResponse timeout: only {} bytes read after {} ms", count, READ_TIMEOUT_MS);
227 throw new CommunicationException("readResponse failed: timeout");
229 logger.debug("readResponse: {}", HexUtils.bytesToHex(message));
230 if (count < MSG_MIN_SIZE) {
231 logger.debug("readResponse: unexpected response data length: {}", count);
232 throw new CommunicationException("Unexpected response data length");
238 protected void validateResponse(byte[] responseMessage, SonyProjectorItem item) throws CommunicationException {
239 // Check response size
240 if (responseMessage.length < MSG_MIN_SIZE) {
241 logger.debug("Unexpected response data length: {}", responseMessage.length);
242 throw new CommunicationException("Unexpected response data length");
245 // Header should be a sony projector header
246 byte[] headerMsg = Arrays.copyOf(responseMessage, HEADER.length);
247 if (!Arrays.equals(headerMsg, HEADER)) {
248 logger.debug("Unexpected HEADER in response: {} rather than {}", HexUtils.bytesToHex(headerMsg),
249 HexUtils.bytesToHex(HEADER));
250 throw new CommunicationException("Unexpected HEADER in response");
253 // Community should be the same as used for sending
254 byte[] communityResponseMsg = Arrays.copyOfRange(responseMessage, 2, 6);
255 if (!Arrays.equals(communityResponseMsg, community.getBytes())) {
256 logger.debug("Unexpected community in response: {} rather than {}",
257 HexUtils.bytesToHex(communityResponseMsg), HexUtils.bytesToHex(community.getBytes()));
258 throw new CommunicationException("Unexpected community in response");
261 // Item number should be the same as used for sending
262 byte[] itemResponseMsg = Arrays.copyOfRange(responseMessage, 7, 9);
263 byte[] itemSentMsg = Objects.requireNonNull(item.getCode());
264 if (!Arrays.equals(itemResponseMsg, itemSentMsg)) {
265 logger.debug("Unexpected item number in response: {} rather than {}", HexUtils.bytesToHex(itemResponseMsg),
266 HexUtils.bytesToHex(itemSentMsg));
267 throw new CommunicationException("Unexpected item number in response");
270 // Check response size
271 int dataLength = responseMessage[9] & 0x000000FF;
272 if (responseMessage.length < (10 + dataLength)) {
273 logger.debug("Unexpected response data length: {}", dataLength);
274 throw new CommunicationException("Unexpected response data length");
277 // byte 7 is expected to be 1, which indicates that the request was successful
278 if (responseMessage[6] != OK) {
280 if (dataLength == 12) {
281 byte[] errorCode = Arrays.copyOfRange(responseMessage, 10, 12);
283 SonyProjectorSdcpError error = SonyProjectorSdcpError.getFromDataCode(errorCode);
284 msg = error.getMessage();
285 } catch (CommunicationException e) {
288 logger.debug("{} received in response", msg);
289 throw new CommunicationException(msg + " received in response");
294 protected byte[] getResponseData(byte[] responseMessage) {
295 // Data length is in 10th byte of message
296 int dataLength = responseMessage[9] & 0x000000FF;
297 if (dataLength > 0) {
298 return Arrays.copyOfRange(responseMessage, 10, 10 + dataLength);
300 return new byte[] { (byte) 0xFF };
305 * Request the model name
307 * @return the model name
309 * @throws SonyProjectorException in case of any problem
311 public String getModelName() throws SonyProjectorException {
312 return new String(getSetting(SonyProjectorItem.MODEL_NAME), StandardCharsets.UTF_8);