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