2 * Copyright (c) 2010-2021 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.ModeReply;
33 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorFirmwareReply;
34 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply;
35 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorReply;
36 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SleepReply;
37 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.WorkingPeriodReply;
38 import org.openhab.core.io.transport.serial.PortInUseException;
39 import org.openhab.core.io.transport.serial.SerialPort;
40 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
41 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
42 import org.openhab.core.util.HexUtils;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
47 * Central instance to communicate with the device, i.e. receive data from it and send commands to it
49 * @author Stefan Triller - Initial contribution
53 public class SDS011Communicator {
55 private static final int MAX_SENDOR_REPORTINGS_UNTIL_EXPECTED_REPLY = 20;
57 private final Logger logger = LoggerFactory.getLogger(SDS011Communicator.class);
59 private SerialPortIdentifier portId;
60 private SDS011Handler thingHandler;
61 private @Nullable SerialPort serialPort;
63 private @Nullable OutputStream outputStream;
64 private @Nullable InputStream inputStream;
65 private @Nullable ScheduledExecutorService scheduler;
67 public SDS011Communicator(SDS011Handler thingHandler, SerialPortIdentifier portId,
68 ScheduledExecutorService scheduler) {
69 this.thingHandler = thingHandler;
71 this.scheduler = scheduler;
75 * Initialize the communication with the device, i.e. open the serial port etc.
77 * @param mode the {@link WorkMode} if we want to use polling or reporting
78 * @param interval the time between polling or reportings
79 * @return {@code true} if we can communicate with the device
80 * @throws PortInUseException
81 * @throws TooManyListenersException
83 * @throws UnsupportedCommOperationException
85 public boolean initialize(WorkMode mode, Duration interval)
86 throws PortInUseException, TooManyListenersException, IOException, UnsupportedCommOperationException {
87 boolean initSuccessful = true;
89 logger.trace("Initializing with mode={}, interval={}", mode, interval);
91 SerialPort localSerialPort = portId.open(thingHandler.getThing().getUID().toString(), 2000);
92 logger.trace("Port opened, object is={}", localSerialPort);
93 localSerialPort.setSerialPortParams(9600, 8, 1, 0);
94 logger.trace("Serial parameters set on port");
96 outputStream = localSerialPort.getOutputStream();
97 inputStream = localSerialPort.getInputStream();
99 if (inputStream == null || outputStream == null) {
100 throw new IOException("Could not create input or outputstream for the port");
102 logger.trace("Input and Outputstream opened for the port");
104 // wake up the device
105 initSuccessful &= sendSleep(false);
106 logger.trace("Wake up call done, initSuccessful={}", initSuccessful);
107 initSuccessful &= getFirmware();
108 logger.trace("Firmware requested, initSuccessful={}", initSuccessful);
110 if (mode == WorkMode.POLLING) {
111 initSuccessful &= setMode(WorkMode.POLLING);
112 logger.trace("Polling mode set, initSuccessful={}", initSuccessful);
113 initSuccessful &= setWorkingPeriod((byte) 0);
114 logger.trace("Working period for polling set, initSuccessful={}", initSuccessful);
117 initSuccessful &= setWorkingPeriod((byte) interval.toMinutes());
118 logger.trace("Working period for reporting set, initSuccessful={}", initSuccessful);
119 initSuccessful &= setMode(WorkMode.REPORTING);
120 logger.trace("Reporting mode set, initSuccessful={}", initSuccessful);
123 this.serialPort = localSerialPort;
125 return initSuccessful;
128 private @Nullable SensorReply sendCommand(CommandMessage message) throws IOException {
129 byte[] commandData = message.getBytes();
130 if (logger.isDebugEnabled()) {
131 logger.debug("Will send command: {} ({})", HexUtils.bytesToHex(commandData), Arrays.toString(commandData));
136 } catch (IOException ioex) {
137 logger.debug("Got an exception while writing a command, will not try to fetch a reply for it.", ioex);
142 // Give the sensor some time to handle the command
144 } catch (InterruptedException e) {
145 logger.warn("Problem while waiting for reading a reply to our command.");
146 Thread.currentThread().interrupt();
148 SensorReply reply = readReply();
149 // in case there is still another reporting active, we want to discard the sensor data and read the reply to our
150 // command again, this might happen more often in case the sensor has buffered some data
151 for (int i = 0; i < MAX_SENDOR_REPORTINGS_UNTIL_EXPECTED_REPLY; i++) {
152 if (reply instanceof SensorMeasuredDataReply) {
161 private void write(byte[] commandData) throws IOException {
162 OutputStream localOutputStream = outputStream;
163 if (localOutputStream != null) {
164 localOutputStream.write(commandData);
165 localOutputStream.flush();
169 private boolean setWorkingPeriod(byte period) throws IOException {
170 CommandMessage m = new CommandMessage(Command.WORKING_PERIOD, new byte[] { Constants.SET_ACTION, period });
171 logger.debug("Sending work period: {}", period);
172 SensorReply reply = sendCommand(m);
173 logger.debug("Got reply to setWorkingPeriod command: {}", reply);
174 if (reply instanceof WorkingPeriodReply) {
175 WorkingPeriodReply wpReply = (WorkingPeriodReply) reply;
176 if (wpReply.getPeriod() == period && wpReply.getActionType() == Constants.SET_ACTION) {
183 private boolean setMode(WorkMode workMode) throws IOException {
184 byte haveToRequestData = 0;
185 if (workMode == WorkMode.POLLING) {
186 haveToRequestData = 1;
189 CommandMessage m = new CommandMessage(Command.MODE, new byte[] { Constants.SET_ACTION, haveToRequestData });
190 logger.debug("Sending mode: {}", workMode);
191 SensorReply reply = sendCommand(m);
192 logger.debug("Got reply to setMode command: {}", reply);
193 if (reply instanceof ModeReply) {
194 ModeReply mr = (ModeReply) reply;
195 if (mr.getActionType() == Constants.SET_ACTION && mr.getMode() == workMode) {
202 private boolean sendSleep(boolean doSleep) throws IOException {
203 byte payload = (byte) 1;
208 CommandMessage m = new CommandMessage(Command.SLEEP, new byte[] { Constants.SET_ACTION, payload });
209 logger.debug("Sending doSleep: {}", doSleep);
210 SensorReply reply = sendCommand(m);
211 logger.debug("Got reply to sendSleep command: {}", reply);
214 // sometimes the sensor does not wakeup on the first attempt, thus we try again
215 for (int i = 0; reply == null && i < 3; i++) {
216 reply = sendCommand(m);
217 logger.debug("Got reply to sendSleep command after retry#{}: {}", i + 1, reply);
221 if (reply instanceof SleepReply) {
222 SleepReply sr = (SleepReply) reply;
223 if (sr.getActionType() == Constants.SET_ACTION && sr.getSleep() == payload) {
230 private boolean getFirmware() throws IOException {
231 CommandMessage m = new CommandMessage(Command.FIRMWARE, new byte[] {});
232 logger.debug("Sending get firmware request");
233 SensorReply reply = sendCommand(m);
234 logger.debug("Got reply to getFirmware command: {}", reply);
236 if (reply instanceof SensorFirmwareReply) {
237 SensorFirmwareReply fwReply = (SensorFirmwareReply) reply;
238 thingHandler.setFirmware(fwReply.getFirmware());
245 * Request data from the device
247 * @throws IOException
249 public void requestSensorData() throws IOException {
250 CommandMessage m = new CommandMessage(Command.REQUEST_DATA, new byte[] {});
251 byte[] data = m.getBytes();
252 if (logger.isDebugEnabled()) {
253 logger.debug("Requesting sensor data, will send: {}", HexUtils.bytesToHex(data));
257 Thread.sleep(200); // give the device some time to handle the command
258 } catch (InterruptedException e) {
259 logger.warn("Interrupted while waiting before reading a reply to our rquest data command.");
260 Thread.currentThread().interrupt();
265 private @Nullable SensorReply readReply() throws IOException {
266 byte[] readBuffer = new byte[Constants.REPLY_LENGTH];
268 InputStream localInpuStream = inputStream;
271 if (localInpuStream != null) {
272 logger.trace("Reading for reply until first byte is found");
273 while ((b = localInpuStream.read()) != Constants.MESSAGE_START_AS_INT) {
274 logger.debug("Trying to find first reply byte now...");
276 readBuffer[0] = (byte) b;
277 int remainingBytesRead = localInpuStream.read(readBuffer, 1, Constants.REPLY_LENGTH - 1);
278 if (logger.isDebugEnabled()) {
279 logger.debug("Read remaining bytes: {}, full reply={}", remainingBytesRead,
280 HexUtils.bytesToHex(readBuffer));
282 return ReplyFactory.create(readBuffer);
287 public void readSensorData() throws IOException {
288 logger.trace("readSensorData() called");
289 SensorReply reply = readReply();
290 logger.trace("readSensorData(): Read reply={}", reply);
291 if (reply instanceof SensorMeasuredDataReply) {
292 SensorMeasuredDataReply sensorData = (SensorMeasuredDataReply) reply;
293 logger.trace("We received sensor data");
294 if (sensorData.isValidData()) {
295 logger.trace("Sensor data is valid => updating channels");
296 thingHandler.updateChannels(sensorData);
302 * Shutdown the communication, i.e. send the device to sleep and close the serial port
304 public void dispose(boolean sendtoSleep) {
305 SerialPort localSerialPort = serialPort;
306 if (localSerialPort != null) {
308 sendDeviceToSleepOnDispose();
311 logger.debug("Closing the port now");
312 localSerialPort.close();
316 this.scheduler = null;
319 private void sendDeviceToSleepOnDispose() {
321 ScheduledExecutorService localScheduler = scheduler;
322 if (localScheduler != null) {
323 Future<?> sleepJob = null;
325 sleepJob = localScheduler.submit(() -> {
328 } catch (IOException e) {
329 logger.debug("Exception while sending sleep on disposing the communicator (will ignore it)", e);
332 sleepJob.get(5, TimeUnit.SECONDS);
333 } catch (TimeoutException e) {
334 logger.warn("Could not send device to sleep, because command takes longer than 5 seconds.");
335 sleepJob.cancel(true);
336 } catch (ExecutionException e) {
337 logger.debug("Could not execute sleep command.", e);
338 } catch (InterruptedException e) {
339 logger.debug("Sending device to sleep was interrupted.");
340 Thread.currentThread().interrupt();
343 logger.debug("Scheduler was null, could not send device to sleep.");