]> git.basschouten.com Git - openhab-addons.git/blob
52762197a45c4dc274a24ed3bf876508cd38e547
[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.bluetooth.bluegiga.internal;
14
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.OutputStream;
18 import java.util.Set;
19 import java.util.concurrent.CopyOnWriteArraySet;
20
21 import org.apache.commons.io.IOUtils;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaEndProcedureCommand;
24 import org.slf4j.Logger;
25 import org.slf4j.LoggerFactory;
26
27 /**
28  * The main handler class for interacting with the BlueGiga serial API. This class provides conversion of packets from
29  * the serial stream into command and response classes.
30  *
31  * @author Chris Jackson - Initial contribution and API
32  * @author Pauli Anttila - Split serial handler and transaction management
33  *
34  */
35 @NonNullByDefault
36 public class BlueGigaSerialHandler {
37
38     private static final int BLE_MAX_LENGTH = 64;
39
40     private final Logger logger = LoggerFactory.getLogger(BlueGigaSerialHandler.class);
41
42     /**
43      * The event listeners will be notified of any asynchronous events
44      */
45     private final Set<BlueGigaSerialEventListener> eventListeners = new CopyOnWriteArraySet<>();
46
47     /**
48      * The event listeners will be notified of any life-cycle events of the handler.
49      */
50     private final Set<BlueGigaHandlerListener> handlerListeners = new CopyOnWriteArraySet<>();
51
52     /**
53      * Flag reflecting that parser has been closed and parser parserThread
54      * should exit.
55      */
56     private boolean close = false;
57
58     private final OutputStream outputStream;
59     private final InputStream inputStream;
60     private final Thread parserThread;
61
62     public BlueGigaSerialHandler(final InputStream inputStream, final OutputStream outputStream) {
63         this.outputStream = outputStream;
64         this.inputStream = inputStream;
65
66         flush();
67         parserThread = createBlueGigaBLEHandler();
68         parserThread.setDaemon(true);
69         parserThread.start();
70         int tries = 0;
71
72         // wait until the daemon thread kicks off, e.g. when it is ready to receive any commands
73         while (parserThread.getState() == Thread.State.NEW) {
74             try {
75                 Thread.sleep(100);
76                 tries++;
77                 if (tries > 10) {
78                     throw new IllegalStateException("BlueGiga handler thread failed to start");
79                 }
80             } catch (InterruptedException ignore) {
81                 /* ignore */
82             }
83         }
84     }
85
86     private void flush() {
87         // Send End Procedure command to end all activity and flush input buffer to start from know state
88         logger.trace("Flush input stream");
89         sendFrame(new BlueGigaEndProcedureCommand(), false);
90         try {
91             // Wait BlueGiga controller have stopped all activity
92             Thread.sleep(100);
93             logger.trace("Bytes available: {}", inputStream.available());
94             IOUtils.skipFully(inputStream, inputStream.available());
95         } catch (InterruptedException e) {
96             close = true;
97         } catch (IOException e) {
98             // Ignore
99         }
100         logger.trace("Flush done");
101     }
102
103     /**
104      * Requests parser thread to shutdown. Waits forever while the parser thread is getting shut down.
105      */
106     public void close() {
107         close(0);
108     }
109
110     /**
111      * Requests parser thread to shutdown. Waits specified milliseconds while the parser thread is getting shut down.
112      *
113      * @param timeout milliseconds to wait
114      */
115     public void close(long timeout) {
116         close = true;
117         try {
118             parserThread.interrupt();
119             // Give a fair chance to shutdown nicely
120             Thread.sleep(50);
121             IOUtils.closeQuietly(outputStream);
122             IOUtils.closeQuietly(inputStream);
123             parserThread.join(0);
124         } catch (InterruptedException e) {
125             logger.warn("Interrupted in packet parser thread shutdown join.");
126         }
127
128         handlerListeners.clear();
129         eventListeners.clear();
130         logger.debug("Closed");
131     }
132
133     /**
134      * Checks if parser thread is alive.
135      *
136      * @return true if parser thread is alive.
137      */
138     public boolean isAlive() {
139         return parserThread.isAlive() && !close;
140     }
141
142     public void sendFrame(BlueGigaCommand bleFrame) throws IllegalStateException {
143         sendFrame(bleFrame, true);
144     }
145
146     private void sendFrame(BlueGigaCommand bleFrame, boolean checkIsAlive) throws IllegalStateException {
147         if (checkIsAlive) {
148             checkIfAlive();
149         }
150
151         // Send the data
152         logger.trace("sendFrame: {}", bleFrame);
153         try {
154             int[] payload = bleFrame.serialize();
155             if (logger.isTraceEnabled()) {
156                 logger.trace("BLE TX: {}", printHex(payload, payload.length));
157             }
158             for (int b : payload) {
159                 outputStream.write(b);
160             }
161             outputStream.flush();
162
163         } catch (IOException e) {
164             throw new BlueGigaException("Error sending BLE frame", e);
165         }
166     }
167
168     public void addEventListener(BlueGigaSerialEventListener listener) {
169         eventListeners.add(listener);
170     }
171
172     public void removeEventListener(BlueGigaSerialEventListener listener) {
173         eventListeners.remove(listener);
174     }
175
176     public void addHandlerListener(BlueGigaHandlerListener listener) {
177         handlerListeners.add(listener);
178     }
179
180     public void removeHandlerListener(BlueGigaHandlerListener listener) {
181         handlerListeners.remove(listener);
182     }
183
184     /**
185      * Notify any transaction listeners when we receive a response.
186      *
187      * @param response the response data received
188      */
189     private void notifyEventListeners(final BlueGigaResponse response) {
190         // Notify the listeners
191         for (final BlueGigaSerialEventListener listener : eventListeners) {
192             try {
193                 listener.bluegigaFrameReceived(response);
194             } catch (Exception ex) {
195                 logger.warn("Execution error of a BlueGigaHandlerListener listener.", ex);
196             }
197         }
198     }
199
200     /**
201      * Notify handler event listeners that the handler was bluegigaClosed due to an error specified as an argument.
202      *
203      * @param reason the reason to bluegigaClosed
204      */
205     private void notifyEventListeners(final Exception reason) {
206         // It should be safe enough not to use the NotificationService as this is a fatal error, no any further actions
207         // can be done with the handler, a new handler should be re-created
208         // There is another reason why NotificationService can't be used - the listeners should be notified immediately
209         for (final BlueGigaHandlerListener listener : handlerListeners) {
210             try {
211                 listener.bluegigaClosed(reason);
212             } catch (Exception ex) {
213                 logger.warn("Execution error of a BlueGigaHandlerListener listener.", ex);
214             }
215         }
216     }
217
218     private String printHex(int[] data, int len) {
219         StringBuilder builder = new StringBuilder();
220
221         for (int cnt = 0; cnt < len; cnt++) {
222             builder.append(String.format("%02X ", data[cnt]));
223         }
224
225         return builder.toString();
226     }
227
228     private void checkIfAlive() {
229         if (!isAlive()) {
230             throw new IllegalStateException("Bluegiga handler is dead. Most likely because of IO errors. "
231                     + "Re-initialization of the BlueGigaSerialHandler is required.");
232         }
233     }
234
235     private Thread createBlueGigaBLEHandler() {
236         final int framecheckParams[] = new int[] { 0x00, 0x7F, 0xC0, 0xF8, 0xE0 };
237         return new Thread("BlueGigaBLEHandler") {
238             @Override
239             public void run() {
240                 int exceptionCnt = 0;
241                 logger.trace("BlueGiga BLE thread started");
242                 int[] inputBuffer = new int[BLE_MAX_LENGTH];
243                 int inputCount = 0;
244                 int inputLength = 0;
245
246                 while (!close) {
247                     try {
248                         int val = inputStream.read();
249                         if (val == -1) {
250                             continue;
251                         }
252
253                         inputBuffer[inputCount++] = val;
254
255                         if (inputCount == 1) {
256                             if (inputStream.markSupported()) {
257                                 inputStream.mark(BLE_MAX_LENGTH);
258                             }
259                         }
260
261                         if (inputCount < 4) {
262                             // The BGAPI protocol has no packet framing, and no error detection, so we do a few
263                             // sanity checks on the header to try and allow resyncronisation should there be an
264                             // error.
265                             // Byte 0: Check technology type is bluetooth and high length is 0
266                             // Byte 1: Check length is less than 64 bytes
267                             // Byte 2: Check class ID is less than 8
268                             // Byte 3: Check command ID is less than 16
269                             if ((val & framecheckParams[inputCount]) != 0) {
270                                 logger.debug("BlueGiga framing error byte {} = {}", inputCount, val);
271                                 if (inputStream.markSupported()) {
272                                     inputStream.reset();
273                                 }
274                                 inputCount = 0;
275                                 continue;
276                             }
277                         } else if (inputCount == 4) {
278                             // Process the header to get the length
279                             inputLength = inputBuffer[1] + (inputBuffer[0] & 0x02 << 8) + 4;
280                             if (inputLength > 64) {
281                                 logger.debug("BLE length larger than 64 bytes ({})", inputLength);
282                             }
283                         }
284                         if (inputCount == inputLength) {
285                             // End of packet reached - process
286                             BlueGigaResponse responsePacket = BlueGigaResponsePackets.getPacket(inputBuffer);
287
288                             if (logger.isTraceEnabled()) {
289                                 logger.trace("BLE RX: {}", printHex(inputBuffer, inputLength));
290                                 logger.trace("BLE RX: {}", responsePacket);
291                             }
292                             if (responsePacket != null) {
293                                 notifyEventListeners(responsePacket);
294                             }
295
296                             inputCount = 0;
297                             exceptionCnt = 0;
298                         }
299
300                     } catch (final IOException e) {
301                         logger.debug("BlueGiga BLE IOException: ", e);
302
303                         if (exceptionCnt++ > 10) {
304                             logger.error("BlueGiga BLE exception count exceeded, closing handler");
305                             close = true;
306                             notifyEventListeners(e);
307                         }
308                     }
309                 }
310                 logger.debug("BlueGiga BLE exited.");
311             }
312         };
313     }
314 }