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;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.novafinedust.internal.SDS011Handler;
25 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.CommandMessage;
26 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.Constants;
27 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.ModeReply;
28 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorFirmwareReply;
29 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorMeasuredDataReply;
30 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SensorReply;
31 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.SleepReply;
32 import org.openhab.binding.novafinedust.internal.sds011protocol.messages.WorkingPeriodReply;
33 import org.openhab.core.io.transport.serial.PortInUseException;
34 import org.openhab.core.io.transport.serial.SerialPort;
35 import org.openhab.core.io.transport.serial.SerialPortEvent;
36 import org.openhab.core.io.transport.serial.SerialPortEventListener;
37 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
38 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
39 import org.openhab.core.util.HexUtils;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
44 * Central instance to communicate with the device, i.e. receive data from it and send commands to it
46 * @author Stefan Triller - Initial contribution
50 public class SDS011Communicator implements SerialPortEventListener {
52 private final Logger logger = LoggerFactory.getLogger(SDS011Communicator.class);
54 private SerialPortIdentifier portId;
55 private SDS011Handler thingHandler;
56 private @Nullable SerialPort serialPort;
58 private @Nullable OutputStream outputStream;
59 private @Nullable InputStream inputStream;
61 public SDS011Communicator(SDS011Handler thingHandler, SerialPortIdentifier portId) {
62 this.thingHandler = thingHandler;
67 * Initialize the communication with the device, i.e. open the serial port etc.
69 * @param mode the {@link WorkMode} if we want to use polling or reporting
70 * @param interval the time between polling or reportings
71 * @return {@code true} if we can communicate with the device
72 * @throws PortInUseException
73 * @throws TooManyListenersException
75 * @throws UnsupportedCommOperationException
77 public boolean initialize(WorkMode mode, Duration interval)
78 throws PortInUseException, TooManyListenersException, IOException, UnsupportedCommOperationException {
79 boolean initSuccessful = true;
81 SerialPort localSerialPort = portId.open(thingHandler.getThing().getUID().toString(), 2000);
82 localSerialPort.setSerialPortParams(9600, 8, 1, 0);
84 outputStream = localSerialPort.getOutputStream();
85 inputStream = localSerialPort.getInputStream();
87 if (inputStream == null || outputStream == null) {
88 throw new IOException("Could not create input or outputstream for the port");
92 initSuccessful &= sendSleep(false);
93 initSuccessful &= getFirmware();
95 if (mode == WorkMode.POLLING) {
96 initSuccessful &= setMode(WorkMode.POLLING);
97 initSuccessful &= setWorkingPeriod((byte) 0);
100 initSuccessful &= setWorkingPeriod((byte) interval.toMinutes());
101 initSuccessful &= setMode(WorkMode.REPORTING);
104 // enable listeners only after we have configured the sensor above because for configuring we send and read data
106 localSerialPort.notifyOnDataAvailable(true);
107 localSerialPort.addEventListener(this);
108 this.serialPort = localSerialPort;
110 return initSuccessful;
113 private @Nullable SensorReply sendCommand(CommandMessage message) throws IOException {
114 byte[] commandData = message.getBytes();
115 if (logger.isDebugEnabled()) {
116 logger.debug("Will send command: {} ({})", HexUtils.bytesToHex(commandData), Arrays.toString(commandData));
122 // Give the sensor some time to handle the command
124 } catch (InterruptedException e) {
125 logger.warn("Problem while waiting for reading a reply to our command.");
126 Thread.currentThread().interrupt();
128 SensorReply reply = readReply();
129 // in case there is still another reporting active, we want to discard the sensor data and read the reply to our
131 if (reply instanceof SensorMeasuredDataReply) {
137 private void write(byte[] commandData) throws IOException {
138 OutputStream localOutputStream = outputStream;
139 if (localOutputStream != null) {
140 localOutputStream.write(commandData);
141 localOutputStream.flush();
145 private boolean setWorkingPeriod(byte period) throws IOException {
146 CommandMessage m = new CommandMessage(Command.WORKING_PERIOD, new byte[] { Constants.SET_ACTION, period });
147 logger.debug("Sending work period: {}", period);
148 SensorReply reply = sendCommand(m);
149 logger.debug("Got reply to setWorkingPeriod command: {}", reply);
150 if (reply instanceof WorkingPeriodReply) {
151 WorkingPeriodReply wpReply = (WorkingPeriodReply) reply;
152 if (wpReply.getPeriod() == period && wpReply.getActionType() == Constants.SET_ACTION) {
159 private boolean setMode(WorkMode workMode) throws IOException {
160 byte haveToRequestData = 0;
161 if (workMode == WorkMode.POLLING) {
162 haveToRequestData = 1;
165 CommandMessage m = new CommandMessage(Command.MODE, new byte[] { Constants.SET_ACTION, haveToRequestData });
166 logger.debug("Sending mode: {}", workMode);
167 SensorReply reply = sendCommand(m);
168 logger.debug("Got reply to setMode command: {}", reply);
169 if (reply instanceof ModeReply) {
170 ModeReply mr = (ModeReply) reply;
171 if (mr.getActionType() == Constants.SET_ACTION && mr.getMode() == workMode) {
178 private boolean sendSleep(boolean doSleep) throws IOException {
179 byte payload = (byte) 1;
184 CommandMessage m = new CommandMessage(Command.SLEEP, new byte[] { Constants.SET_ACTION, payload });
185 logger.debug("Sending doSleep: {}", doSleep);
186 SensorReply reply = sendCommand(m);
187 logger.debug("Got reply to sendSleep command: {}", reply);
190 // sometimes the sensor does not wakeup on the first attempt, thus we try again
191 for (int i = 0; reply == null && i < 3; i++) {
192 reply = sendCommand(m);
193 logger.debug("Got reply to sendSleep command after retry#{}: {}", i + 1, reply);
197 if (reply instanceof SleepReply) {
198 SleepReply sr = (SleepReply) reply;
199 if (sr.getActionType() == Constants.SET_ACTION && sr.getSleep() == payload) {
206 private boolean getFirmware() throws IOException {
207 CommandMessage m = new CommandMessage(Command.FIRMWARE, new byte[] {});
208 logger.debug("Sending get firmware request");
209 SensorReply reply = sendCommand(m);
210 logger.debug("Got reply to getFirmware command: {}", reply);
212 if (reply instanceof SensorFirmwareReply) {
213 SensorFirmwareReply fwReply = (SensorFirmwareReply) reply;
214 thingHandler.setFirmware(fwReply.getFirmware());
221 * Request data from the device, they will be returned via the serialEvent callback
223 * @throws IOException
225 public void requestSensorData() throws IOException {
226 CommandMessage m = new CommandMessage(Command.REQUEST_DATA, new byte[] {});
227 byte[] data = m.getBytes();
228 if (logger.isDebugEnabled()) {
229 logger.debug("Requesting sensor data, will send: {}", HexUtils.bytesToHex(data));
234 private @Nullable SensorReply readReply() throws IOException {
235 byte[] readBuffer = new byte[Constants.REPLY_LENGTH];
237 InputStream localInpuStream = inputStream;
240 if (localInpuStream != null && localInpuStream.available() > 0) {
241 while ((b = localInpuStream.read()) != Constants.MESSAGE_START_AS_INT) {
242 logger.debug("Trying to find first reply byte now...");
244 readBuffer[0] = (byte) b;
245 int remainingBytesRead = localInpuStream.read(readBuffer, 1, Constants.REPLY_LENGTH - 1);
246 if (logger.isDebugEnabled()) {
247 logger.debug("Read remaining bytes: {}, full reply={}", remainingBytesRead,
248 HexUtils.bytesToHex(readBuffer));
250 return ReplyFactory.create(readBuffer);
256 * Data from the device is arriving and will be parsed accordingly
259 public void serialEvent(SerialPortEvent event) {
260 if (event.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
261 // we get here if data has been received
262 SensorReply reply = null;
265 logger.debug("Got data from sensor: {}", reply);
266 } catch (IOException e) {
267 logger.warn("Could not read available data from the serial port: {}", e.getMessage());
269 if (reply instanceof SensorMeasuredDataReply) {
270 SensorMeasuredDataReply sensorData = (SensorMeasuredDataReply) reply;
271 if (sensorData.isValidData()) {
272 thingHandler.updateChannels(sensorData);
279 * Shutdown the communication, i.e. send the device to sleep and close the serial port
281 public void dispose() {
282 SerialPort localSerialPort = serialPort;
283 if (localSerialPort != null) {
285 // send the device to sleep to preserve power and extend the lifetime of the sensor
287 } catch (IOException e) {
288 // ignore because we are shutting down anyway
289 logger.debug("Exception while disposing communicator (will ignore it)", e);
291 localSerialPort.removeEventListener();
292 localSerialPort.close();
298 InputStream localInputStream = inputStream;
299 if (localInputStream != null) {
300 localInputStream.close();
302 } catch (IOException e) {
303 logger.debug("Error while closing the input stream: {}", e.getMessage());
307 OutputStream localOutputStream = outputStream;
308 if (localOutputStream != null) {
309 localOutputStream.close();
311 } catch (IOException e) {
312 logger.debug("Error while closing the output stream: {}", e.getMessage());