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.novafinedust.internal.sds011protocol;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.OutputStream;
18 import java.time.Duration;
19 import java.util.Arrays;
20 import java.util.TooManyListenersException;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.Future;
23 import java.util.concurrent.ScheduledExecutorService;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.TimeoutException;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.novafinedust.internal.SDS011Handler;
30 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.CommandMessage;
31 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.Constants;
32 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply;
33 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorReply;
34 import org.openhab.core.io.transport.serial.PortInUseException;
35 import org.openhab.core.io.transport.serial.SerialPort;
36 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
37 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
38 import org.openhab.core.util.HexUtils;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
43 * Central instance to communicate with the device, i.e. receive data from it and send commands to it
45 * @author Stefan Triller - Initial contribution
49 public class SDS011Communicator {
51 private static final int MAX_READ_UNTIL_SENSOR_DATA = 6; // at least 6 because we send 5 configuration commands
53 private final Logger logger = LoggerFactory.getLogger(SDS011Communicator.class);
55 private SerialPortIdentifier portId;
56 private SDS011Handler thingHandler;
57 private @Nullable SerialPort serialPort;
59 private @Nullable OutputStream outputStream;
60 private @Nullable InputStream inputStream;
61 private @Nullable ScheduledExecutorService scheduler;
63 public SDS011Communicator(SDS011Handler thingHandler, SerialPortIdentifier portId,
64 ScheduledExecutorService scheduler) {
65 this.thingHandler = thingHandler;
67 this.scheduler = scheduler;
71 * Initialize the communication with the device, i.e. open the serial port etc.
73 * @param mode the {@link WorkMode} if we want to use polling or reporting
74 * @param interval the time between polling or reportings
75 * @throws PortInUseException
76 * @throws TooManyListenersException
78 * @throws UnsupportedCommOperationException
80 public void initialize(WorkMode mode, Duration interval)
81 throws PortInUseException, TooManyListenersException, IOException, UnsupportedCommOperationException {
82 logger.trace("Initializing with mode={}, interval={}", mode, interval);
84 SerialPort localSerialPort = portId.open(thingHandler.getThing().getUID().toString(), 2000);
85 logger.trace("Port opened, object is={}", localSerialPort);
86 localSerialPort.setSerialPortParams(9600, 8, 1, 0);
87 logger.trace("Serial parameters set on port");
89 outputStream = localSerialPort.getOutputStream();
90 inputStream = localSerialPort.getInputStream();
92 if (inputStream == null || outputStream == null) {
93 throw new IOException("Could not create input or outputstream for the port");
95 logger.trace("Input and Outputstream opened for the port");
99 logger.trace("Wake up call done");
101 logger.trace("Firmware requested");
103 if (mode == WorkMode.POLLING) {
104 setMode(WorkMode.POLLING);
105 logger.trace("Polling mode set");
106 setWorkingPeriod((byte) 0);
107 logger.trace("Working period for polling set");
110 setWorkingPeriod((byte) interval.toMinutes());
111 logger.trace("Working period for reporting set");
112 setMode(WorkMode.REPORTING);
113 logger.trace("Reporting mode set");
116 this.serialPort = localSerialPort;
119 private void sendCommand(CommandMessage message) throws IOException {
120 byte[] commandData = message.getBytes();
121 if (logger.isDebugEnabled()) {
122 logger.debug("Will send command: {} ({})", HexUtils.bytesToHex(commandData), Arrays.toString(commandData));
127 } catch (IOException ioex) {
128 logger.debug("Got an exception while writing a command, will not try to fetch a reply for it.", ioex);
133 // Give the sensor some time to handle the command before doing something else with it
135 } catch (InterruptedException e) {
136 logger.warn("Interrupted while waiting after sending command={}", message);
137 Thread.currentThread().interrupt();
141 private void write(byte[] commandData) throws IOException {
142 OutputStream localOutputStream = outputStream;
143 if (localOutputStream != null) {
144 localOutputStream.write(commandData);
145 localOutputStream.flush();
149 private void setWorkingPeriod(byte period) throws IOException {
150 CommandMessage m = new CommandMessage(Command.WORKING_PERIOD, new byte[] { Constants.SET_ACTION, period });
151 logger.debug("Sending work period: {}", period);
155 private void setMode(WorkMode workMode) throws IOException {
156 byte haveToRequestData = 0;
157 if (workMode == WorkMode.POLLING) {
158 haveToRequestData = 1;
161 CommandMessage m = new CommandMessage(Command.MODE, new byte[] { Constants.SET_ACTION, haveToRequestData });
162 logger.debug("Sending mode: {}", workMode);
166 private void sendSleep(boolean doSleep) throws IOException {
167 byte payload = (byte) 1;
172 CommandMessage m = new CommandMessage(Command.SLEEP, new byte[] { Constants.SET_ACTION, payload });
173 logger.debug("Sending doSleep: {}", doSleep);
176 // as it turns out, the protocol doesn't work as described: sometimes the device just wakes up without replying
177 // to us. Hence we should not wait for a reply, but just force to wake it up to then send out our configuration
180 // sometimes the sensor does not wakeup on the first attempt, thus we try again
185 private void getFirmware() throws IOException {
186 CommandMessage m = new CommandMessage(Command.FIRMWARE, new byte[] {});
187 logger.debug("Sending get firmware request");
192 * Request data from the device
194 * @throws IOException
196 public void requestSensorData() throws IOException {
197 CommandMessage m = new CommandMessage(Command.REQUEST_DATA, new byte[] {});
198 byte[] data = m.getBytes();
199 if (logger.isDebugEnabled()) {
200 logger.debug("Requesting sensor data, will send: {}", HexUtils.bytesToHex(data));
204 Thread.sleep(200); // give the device some time to handle the command
205 } catch (InterruptedException e) {
206 logger.warn("Interrupted while waiting before reading a reply to our request data command.");
207 Thread.currentThread().interrupt();
212 private @Nullable SensorReply readReply() throws IOException {
213 byte[] readBuffer = new byte[Constants.REPLY_LENGTH];
215 InputStream localInpuStream = inputStream;
218 if (localInpuStream != null) {
219 logger.trace("Reading for reply until first byte is found");
220 while ((b = localInpuStream.read()) != Constants.MESSAGE_START_AS_INT) {
221 // logger.trace("Trying to find first reply byte now...");
223 readBuffer[0] = (byte) b;
224 int remainingBytesRead = localInpuStream.read(readBuffer, 1, Constants.REPLY_LENGTH - 1);
225 if (logger.isDebugEnabled()) {
226 logger.debug("Read remaining bytes: {}, full reply={}", remainingBytesRead,
227 HexUtils.bytesToHex(readBuffer));
228 logger.trace("Read bytes as numbers: {}", Arrays.toString(readBuffer));
230 return ReplyFactory.create(readBuffer);
235 public void readSensorData() throws IOException {
236 logger.trace("readSensorData() called");
238 boolean foundSensorData = doRead();
239 for (int i = 0; !foundSensorData && i < MAX_READ_UNTIL_SENSOR_DATA; i++) {
240 foundSensorData = doRead();
244 private boolean doRead() throws IOException {
245 SensorReply reply = readReply();
246 logger.trace("doRead(): Read reply={}", reply);
247 if (reply instanceof SensorMeasuredDataReply sensorData) {
248 logger.trace("We received sensor data");
249 if (sensorData.isValidData()) {
250 logger.trace("Sensor data is valid => updating channels");
251 thingHandler.updateChannels(sensorData);
259 * Shutdown the communication, i.e. send the device to sleep and close the serial port
261 public void dispose(boolean sendtoSleep) {
262 SerialPort localSerialPort = serialPort;
263 if (localSerialPort != null) {
265 sendDeviceToSleepOnDispose();
268 logger.debug("Closing the port now");
269 localSerialPort.close();
273 this.scheduler = null;
276 private void sendDeviceToSleepOnDispose() {
278 ScheduledExecutorService localScheduler = scheduler;
279 if (localScheduler != null) {
280 Future<?> sleepJob = null;
282 sleepJob = localScheduler.submit(() -> {
285 } catch (IOException e) {
286 logger.debug("Exception while sending sleep on disposing the communicator (will ignore it)", e);
289 sleepJob.get(5, TimeUnit.SECONDS);
290 } catch (TimeoutException e) {
291 logger.warn("Could not send device to sleep, because command takes longer than 5 seconds.");
292 sleepJob.cancel(true);
293 } catch (ExecutionException e) {
294 logger.debug("Could not execute sleep command.", e);
295 } catch (InterruptedException e) {
296 logger.debug("Sending device to sleep was interrupted.");
297 Thread.currentThread().interrupt();
300 logger.debug("Scheduler was null, could not send device to sleep.");