]> git.basschouten.com Git - openhab-addons.git/blob
3e3c11e32980d2f8f2176f09c93350b3cb69e356
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.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;
45
46 /**
47  * Central instance to communicate with the device, i.e. receive data from it and send commands to it
48  *
49  * @author Stefan Triller - Initial contribution
50  *
51  */
52 @NonNullByDefault
53 public class SDS011Communicator {
54
55     private static final int MAX_SENDOR_REPORTINGS_UNTIL_EXPECTED_REPLY = 20;
56
57     private final Logger logger = LoggerFactory.getLogger(SDS011Communicator.class);
58
59     private SerialPortIdentifier portId;
60     private SDS011Handler thingHandler;
61     private @Nullable SerialPort serialPort;
62
63     private @Nullable OutputStream outputStream;
64     private @Nullable InputStream inputStream;
65     private @Nullable ScheduledExecutorService scheduler;
66
67     public SDS011Communicator(SDS011Handler thingHandler, SerialPortIdentifier portId,
68             ScheduledExecutorService scheduler) {
69         this.thingHandler = thingHandler;
70         this.portId = portId;
71         this.scheduler = scheduler;
72     }
73
74     /**
75      * Initialize the communication with the device, i.e. open the serial port etc.
76      *
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
82      * @throws IOException
83      * @throws UnsupportedCommOperationException
84      */
85     public boolean initialize(WorkMode mode, Duration interval)
86             throws PortInUseException, TooManyListenersException, IOException, UnsupportedCommOperationException {
87         boolean initSuccessful = true;
88
89         logger.trace("Initializing with mode={}, interval={}", mode, interval);
90
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");
95
96         outputStream = localSerialPort.getOutputStream();
97         inputStream = localSerialPort.getInputStream();
98
99         if (inputStream == null || outputStream == null) {
100             throw new IOException("Could not create input or outputstream for the port");
101         }
102         logger.trace("Input and Outputstream opened for the port");
103
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);
109
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);
115         } else {
116             // reporting
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);
121         }
122
123         this.serialPort = localSerialPort;
124
125         return initSuccessful;
126     }
127
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));
132         }
133
134         try {
135             write(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);
138             throw ioex;
139         }
140
141         try {
142             // Give the sensor some time to handle the command
143             Thread.sleep(500);
144         } catch (InterruptedException e) {
145             logger.warn("Problem while waiting for reading a reply to our command.");
146             Thread.currentThread().interrupt();
147         }
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) {
153                 reply = readReply();
154             } else {
155                 break;
156             }
157         }
158         return reply;
159     }
160
161     private void write(byte[] commandData) throws IOException {
162         OutputStream localOutputStream = outputStream;
163         if (localOutputStream != null) {
164             localOutputStream.write(commandData);
165             localOutputStream.flush();
166         }
167     }
168
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) {
177                 return true;
178             }
179         }
180         return false;
181     }
182
183     private boolean setMode(WorkMode workMode) throws IOException {
184         byte haveToRequestData = 0;
185         if (workMode == WorkMode.POLLING) {
186             haveToRequestData = 1;
187         }
188
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) {
196                 return true;
197             }
198         }
199         return false;
200     }
201
202     private boolean sendSleep(boolean doSleep) throws IOException {
203         byte payload = (byte) 1;
204         if (doSleep) {
205             payload = (byte) 0;
206         }
207
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);
212
213         if (!doSleep) {
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);
218             }
219         }
220
221         if (reply instanceof SleepReply) {
222             SleepReply sr = (SleepReply) reply;
223             if (sr.getActionType() == Constants.SET_ACTION && sr.getSleep() == payload) {
224                 return true;
225             }
226         }
227         return false;
228     }
229
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);
235
236         if (reply instanceof SensorFirmwareReply) {
237             SensorFirmwareReply fwReply = (SensorFirmwareReply) reply;
238             thingHandler.setFirmware(fwReply.getFirmware());
239             return true;
240         }
241         return false;
242     }
243
244     /**
245      * Request data from the device
246      *
247      * @throws IOException
248      */
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));
254         }
255         write(data);
256         try {
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();
261         }
262         readSensorData();
263     }
264
265     private @Nullable SensorReply readReply() throws IOException {
266         byte[] readBuffer = new byte[Constants.REPLY_LENGTH];
267
268         InputStream localInpuStream = inputStream;
269
270         int b = -1;
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...");
275             }
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));
281             }
282             return ReplyFactory.create(readBuffer);
283         }
284         return null;
285     }
286
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);
297             }
298         }
299     }
300
301     /**
302      * Shutdown the communication, i.e. send the device to sleep and close the serial port
303      */
304     public void dispose(boolean sendtoSleep) {
305         SerialPort localSerialPort = serialPort;
306         if (localSerialPort != null) {
307             if (sendtoSleep) {
308                 sendDeviceToSleepOnDispose();
309             }
310
311             logger.debug("Closing the port now");
312             localSerialPort.close();
313
314             serialPort = null;
315         }
316         this.scheduler = null;
317     }
318
319     private void sendDeviceToSleepOnDispose() {
320         @Nullable
321         ScheduledExecutorService localScheduler = scheduler;
322         if (localScheduler != null) {
323             Future<?> sleepJob = null;
324             try {
325                 sleepJob = localScheduler.submit(() -> {
326                     try {
327                         sendSleep(true);
328                     } catch (IOException e) {
329                         logger.debug("Exception while sending sleep on disposing the communicator (will ignore it)", e);
330                     }
331                 });
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();
341             }
342         } else {
343             logger.debug("Scheduler was null, could not send device to sleep.");
344         }
345     }
346 }