2 * Copyright (c) 2010-2022 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 {
84 logger.trace("Initializing with mode={}, interval={}", mode, interval);
86 SerialPort localSerialPort = portId.open(thingHandler.getThing().getUID().toString(), 2000);
87 logger.trace("Port opened, object is={}", localSerialPort);
88 localSerialPort.setSerialPortParams(9600, 8, 1, 0);
89 logger.trace("Serial parameters set on port");
91 outputStream = localSerialPort.getOutputStream();
92 inputStream = localSerialPort.getInputStream();
94 if (inputStream == null || outputStream == null) {
95 throw new IOException("Could not create input or outputstream for the port");
97 logger.trace("Input and Outputstream opened for the port");
101 logger.trace("Wake up call done");
103 logger.trace("Firmware requested");
105 if (mode == WorkMode.POLLING) {
106 setMode(WorkMode.POLLING);
107 logger.trace("Polling mode set");
108 setWorkingPeriod((byte) 0);
109 logger.trace("Working period for polling set");
112 setWorkingPeriod((byte) interval.toMinutes());
113 logger.trace("Working period for reporting set");
114 setMode(WorkMode.REPORTING);
115 logger.trace("Reporting mode set");
118 this.serialPort = localSerialPort;
121 private void sendCommand(CommandMessage message) throws IOException {
122 byte[] commandData = message.getBytes();
123 if (logger.isDebugEnabled()) {
124 logger.debug("Will send command: {} ({})", HexUtils.bytesToHex(commandData), Arrays.toString(commandData));
129 } catch (IOException ioex) {
130 logger.debug("Got an exception while writing a command, will not try to fetch a reply for it.", ioex);
135 // Give the sensor some time to handle the command before doing something else with it
137 } catch (InterruptedException e) {
138 logger.warn("Interrupted while waiting after sending command={}", message);
139 Thread.currentThread().interrupt();
143 private void write(byte[] commandData) throws IOException {
144 OutputStream localOutputStream = outputStream;
145 if (localOutputStream != null) {
146 localOutputStream.write(commandData);
147 localOutputStream.flush();
151 private void setWorkingPeriod(byte period) throws IOException {
152 CommandMessage m = new CommandMessage(Command.WORKING_PERIOD, new byte[] { Constants.SET_ACTION, period });
153 logger.debug("Sending work period: {}", period);
157 private void setMode(WorkMode workMode) throws IOException {
158 byte haveToRequestData = 0;
159 if (workMode == WorkMode.POLLING) {
160 haveToRequestData = 1;
163 CommandMessage m = new CommandMessage(Command.MODE, new byte[] { Constants.SET_ACTION, haveToRequestData });
164 logger.debug("Sending mode: {}", workMode);
168 private void sendSleep(boolean doSleep) throws IOException {
169 byte payload = (byte) 1;
174 CommandMessage m = new CommandMessage(Command.SLEEP, new byte[] { Constants.SET_ACTION, payload });
175 logger.debug("Sending doSleep: {}", doSleep);
178 // as it turns out, the protocol doesn't work as described: sometimes the device just wakes up without replying
179 // to us. Hence we should not wait for a reply, but just force to wake it up to then send out our configuration
182 // sometimes the sensor does not wakeup on the first attempt, thus we try again
187 private void getFirmware() throws IOException {
188 CommandMessage m = new CommandMessage(Command.FIRMWARE, new byte[] {});
189 logger.debug("Sending get firmware request");
194 * Request data from the device
196 * @throws IOException
198 public void requestSensorData() throws IOException {
199 CommandMessage m = new CommandMessage(Command.REQUEST_DATA, new byte[] {});
200 byte[] data = m.getBytes();
201 if (logger.isDebugEnabled()) {
202 logger.debug("Requesting sensor data, will send: {}", HexUtils.bytesToHex(data));
206 Thread.sleep(200); // give the device some time to handle the command
207 } catch (InterruptedException e) {
208 logger.warn("Interrupted while waiting before reading a reply to our request data command.");
209 Thread.currentThread().interrupt();
214 private @Nullable SensorReply readReply() throws IOException {
215 byte[] readBuffer = new byte[Constants.REPLY_LENGTH];
217 InputStream localInpuStream = inputStream;
220 if (localInpuStream != null) {
221 logger.trace("Reading for reply until first byte is found");
222 while ((b = localInpuStream.read()) != Constants.MESSAGE_START_AS_INT) {
223 // logger.trace("Trying to find first reply byte now...");
225 readBuffer[0] = (byte) b;
226 int remainingBytesRead = localInpuStream.read(readBuffer, 1, Constants.REPLY_LENGTH - 1);
227 if (logger.isDebugEnabled()) {
228 logger.debug("Read remaining bytes: {}, full reply={}", remainingBytesRead,
229 HexUtils.bytesToHex(readBuffer));
230 logger.trace("Read bytes as numbers: {}", Arrays.toString(readBuffer));
232 return ReplyFactory.create(readBuffer);
237 public void readSensorData() throws IOException {
238 logger.trace("readSensorData() called");
240 boolean foundSensorData = doRead();
241 for (int i = 0; !foundSensorData && i < MAX_READ_UNTIL_SENSOR_DATA; i++) {
242 foundSensorData = doRead();
246 private boolean doRead() throws IOException {
247 SensorReply reply = readReply();
248 logger.trace("doRead(): Read reply={}", reply);
249 if (reply instanceof SensorMeasuredDataReply) {
250 SensorMeasuredDataReply sensorData = (SensorMeasuredDataReply) reply;
251 logger.trace("We received sensor data");
252 if (sensorData.isValidData()) {
253 logger.trace("Sensor data is valid => updating channels");
254 thingHandler.updateChannels(sensorData);
262 * Shutdown the communication, i.e. send the device to sleep and close the serial port
264 public void dispose(boolean sendtoSleep) {
265 SerialPort localSerialPort = serialPort;
266 if (localSerialPort != null) {
268 sendDeviceToSleepOnDispose();
271 logger.debug("Closing the port now");
272 localSerialPort.close();
276 this.scheduler = null;
279 private void sendDeviceToSleepOnDispose() {
281 ScheduledExecutorService localScheduler = scheduler;
282 if (localScheduler != null) {
283 Future<?> sleepJob = null;
285 sleepJob = localScheduler.submit(() -> {
288 } catch (IOException e) {
289 logger.debug("Exception while sending sleep on disposing the communicator (will ignore it)", e);
292 sleepJob.get(5, TimeUnit.SECONDS);
293 } catch (TimeoutException e) {
294 logger.warn("Could not send device to sleep, because command takes longer than 5 seconds.");
295 sleepJob.cancel(true);
296 } catch (ExecutionException e) {
297 logger.debug("Could not execute sleep command.", e);
298 } catch (InterruptedException e) {
299 logger.debug("Sending device to sleep was interrupted.");
300 Thread.currentThread().interrupt();
303 logger.debug("Scheduler was null, could not send device to sleep.");