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