]> git.basschouten.com Git - openhab-addons.git/blob
5baa3612d3a0e6e7f60823edf6d18991ec1c9adf
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.novafinedust.internal.sds011protocol;
14
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
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;
42
43 /**
44  * Central instance to communicate with the device, i.e. receive data from it and send commands to it
45  *
46  * @author Stefan Triller - Initial contribution
47  *
48  */
49 @NonNullByDefault
50 public class SDS011Communicator implements SerialPortEventListener {
51
52     private final Logger logger = LoggerFactory.getLogger(SDS011Communicator.class);
53
54     private SerialPortIdentifier portId;
55     private SDS011Handler thingHandler;
56     private @Nullable SerialPort serialPort;
57
58     private @Nullable OutputStream outputStream;
59     private @Nullable InputStream inputStream;
60
61     public SDS011Communicator(SDS011Handler thingHandler, SerialPortIdentifier portId) {
62         this.thingHandler = thingHandler;
63         this.portId = portId;
64     }
65
66     /**
67      * Initialize the communication with the device, i.e. open the serial port etc.
68      *
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
74      * @throws IOException
75      * @throws UnsupportedCommOperationException
76      */
77     public boolean initialize(WorkMode mode, Duration interval)
78             throws PortInUseException, TooManyListenersException, IOException, UnsupportedCommOperationException {
79         boolean initSuccessful = true;
80
81         SerialPort localSerialPort = portId.open(thingHandler.getThing().getUID().toString(), 2000);
82         localSerialPort.setSerialPortParams(9600, 8, 1, 0);
83
84         outputStream = localSerialPort.getOutputStream();
85         inputStream = localSerialPort.getInputStream();
86
87         if (inputStream == null || outputStream == null) {
88             throw new IOException("Could not create input or outputstream for the port");
89         }
90
91         // wake up the device
92         initSuccessful &= sendSleep(false);
93         initSuccessful &= getFirmware();
94
95         if (mode == WorkMode.POLLING) {
96             initSuccessful &= setMode(WorkMode.POLLING);
97             initSuccessful &= setWorkingPeriod((byte) 0);
98         } else {
99             // reporting
100             initSuccessful &= setWorkingPeriod((byte) interval.toMinutes());
101             initSuccessful &= setMode(WorkMode.REPORTING);
102         }
103
104         // enable listeners only after we have configured the sensor above because for configuring we send and read data
105         // sequentially
106         localSerialPort.notifyOnDataAvailable(true);
107         localSerialPort.addEventListener(this);
108         this.serialPort = localSerialPort;
109
110         return initSuccessful;
111     }
112
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));
117         }
118
119         write(commandData);
120
121         try {
122             // Give the sensor some time to handle the command
123             Thread.sleep(500);
124         } catch (InterruptedException e) {
125             logger.warn("Problem while waiting for reading a reply to our command.");
126             Thread.currentThread().interrupt();
127         }
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
130         // command again
131         if (reply instanceof SensorMeasuredDataReply) {
132             reply = readReply();
133         }
134         return reply;
135     }
136
137     private void write(byte[] commandData) throws IOException {
138         OutputStream localOutputStream = outputStream;
139         if (localOutputStream != null) {
140             localOutputStream.write(commandData);
141             localOutputStream.flush();
142         }
143     }
144
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) {
153                 return true;
154             }
155         }
156         return false;
157     }
158
159     private boolean setMode(WorkMode workMode) throws IOException {
160         byte haveToRequestData = 0;
161         if (workMode == WorkMode.POLLING) {
162             haveToRequestData = 1;
163         }
164
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) {
172                 return true;
173             }
174         }
175         return false;
176     }
177
178     private boolean sendSleep(boolean doSleep) throws IOException {
179         byte payload = (byte) 1;
180         if (doSleep) {
181             payload = (byte) 0;
182         }
183
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);
188
189         if (!doSleep) {
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);
194             }
195         }
196
197         if (reply instanceof SleepReply) {
198             SleepReply sr = (SleepReply) reply;
199             if (sr.getActionType() == Constants.SET_ACTION && sr.getSleep() == payload) {
200                 return true;
201             }
202         }
203         return false;
204     }
205
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);
211
212         if (reply instanceof SensorFirmwareReply) {
213             SensorFirmwareReply fwReply = (SensorFirmwareReply) reply;
214             thingHandler.setFirmware(fwReply.getFirmware());
215             return true;
216         }
217         return false;
218     }
219
220     /**
221      * Request data from the device, they will be returned via the serialEvent callback
222      *
223      * @throws IOException
224      */
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));
230         }
231         write(data);
232     }
233
234     private @Nullable SensorReply readReply() throws IOException {
235         byte[] readBuffer = new byte[Constants.REPLY_LENGTH];
236
237         InputStream localInpuStream = inputStream;
238
239         int b = -1;
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...");
243             }
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));
249             }
250             return ReplyFactory.create(readBuffer);
251         }
252         return null;
253     }
254
255     /**
256      * Data from the device is arriving and will be parsed accordingly
257      */
258     @Override
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;
263             try {
264                 reply = readReply();
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());
268             }
269             if (reply instanceof SensorMeasuredDataReply) {
270                 SensorMeasuredDataReply sensorData = (SensorMeasuredDataReply) reply;
271                 if (sensorData.isValidData()) {
272                     thingHandler.updateChannels(sensorData);
273                 }
274             }
275         }
276     }
277
278     /**
279      * Shutdown the communication, i.e. send the device to sleep and close the serial port
280      */
281     public void dispose() {
282         SerialPort localSerialPort = serialPort;
283         if (localSerialPort != null) {
284             try {
285                 // send the device to sleep to preserve power and extend the lifetime of the sensor
286                 sendSleep(true);
287             } catch (IOException e) {
288                 // ignore because we are shutting down anyway
289                 logger.debug("Exception while disposing communicator (will ignore it)", e);
290             } finally {
291                 localSerialPort.removeEventListener();
292                 localSerialPort.close();
293                 serialPort = null;
294             }
295         }
296
297         try {
298             InputStream localInputStream = inputStream;
299             if (localInputStream != null) {
300                 localInputStream.close();
301             }
302         } catch (IOException e) {
303             logger.debug("Error while closing the input stream: {}", e.getMessage());
304         }
305
306         try {
307             OutputStream localOutputStream = outputStream;
308             if (localOutputStream != null) {
309                 localOutputStream.close();
310             }
311         } catch (IOException e) {
312             logger.debug("Error while closing the output stream: {}", e.getMessage());
313         }
314     }
315 }