]> git.basschouten.com Git - openhab-addons.git/blob
35fc8e3e1ddbe91157f5ff2400499dd568a36785
[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.bluetooth.bluegiga.internal;
14
15 import java.io.EOFException;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.io.OutputStream;
19 import java.util.Set;
20 import java.util.concurrent.CopyOnWriteArraySet;
21
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             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     private void skipFully(final InputStream input, final long toSkip) throws IOException {
109         if (toSkip < 0) {
110             throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip);
111         }
112
113         long remain = toSkip;
114
115         final byte[] byteArray = new byte[8192];
116         while (remain > 0) {
117             final long n = input.read(byteArray, 0, (int) Math.min(remain, byteArray.length));
118             if (n < 0) { // EOF
119                 break;
120             }
121             remain -= n;
122         }
123
124         long skipped = toSkip - remain;
125         if (skipped != toSkip) {
126             throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped);
127         }
128     }
129
130     /**
131      * Requests parser thread to shutdown. Waits forever while the parser thread is getting shut down.
132      */
133     public void close() {
134         close(0);
135     }
136
137     /**
138      * Requests parser thread to shutdown. Waits specified milliseconds while the parser thread is getting shut down.
139      *
140      * @param timeout milliseconds to wait
141      */
142     public void close(long timeout) {
143         close = true;
144         try {
145             parserThread.interrupt();
146             // Give a fair chance to shutdown nicely
147             Thread.sleep(50);
148             try {
149                 outputStream.close();
150             } catch (IOException e) {
151             }
152             try {
153                 inputStream.close();
154             } catch (IOException e) {
155             }
156             parserThread.join(0);
157         } catch (InterruptedException e) {
158             logger.warn("Interrupted in packet parser thread shutdown join.");
159         }
160
161         handlerListeners.clear();
162         eventListeners.clear();
163         logger.debug("Closed");
164     }
165
166     /**
167      * Checks if parser thread is alive.
168      *
169      * @return true if parser thread is alive.
170      */
171     public boolean isAlive() {
172         return parserThread.isAlive() && !close;
173     }
174
175     public void sendFrame(BlueGigaCommand bleFrame) throws IllegalStateException {
176         sendFrame(bleFrame, true);
177     }
178
179     private void sendFrame(BlueGigaCommand bleFrame, boolean checkIsAlive) throws IllegalStateException {
180         if (checkIsAlive) {
181             checkIfAlive();
182         }
183
184         // Send the data
185         logger.trace("sendFrame: {}", bleFrame);
186         try {
187             int[] payload = bleFrame.serialize();
188             if (logger.isTraceEnabled()) {
189                 logger.trace("BLE TX: {}", printHex(payload, payload.length));
190             }
191             for (int b : payload) {
192                 outputStream.write(b);
193             }
194             outputStream.flush();
195
196         } catch (IOException e) {
197             throw new BlueGigaException("Error sending BLE frame", e);
198         }
199     }
200
201     public void addEventListener(BlueGigaSerialEventListener listener) {
202         eventListeners.add(listener);
203     }
204
205     public void removeEventListener(BlueGigaSerialEventListener listener) {
206         eventListeners.remove(listener);
207     }
208
209     public void addHandlerListener(BlueGigaHandlerListener listener) {
210         handlerListeners.add(listener);
211     }
212
213     public void removeHandlerListener(BlueGigaHandlerListener listener) {
214         handlerListeners.remove(listener);
215     }
216
217     /**
218      * Notify any transaction listeners when we receive a response.
219      *
220      * @param response the response data received
221      */
222     private void notifyEventListeners(final BlueGigaResponse response) {
223         // Notify the listeners
224         for (final BlueGigaSerialEventListener listener : eventListeners) {
225             try {
226                 listener.bluegigaFrameReceived(response);
227             } catch (Exception ex) {
228                 logger.warn("Execution error of a BlueGigaHandlerListener listener.", ex);
229             }
230         }
231     }
232
233     /**
234      * Notify handler event listeners that the handler was bluegigaClosed due to an error specified as an argument.
235      *
236      * @param reason the reason to bluegigaClosed
237      */
238     private void notifyEventListeners(final Exception reason) {
239         // It should be safe enough not to use the NotificationService as this is a fatal error, no any further actions
240         // can be done with the handler, a new handler should be re-created
241         // There is another reason why NotificationService can't be used - the listeners should be notified immediately
242         for (final BlueGigaHandlerListener listener : handlerListeners) {
243             try {
244                 listener.bluegigaClosed(reason);
245             } catch (Exception ex) {
246                 logger.warn("Execution error of a BlueGigaHandlerListener listener.", ex);
247             }
248         }
249     }
250
251     private String printHex(int[] data, int len) {
252         StringBuilder builder = new StringBuilder();
253
254         for (int cnt = 0; cnt < len; cnt++) {
255             builder.append(String.format("%02X ", data[cnt]));
256         }
257
258         return builder.toString();
259     }
260
261     private void checkIfAlive() {
262         if (!isAlive()) {
263             throw new IllegalStateException("Bluegiga handler is dead. Most likely because of IO errors. "
264                     + "Re-initialization of the BlueGigaSerialHandler is required.");
265         }
266     }
267
268     private void inboundMessageHandlerLoop() {
269         final int[] framecheckParams = { 0x00, 0x7F, 0xC0, 0xF8, 0xE0 };
270
271         logger.trace("BlueGiga BLE thread started");
272         int[] inputBuffer = new int[BLE_MAX_LENGTH];
273         int inputCount = 0;
274         int inputLength = 0;
275
276         while (!close) {
277             try {
278                 int val = inputStream.read();
279                 if (val == -1) {
280                     continue;
281                 }
282
283                 inputBuffer[inputCount++] = val;
284
285                 if (inputCount == 1) {
286                     if (inputStream.markSupported()) {
287                         inputStream.mark(BLE_MAX_LENGTH);
288                     }
289                 }
290
291                 if (inputCount < 4) {
292                     // The BGAPI protocol has no packet framing and no error detection, so we do a few
293                     // sanity checks on the header to try and allow resynchronisation.
294                     // Byte 0: Check technology type is bluetooth and high length is 0
295                     // Byte 1: Check length is less than 64 bytes
296                     // Byte 2: Check class ID is less than 8
297                     // Byte 3: Check command ID is less than 16
298                     if ((val & framecheckParams[inputCount]) != 0) {
299                         logger.debug("BlueGiga framing error byte {} = {}", inputCount, val);
300                         if (inputStream.markSupported()) {
301                             inputStream.reset();
302                         }
303                         inputCount = 0;
304                         continue;
305                     }
306                 } else if (inputCount == 4) {
307                     // Process the header to get the length
308                     inputLength = inputBuffer[1] + (inputBuffer[0] & 0x02 << 8) + 4;
309                     if (inputLength > BLE_MAX_LENGTH) {
310                         logger.debug("Received illegal BLE packet, length larger than max {} bytes ({})",
311                                 BLE_MAX_LENGTH, inputLength);
312                         if (inputStream.markSupported()) {
313                             inputStream.reset();
314                         }
315                         inputCount = 0;
316                         inputLength = 0;
317                         continue;
318                     }
319                 } else if (inputCount == inputLength) {
320                     // End of packet reached - process
321                     if (logger.isTraceEnabled()) {
322                         logger.trace("BLE RX: {}", printHex(inputBuffer, inputLength));
323                     }
324
325                     BlueGigaResponse responsePacket = BlueGigaResponsePackets.getPacket(inputBuffer);
326
327                     if (logger.isTraceEnabled()) {
328                         logger.trace("BLE RX: {}", responsePacket);
329                     }
330                     if (responsePacket != null) {
331                         notifyEventListeners(responsePacket);
332                     } else {
333                         logger.debug("Unknown packet received: {}", printHex(inputBuffer, inputLength));
334                     }
335
336                     inputCount = 0;
337                 }
338             } catch (Exception e) {
339                 logger.trace("BlueGiga BLE Exception: ", e);
340                 close = true;
341                 notifyEventListeners(new BlueGigaException("BlueGiga BLE Exception, reason " + e.getMessage(), e));
342             }
343         }
344         logger.debug("BlueGiga BLE exited.");
345     }
346
347     private Thread createBlueGigaBLEHandler(String uid) {
348         return new Thread(this::inboundMessageHandlerLoop, "OH-binding-" + uid + "-blueGigaBLEHandler");
349     }
350 }