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 * @return {@code true} if we can communicate with the device
76 * @throws PortInUseException
77 * @throws TooManyListenersException
79 * @throws UnsupportedCommOperationException
81 public void initialize(WorkMode mode, Duration interval)
82 throws PortInUseException, TooManyListenersException, IOException, UnsupportedCommOperationException {
83 logger.trace("Initializing with mode={}, interval={}", mode, interval);
85 SerialPort localSerialPort = portId.open(thingHandler.getThing().getUID().toString(), 2000);
86 logger.trace("Port opened, object is={}", localSerialPort);
87 localSerialPort.setSerialPortParams(9600, 8, 1, 0);
88 logger.trace("Serial parameters set on port");
90 outputStream = localSerialPort.getOutputStream();
91 inputStream = localSerialPort.getInputStream();
93 if (inputStream == null || outputStream == null) {
94 throw new IOException("Could not create input or outputstream for the port");
96 logger.trace("Input and Outputstream opened for the port");
100 logger.trace("Wake up call done");
102 logger.trace("Firmware requested");
104 if (mode == WorkMode.POLLING) {
105 setMode(WorkMode.POLLING);
106 logger.trace("Polling mode set");
107 setWorkingPeriod((byte) 0);
108 logger.trace("Working period for polling set");
111 setWorkingPeriod((byte) interval.toMinutes());
112 logger.trace("Working period for reporting set");
113 setMode(WorkMode.REPORTING);
114 logger.trace("Reporting mode set");
117 this.serialPort = localSerialPort;
120 private void sendCommand(CommandMessage message) throws IOException {
121 byte[] commandData = message.getBytes();
122 if (logger.isDebugEnabled()) {
123 logger.debug("Will send command: {} ({})", HexUtils.bytesToHex(commandData), Arrays.toString(commandData));
128 } catch (IOException ioex) {
129 logger.debug("Got an exception while writing a command, will not try to fetch a reply for it.", ioex);
134 // Give the sensor some time to handle the command before doing something else with it
136 } catch (InterruptedException e) {
137 logger.warn("Interrupted while waiting after sending command={}", message);
138 Thread.currentThread().interrupt();
142 private void write(byte[] commandData) throws IOException {
143 OutputStream localOutputStream = outputStream;
144 if (localOutputStream != null) {
145 localOutputStream.write(commandData);
146 localOutputStream.flush();
150 private void setWorkingPeriod(byte period) throws IOException {
151 CommandMessage m = new CommandMessage(Command.WORKING_PERIOD, new byte[] { Constants.SET_ACTION, period });
152 logger.debug("Sending work period: {}", period);
156 private void setMode(WorkMode workMode) throws IOException {
157 byte haveToRequestData = 0;
158 if (workMode == WorkMode.POLLING) {
159 haveToRequestData = 1;
162 CommandMessage m = new CommandMessage(Command.MODE, new byte[] { Constants.SET_ACTION, haveToRequestData });
163 logger.debug("Sending mode: {}", workMode);
167 private void sendSleep(boolean doSleep) throws IOException {
168 byte payload = (byte) 1;
173 CommandMessage m = new CommandMessage(Command.SLEEP, new byte[] { Constants.SET_ACTION, payload });
174 logger.debug("Sending doSleep: {}", doSleep);
177 // as it turns out, the protocol doesn't work as described: sometimes the device just wakes up without replying
178 // to us. Hence we should not wait for a reply, but just force to wake it up to then send out our configuration
181 // sometimes the sensor does not wakeup on the first attempt, thus we try again
186 private void getFirmware() throws IOException {
187 CommandMessage m = new CommandMessage(Command.FIRMWARE, new byte[] {});
188 logger.debug("Sending get firmware request");
193 * Request data from the device
195 * @throws IOException
197 public void requestSensorData() throws IOException {
198 CommandMessage m = new CommandMessage(Command.REQUEST_DATA, new byte[] {});
199 byte[] data = m.getBytes();
200 if (logger.isDebugEnabled()) {
201 logger.debug("Requesting sensor data, will send: {}", HexUtils.bytesToHex(data));
205 Thread.sleep(200); // give the device some time to handle the command
206 } catch (InterruptedException e) {
207 logger.warn("Interrupted while waiting before reading a reply to our request data command.");
208 Thread.currentThread().interrupt();
213 private @Nullable SensorReply readReply() throws IOException {
214 byte[] readBuffer = new byte[Constants.REPLY_LENGTH];
216 InputStream localInpuStream = inputStream;
219 if (localInpuStream != null) {
220 logger.trace("Reading for reply until first byte is found");
221 while ((b = localInpuStream.read()) != Constants.MESSAGE_START_AS_INT) {
222 // logger.trace("Trying to find first reply byte now...");
224 readBuffer[0] = (byte) b;
225 int remainingBytesRead = localInpuStream.read(readBuffer, 1, Constants.REPLY_LENGTH - 1);
226 if (logger.isDebugEnabled()) {
227 logger.debug("Read remaining bytes: {}, full reply={}", remainingBytesRead,
228 HexUtils.bytesToHex(readBuffer));
229 logger.trace("Read bytes as numbers: {}", Arrays.toString(readBuffer));
231 return ReplyFactory.create(readBuffer);
236 public void readSensorData() throws IOException {
237 logger.trace("readSensorData() called");
239 boolean foundSensorData = doRead();
240 for (int i = 0; !foundSensorData && i < MAX_READ_UNTIL_SENSOR_DATA; i++) {
241 foundSensorData = doRead();
245 private boolean doRead() throws IOException {
246 SensorReply reply = readReply();
247 logger.trace("doRead(): Read reply={}", reply);
248 if (reply instanceof SensorMeasuredDataReply sensorData) {
249 logger.trace("We received sensor data");
250 if (sensorData.isValidData()) {
251 logger.trace("Sensor data is valid => updating channels");
252 thingHandler.updateChannels(sensorData);
260 * Shutdown the communication, i.e. send the device to sleep and close the serial port
262 public void dispose(boolean sendtoSleep) {
263 SerialPort localSerialPort = serialPort;
264 if (localSerialPort != null) {
266 sendDeviceToSleepOnDispose();
269 logger.debug("Closing the port now");
270 localSerialPort.close();
274 this.scheduler = null;
277 private void sendDeviceToSleepOnDispose() {
279 ScheduledExecutorService localScheduler = scheduler;
280 if (localScheduler != null) {
281 Future<?> sleepJob = null;
283 sleepJob = localScheduler.submit(() -> {
286 } catch (IOException e) {
287 logger.debug("Exception while sending sleep on disposing the communicator (will ignore it)", e);
290 sleepJob.get(5, TimeUnit.SECONDS);
291 } catch (TimeoutException e) {
292 logger.warn("Could not send device to sleep, because command takes longer than 5 seconds.");
293 sleepJob.cancel(true);
294 } catch (ExecutionException e) {
295 logger.debug("Could not execute sleep command.", e);
296 } catch (InterruptedException e) {
297 logger.debug("Sending device to sleep was interrupted.");
298 Thread.currentThread().interrupt();
301 logger.debug("Scheduler was null, could not send device to sleep.");