2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.bluetooth.bluegiga.internal;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.OutputStream;
19 import java.util.concurrent.CopyOnWriteArraySet;
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;
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.
31 * @author Chris Jackson - Initial contribution and API
32 * @author Pauli Anttila - Split serial handler and transaction management
36 public class BlueGigaSerialHandler {
38 private static final int BLE_MAX_LENGTH = 64;
40 private final Logger logger = LoggerFactory.getLogger(BlueGigaSerialHandler.class);
43 * The event listeners will be notified of any asynchronous events
45 private final Set<BlueGigaSerialEventListener> eventListeners = new CopyOnWriteArraySet<>();
48 * The event listeners will be notified of any life-cycle events of the handler.
50 private final Set<BlueGigaHandlerListener> handlerListeners = new CopyOnWriteArraySet<>();
53 * Flag reflecting that parser has been closed and parser parserThread
56 private boolean close = false;
58 private final OutputStream outputStream;
59 private final InputStream inputStream;
60 private final Thread parserThread;
62 public BlueGigaSerialHandler(final InputStream inputStream, final OutputStream outputStream) {
63 this.outputStream = outputStream;
64 this.inputStream = inputStream;
67 parserThread = createBlueGigaBLEHandler();
68 parserThread.setUncaughtExceptionHandler((t, th) -> {
69 logger.warn("BluegigaSerialHandler terminating due to unhandled error", th);
71 parserThread.setDaemon(true);
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) {
81 throw new IllegalStateException("BlueGiga handler thread failed to start");
83 } catch (InterruptedException ignore) {
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);
94 // Wait BlueGiga controller have stopped all activity
96 logger.trace("Bytes available: {}", inputStream.available());
97 IOUtils.skipFully(inputStream, inputStream.available());
98 } catch (InterruptedException e) {
100 } catch (IOException e) {
103 logger.trace("Flush done");
107 * Requests parser thread to shutdown. Waits forever while the parser thread is getting shut down.
109 public void close() {
114 * Requests parser thread to shutdown. Waits specified milliseconds while the parser thread is getting shut down.
116 * @param timeout milliseconds to wait
118 public void close(long timeout) {
121 parserThread.interrupt();
122 // Give a fair chance to shutdown nicely
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.");
131 handlerListeners.clear();
132 eventListeners.clear();
133 logger.debug("Closed");
137 * Checks if parser thread is alive.
139 * @return true if parser thread is alive.
141 public boolean isAlive() {
142 return parserThread.isAlive() && !close;
145 public void sendFrame(BlueGigaCommand bleFrame) throws IllegalStateException {
146 sendFrame(bleFrame, true);
149 private void sendFrame(BlueGigaCommand bleFrame, boolean checkIsAlive) throws IllegalStateException {
155 logger.trace("sendFrame: {}", bleFrame);
157 int[] payload = bleFrame.serialize();
158 if (logger.isTraceEnabled()) {
159 logger.trace("BLE TX: {}", printHex(payload, payload.length));
161 for (int b : payload) {
162 outputStream.write(b);
164 outputStream.flush();
166 } catch (IOException e) {
167 throw new BlueGigaException("Error sending BLE frame", e);
171 public void addEventListener(BlueGigaSerialEventListener listener) {
172 eventListeners.add(listener);
175 public void removeEventListener(BlueGigaSerialEventListener listener) {
176 eventListeners.remove(listener);
179 public void addHandlerListener(BlueGigaHandlerListener listener) {
180 handlerListeners.add(listener);
183 public void removeHandlerListener(BlueGigaHandlerListener listener) {
184 handlerListeners.remove(listener);
188 * Notify any transaction listeners when we receive a response.
190 * @param response the response data received
192 private void notifyEventListeners(final BlueGigaResponse response) {
193 // Notify the listeners
194 for (final BlueGigaSerialEventListener listener : eventListeners) {
196 listener.bluegigaFrameReceived(response);
197 } catch (Exception ex) {
198 logger.warn("Execution error of a BlueGigaHandlerListener listener.", ex);
204 * Notify handler event listeners that the handler was bluegigaClosed due to an error specified as an argument.
206 * @param reason the reason to bluegigaClosed
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) {
214 listener.bluegigaClosed(reason);
215 } catch (Exception ex) {
216 logger.warn("Execution error of a BlueGigaHandlerListener listener.", ex);
221 private String printHex(int[] data, int len) {
222 StringBuilder builder = new StringBuilder();
224 for (int cnt = 0; cnt < len; cnt++) {
225 builder.append(String.format("%02X ", data[cnt]));
228 return builder.toString();
231 private void checkIfAlive() {
233 throw new IllegalStateException("Bluegiga handler is dead. Most likely because of IO errors. "
234 + "Re-initialization of the BlueGigaSerialHandler is required.");
238 private void inboundMessageHandlerLoop() {
239 final int[] framecheckParams = { 0x00, 0x7F, 0xC0, 0xF8, 0xE0 };
241 int exceptionCnt = 0;
242 logger.trace("BlueGiga BLE thread started");
243 int[] inputBuffer = new int[BLE_MAX_LENGTH];
249 int val = inputStream.read();
254 inputBuffer[inputCount++] = val;
256 if (inputCount == 1) {
257 if (inputStream.markSupported()) {
258 inputStream.mark(BLE_MAX_LENGTH);
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
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()) {
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 if (inputStream.markSupported()) {
290 if (inputCount == inputLength) {
291 if (logger.isTraceEnabled()) {
292 logger.trace("BLE RX: {}", printHex(inputBuffer, inputLength));
295 // End of packet reached - process
296 BlueGigaResponse responsePacket = BlueGigaResponsePackets.getPacket(inputBuffer);
298 if (logger.isTraceEnabled()) {
299 logger.trace("BLE RX: {}", responsePacket);
301 if (responsePacket != null) {
302 notifyEventListeners(responsePacket);
309 } catch (IOException e) {
310 logger.debug("BlueGiga BLE IOException: ", e);
312 if (exceptionCnt++ > 10) {
313 logger.error("BlueGiga BLE exception count exceeded, closing handler");
315 notifyEventListeners(e);
317 } catch (Exception e) {
318 logger.debug("BlueGiga BLE Exception, closing handler", e);
320 notifyEventListeners(e);
323 logger.debug("BlueGiga BLE exited.");
326 private Thread createBlueGigaBLEHandler() {
327 return new Thread(this::inboundMessageHandlerLoop, "BlueGigaBLEHandler");