]> git.basschouten.com Git - openhab-addons.git/blob
0383c165c60c99ddf969fffb692d0c499cf9951b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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         logger.trace("Initializing with mode={}, interval={}", mode, interval);
84
85         SerialPort localSerialPort = portId.open(thingHandler.getThing().getUID().toString(), 2000);
86         logger.trace("Port opened, object is={}", localSerialPort);
87         localSerialPort.setSerialPortParams(9600, 8, 1, 0);
88         logger.trace("Serial parameters set on port");
89
90         outputStream = localSerialPort.getOutputStream();
91         inputStream = localSerialPort.getInputStream();
92
93         if (inputStream == null || outputStream == null) {
94             throw new IOException("Could not create input or outputstream for the port");
95         }
96         logger.trace("Input and Outputstream opened for the port");
97
98         // wake up the device
99         sendSleep(false);
100         logger.trace("Wake up call done");
101         getFirmware();
102         logger.trace("Firmware requested");
103
104         if (mode == WorkMode.POLLING) {
105             setMode(WorkMode.POLLING);
106             logger.trace("Polling mode set");
107             setWorkingPeriod((byte) 0);
108             logger.trace("Working period for polling set");
109         } else {
110             // reporting
111             setWorkingPeriod((byte) interval.toMinutes());
112             logger.trace("Working period for reporting set");
113             setMode(WorkMode.REPORTING);
114             logger.trace("Reporting mode set");
115         }
116
117         this.serialPort = localSerialPort;
118     }
119
120     private void sendCommand(CommandMessage message) throws IOException {
121         byte[] commandData = message.getBytes();
122         if (logger.isDebugEnabled()) {
123             logger.debug("Will send command: {} ({})", HexUtils.bytesToHex(commandData), Arrays.toString(commandData));
124         }
125
126         try {
127             write(commandData);
128         } catch (IOException ioex) {
129             logger.debug("Got an exception while writing a command, will not try to fetch a reply for it.", ioex);
130             throw ioex;
131         }
132
133         try {
134             // Give the sensor some time to handle the command before doing something else with it
135             Thread.sleep(500);
136         } catch (InterruptedException e) {
137             logger.warn("Interrupted while waiting after sending command={}", message);
138             Thread.currentThread().interrupt();
139         }
140     }
141
142     private void write(byte[] commandData) throws IOException {
143         OutputStream localOutputStream = outputStream;
144         if (localOutputStream != null) {
145             localOutputStream.write(commandData);
146             localOutputStream.flush();
147         }
148     }
149
150     private void setWorkingPeriod(byte period) throws IOException {
151         CommandMessage m = new CommandMessage(Command.WORKING_PERIOD, new byte[] { Constants.SET_ACTION, period });
152         logger.debug("Sending work period: {}", period);
153         sendCommand(m);
154     }
155
156     private void setMode(WorkMode workMode) throws IOException {
157         byte haveToRequestData = 0;
158         if (workMode == WorkMode.POLLING) {
159             haveToRequestData = 1;
160         }
161
162         CommandMessage m = new CommandMessage(Command.MODE, new byte[] { Constants.SET_ACTION, haveToRequestData });
163         logger.debug("Sending mode: {}", workMode);
164         sendCommand(m);
165     }
166
167     private void sendSleep(boolean doSleep) throws IOException {
168         byte payload = (byte) 1;
169         if (doSleep) {
170             payload = (byte) 0;
171         }
172
173         CommandMessage m = new CommandMessage(Command.SLEEP, new byte[] { Constants.SET_ACTION, payload });
174         logger.debug("Sending doSleep: {}", doSleep);
175         sendCommand(m);
176
177         // as it turns out, the protocol doesn't work as described: sometimes the device just wakes up without replying
178         // to us. Hence we should not wait for a reply, but just force to wake it up to then send out our configuration
179         // commands
180         if (!doSleep) {
181             // sometimes the sensor does not wakeup on the first attempt, thus we try again
182             sendCommand(m);
183         }
184     }
185
186     private void getFirmware() throws IOException {
187         CommandMessage m = new CommandMessage(Command.FIRMWARE, new byte[] {});
188         logger.debug("Sending get firmware request");
189         sendCommand(m);
190     }
191
192     /**
193      * Request data from the device
194      *
195      * @throws IOException
196      */
197     public void requestSensorData() throws IOException {
198         CommandMessage m = new CommandMessage(Command.REQUEST_DATA, new byte[] {});
199         byte[] data = m.getBytes();
200         if (logger.isDebugEnabled()) {
201             logger.debug("Requesting sensor data, will send: {}", HexUtils.bytesToHex(data));
202         }
203         write(data);
204         try {
205             Thread.sleep(200); // give the device some time to handle the command
206         } catch (InterruptedException e) {
207             logger.warn("Interrupted while waiting before reading a reply to our request data command.");
208             Thread.currentThread().interrupt();
209         }
210         readSensorData();
211     }
212
213     private @Nullable SensorReply readReply() throws IOException {
214         byte[] readBuffer = new byte[Constants.REPLY_LENGTH];
215
216         InputStream localInpuStream = inputStream;
217
218         int b = -1;
219         if (localInpuStream != null) {
220             logger.trace("Reading for reply until first byte is found");
221             while ((b = localInpuStream.read()) != Constants.MESSAGE_START_AS_INT) {
222                 // logger.trace("Trying to find first reply byte now...");
223             }
224             readBuffer[0] = (byte) b;
225             int remainingBytesRead = localInpuStream.read(readBuffer, 1, Constants.REPLY_LENGTH - 1);
226             if (logger.isDebugEnabled()) {
227                 logger.debug("Read remaining bytes: {}, full reply={}", remainingBytesRead,
228                         HexUtils.bytesToHex(readBuffer));
229                 logger.trace("Read bytes as numbers: {}", Arrays.toString(readBuffer));
230             }
231             return ReplyFactory.create(readBuffer);
232         }
233         return null;
234     }
235
236     public void readSensorData() throws IOException {
237         logger.trace("readSensorData() called");
238
239         boolean foundSensorData = doRead();
240         for (int i = 0; !foundSensorData && i < MAX_READ_UNTIL_SENSOR_DATA; i++) {
241             foundSensorData = doRead();
242         }
243     }
244
245     private boolean doRead() throws IOException {
246         SensorReply reply = readReply();
247         logger.trace("doRead(): Read reply={}", reply);
248         if (reply instanceof SensorMeasuredDataReply sensorData) {
249             logger.trace("We received sensor data");
250             if (sensorData.isValidData()) {
251                 logger.trace("Sensor data is valid => updating channels");
252                 thingHandler.updateChannels(sensorData);
253                 return true;
254             }
255         }
256         return false;
257     }
258
259     /**
260      * Shutdown the communication, i.e. send the device to sleep and close the serial port
261      */
262     public void dispose(boolean sendtoSleep) {
263         SerialPort localSerialPort = serialPort;
264         if (localSerialPort != null) {
265             if (sendtoSleep) {
266                 sendDeviceToSleepOnDispose();
267             }
268
269             logger.debug("Closing the port now");
270             localSerialPort.close();
271
272             serialPort = null;
273         }
274         this.scheduler = null;
275     }
276
277     private void sendDeviceToSleepOnDispose() {
278         @Nullable
279         ScheduledExecutorService localScheduler = scheduler;
280         if (localScheduler != null) {
281             Future<?> sleepJob = null;
282             try {
283                 sleepJob = localScheduler.submit(() -> {
284                     try {
285                         sendSleep(true);
286                     } catch (IOException e) {
287                         logger.debug("Exception while sending sleep on disposing the communicator (will ignore it)", e);
288                     }
289                 });
290                 sleepJob.get(5, TimeUnit.SECONDS);
291             } catch (TimeoutException e) {
292                 logger.warn("Could not send device to sleep, because command takes longer than 5 seconds.");
293                 sleepJob.cancel(true);
294             } catch (ExecutionException e) {
295                 logger.debug("Could not execute sleep command.", e);
296             } catch (InterruptedException e) {
297                 logger.debug("Sending device to sleep was interrupted.");
298                 Thread.currentThread().interrupt();
299             }
300         } else {
301             logger.debug("Scheduler was null, could not send device to sleep.");
302         }
303     }
304 }