2 * Copyright (c) 2010-2023 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.EOFException;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.io.OutputStream;
20 import java.util.concurrent.CopyOnWriteArraySet;
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 String uid, final InputStream inputStream, final OutputStream outputStream) {
63 this.outputStream = outputStream;
64 this.inputStream = inputStream;
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()));
73 parserThread.setDaemon(true);
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) {
83 throw new IllegalStateException("BlueGiga handler thread failed to start");
85 } catch (InterruptedException ignore) {
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);
96 // Wait BlueGiga controller have stopped all activity
98 logger.trace("Bytes available: {}", inputStream.available());
99 skipFully(inputStream, inputStream.available());
100 } catch (InterruptedException e) {
102 } catch (IOException e) {
105 logger.trace("Flush done");
108 private void skipFully(final InputStream input, final long toSkip) throws IOException {
110 throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip);
113 long remain = toSkip;
115 final byte[] byteArray = new byte[8192];
117 final long n = input.read(byteArray, 0, (int) Math.min(remain, byteArray.length));
124 long skipped = toSkip - remain;
125 if (skipped != toSkip) {
126 throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped);
131 * Requests parser thread to shutdown. Waits forever while the parser thread is getting shut down.
133 public void close() {
138 * Requests parser thread to shutdown. Waits specified milliseconds while the parser thread is getting shut down.
140 * @param timeout milliseconds to wait
142 public void close(long timeout) {
145 parserThread.interrupt();
146 // Give a fair chance to shutdown nicely
149 outputStream.close();
150 } catch (IOException e) {
154 } catch (IOException e) {
156 parserThread.join(0);
157 } catch (InterruptedException e) {
158 logger.warn("Interrupted in packet parser thread shutdown join.");
161 handlerListeners.clear();
162 eventListeners.clear();
163 logger.debug("Closed");
167 * Checks if parser thread is alive.
169 * @return true if parser thread is alive.
171 public boolean isAlive() {
172 return parserThread.isAlive() && !close;
175 public void sendFrame(BlueGigaCommand bleFrame) throws IllegalStateException {
176 sendFrame(bleFrame, true);
179 private void sendFrame(BlueGigaCommand bleFrame, boolean checkIsAlive) throws IllegalStateException {
185 logger.trace("sendFrame: {}", bleFrame);
187 int[] payload = bleFrame.serialize();
188 if (logger.isTraceEnabled()) {
189 logger.trace("BLE TX: {}", printHex(payload, payload.length));
191 for (int b : payload) {
192 outputStream.write(b);
194 outputStream.flush();
196 } catch (IOException e) {
197 throw new BlueGigaException("Error sending BLE frame", e);
201 public void addEventListener(BlueGigaSerialEventListener listener) {
202 eventListeners.add(listener);
205 public void removeEventListener(BlueGigaSerialEventListener listener) {
206 eventListeners.remove(listener);
209 public void addHandlerListener(BlueGigaHandlerListener listener) {
210 handlerListeners.add(listener);
213 public void removeHandlerListener(BlueGigaHandlerListener listener) {
214 handlerListeners.remove(listener);
218 * Notify any transaction listeners when we receive a response.
220 * @param response the response data received
222 private void notifyEventListeners(final BlueGigaResponse response) {
223 // Notify the listeners
224 for (final BlueGigaSerialEventListener listener : eventListeners) {
226 listener.bluegigaFrameReceived(response);
227 } catch (Exception ex) {
228 logger.warn("Execution error of a BlueGigaHandlerListener listener.", ex);
234 * Notify handler event listeners that the handler was bluegigaClosed due to an error specified as an argument.
236 * @param reason the reason to bluegigaClosed
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) {
244 listener.bluegigaClosed(reason);
245 } catch (Exception ex) {
246 logger.warn("Execution error of a BlueGigaHandlerListener listener.", ex);
251 private String printHex(int[] data, int len) {
252 StringBuilder builder = new StringBuilder();
254 for (int cnt = 0; cnt < len; cnt++) {
255 builder.append(String.format("%02X ", data[cnt]));
258 return builder.toString();
261 private void checkIfAlive() {
263 throw new IllegalStateException("Bluegiga handler is dead. Most likely because of IO errors. "
264 + "Re-initialization of the BlueGigaSerialHandler is required.");
268 private void inboundMessageHandlerLoop() {
269 final int[] framecheckParams = { 0x00, 0x7F, 0xC0, 0xF8, 0xE0 };
271 logger.trace("BlueGiga BLE thread started");
272 int[] inputBuffer = new int[BLE_MAX_LENGTH];
278 int val = inputStream.read();
283 inputBuffer[inputCount++] = val;
285 if (inputCount == 1) {
286 if (inputStream.markSupported()) {
287 inputStream.mark(BLE_MAX_LENGTH);
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()) {
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()) {
319 } else if (inputCount == inputLength) {
320 // End of packet reached - process
321 if (logger.isTraceEnabled()) {
322 logger.trace("BLE RX: {}", printHex(inputBuffer, inputLength));
325 BlueGigaResponse responsePacket = BlueGigaResponsePackets.getPacket(inputBuffer);
327 if (logger.isTraceEnabled()) {
328 logger.trace("BLE RX: {}", responsePacket);
330 if (responsePacket != null) {
331 notifyEventListeners(responsePacket);
333 logger.debug("Unknown packet received: {}", printHex(inputBuffer, inputLength));
338 } catch (Exception e) {
339 logger.trace("BlueGiga BLE Exception: ", e);
341 notifyEventListeners(new BlueGigaException("BlueGiga BLE Exception, reason " + e.getMessage(), e));
344 logger.debug("BlueGiga BLE exited.");
347 private Thread createBlueGigaBLEHandler(String uid) {
348 return new Thread(this::inboundMessageHandlerLoop, "OH-binding-" + uid + "-blueGigaBLEHandler");