]> git.basschouten.com Git - openhab-addons.git/blob
48de763cfc7959de93bbb8becc348a2808a4d068
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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 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;
26
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;
41
42 /**
43  * Central instance to communicate with the device, i.e. receive data from it and send commands to it
44  *
45  * @author Stefan Triller - Initial contribution
46  *
47  */
48 @NonNullByDefault
49 public class SDS011Communicator {
50
51     private static final int MAX_READ_UNTIL_SENSOR_DATA = 6; // at least 6 because we send 5 configuration commands
52
53     private final Logger logger = LoggerFactory.getLogger(SDS011Communicator.class);
54
55     private SerialPortIdentifier portId;
56     private SDS011Handler thingHandler;
57     private @Nullable SerialPort serialPort;
58
59     private @Nullable OutputStream outputStream;
60     private @Nullable InputStream inputStream;
61     private @Nullable ScheduledExecutorService scheduler;
62
63     public SDS011Communicator(SDS011Handler thingHandler, SerialPortIdentifier portId,
64             ScheduledExecutorService scheduler) {
65         this.thingHandler = thingHandler;
66         this.portId = portId;
67         this.scheduler = scheduler;
68     }
69
70     /**
71      * Initialize the communication with the device, i.e. open the serial port etc.
72      *
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
78      * @throws IOException
79      * @throws UnsupportedCommOperationException
80      */
81     public void initialize(WorkMode mode, Duration interval)
82             throws PortInUseException, TooManyListenersException, IOException, UnsupportedCommOperationException {
83
84         logger.trace("Initializing with mode={}, interval={}", mode, interval);
85
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");
90
91         outputStream = localSerialPort.getOutputStream();
92         inputStream = localSerialPort.getInputStream();
93
94         if (inputStream == null || outputStream == null) {
95             throw new IOException("Could not create input or outputstream for the port");
96         }
97         logger.trace("Input and Outputstream opened for the port");
98
99         // wake up the device
100         sendSleep(false);
101         logger.trace("Wake up call done");
102         getFirmware();
103         logger.trace("Firmware requested");
104
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");
110         } else {
111             // reporting
112             setWorkingPeriod((byte) interval.toMinutes());
113             logger.trace("Working period for reporting set");
114             setMode(WorkMode.REPORTING);
115             logger.trace("Reporting mode set");
116         }
117
118         this.serialPort = localSerialPort;
119     }
120
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));
125         }
126
127         try {
128             write(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);
131             throw ioex;
132         }
133
134         try {
135             // Give the sensor some time to handle the command before doing something else with it
136             Thread.sleep(500);
137         } catch (InterruptedException e) {
138             logger.warn("Interrupted while waiting after sending command={}", message);
139             Thread.currentThread().interrupt();
140         }
141     }
142
143     private void write(byte[] commandData) throws IOException {
144         OutputStream localOutputStream = outputStream;
145         if (localOutputStream != null) {
146             localOutputStream.write(commandData);
147             localOutputStream.flush();
148         }
149     }
150
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);
154         sendCommand(m);
155     }
156
157     private void setMode(WorkMode workMode) throws IOException {
158         byte haveToRequestData = 0;
159         if (workMode == WorkMode.POLLING) {
160             haveToRequestData = 1;
161         }
162
163         CommandMessage m = new CommandMessage(Command.MODE, new byte[] { Constants.SET_ACTION, haveToRequestData });
164         logger.debug("Sending mode: {}", workMode);
165         sendCommand(m);
166     }
167
168     private void sendSleep(boolean doSleep) throws IOException {
169         byte payload = (byte) 1;
170         if (doSleep) {
171             payload = (byte) 0;
172         }
173
174         CommandMessage m = new CommandMessage(Command.SLEEP, new byte[] { Constants.SET_ACTION, payload });
175         logger.debug("Sending doSleep: {}", doSleep);
176         sendCommand(m);
177
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
180         // commands
181         if (!doSleep) {
182             // sometimes the sensor does not wakeup on the first attempt, thus we try again
183             sendCommand(m);
184         }
185     }
186
187     private void getFirmware() throws IOException {
188         CommandMessage m = new CommandMessage(Command.FIRMWARE, new byte[] {});
189         logger.debug("Sending get firmware request");
190         sendCommand(m);
191     }
192
193     /**
194      * Request data from the device
195      *
196      * @throws IOException
197      */
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));
203         }
204         write(data);
205         try {
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();
210         }
211         readSensorData();
212     }
213
214     private @Nullable SensorReply readReply() throws IOException {
215         byte[] readBuffer = new byte[Constants.REPLY_LENGTH];
216
217         InputStream localInpuStream = inputStream;
218
219         int b = -1;
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...");
224             }
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));
231             }
232             return ReplyFactory.create(readBuffer);
233         }
234         return null;
235     }
236
237     public void readSensorData() throws IOException {
238         logger.trace("readSensorData() called");
239
240         boolean foundSensorData = doRead();
241         for (int i = 0; !foundSensorData && i < MAX_READ_UNTIL_SENSOR_DATA; i++) {
242             foundSensorData = doRead();
243         }
244     }
245
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);
255                 return true;
256             }
257         }
258         return false;
259     }
260
261     /**
262      * Shutdown the communication, i.e. send the device to sleep and close the serial port
263      */
264     public void dispose(boolean sendtoSleep) {
265         SerialPort localSerialPort = serialPort;
266         if (localSerialPort != null) {
267             if (sendtoSleep) {
268                 sendDeviceToSleepOnDispose();
269             }
270
271             logger.debug("Closing the port now");
272             localSerialPort.close();
273
274             serialPort = null;
275         }
276         this.scheduler = null;
277     }
278
279     private void sendDeviceToSleepOnDispose() {
280         @Nullable
281         ScheduledExecutorService localScheduler = scheduler;
282         if (localScheduler != null) {
283             Future<?> sleepJob = null;
284             try {
285                 sleepJob = localScheduler.submit(() -> {
286                     try {
287                         sendSleep(true);
288                     } catch (IOException e) {
289                         logger.debug("Exception while sending sleep on disposing the communicator (will ignore it)", e);
290                     }
291                 });
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();
301             }
302         } else {
303             logger.debug("Scheduler was null, could not send device to sleep.");
304         }
305     }
306 }