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