]> git.basschouten.com Git - openhab-addons.git/blob
0150ba3954aaf3426f75fce0433031bb4bf29092
[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.handler;
14
15 import java.io.BufferedInputStream;
16 import java.io.BufferedOutputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.OutputStream;
20 import java.util.Map;
21 import java.util.Optional;
22 import java.util.UUID;
23 import java.util.concurrent.CancellationException;
24 import java.util.concurrent.CompletableFuture;
25 import java.util.concurrent.CompletionException;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.Future;
28 import java.util.concurrent.ScheduledExecutorService;
29 import java.util.concurrent.ScheduledFuture;
30 import java.util.concurrent.TimeUnit;
31
32 import org.apache.commons.io.IOUtils;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.bluetooth.AbstractBluetoothBridgeHandler;
36 import org.openhab.binding.bluetooth.BluetoothAddress;
37 import org.openhab.binding.bluetooth.BluetoothBindingConstants;
38 import org.openhab.binding.bluetooth.bluegiga.BlueGigaAdapterConstants;
39 import org.openhab.binding.bluetooth.bluegiga.BlueGigaBluetoothDevice;
40 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaCommand;
41 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaConfiguration;
42 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaEventListener;
43 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaException;
44 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaHandlerListener;
45 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaResponse;
46 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaSerialHandler;
47 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaTransactionManager;
48 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaAttributeWriteCommand;
49 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaAttributeWriteResponse;
50 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaFindInformationCommand;
51 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaFindInformationResponse;
52 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByGroupTypeCommand;
53 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByGroupTypeResponse;
54 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByHandleCommand;
55 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByHandleResponse;
56 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByTypeCommand;
57 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaReadByTypeResponse;
58 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaConnectionStatusEvent;
59 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaDisconnectCommand;
60 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaDisconnectResponse;
61 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaDisconnectedEvent;
62 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaConnectDirectCommand;
63 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaConnectDirectResponse;
64 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaDiscoverCommand;
65 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaDiscoverResponse;
66 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaEndProcedureCommand;
67 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaEndProcedureResponse;
68 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaScanResponseEvent;
69 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaSetModeCommand;
70 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaSetModeResponse;
71 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaSetScanParametersCommand;
72 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaSetScanParametersResponse;
73 import org.openhab.binding.bluetooth.bluegiga.internal.command.system.BlueGigaAddressGetCommand;
74 import org.openhab.binding.bluetooth.bluegiga.internal.command.system.BlueGigaAddressGetResponse;
75 import org.openhab.binding.bluetooth.bluegiga.internal.command.system.BlueGigaGetConnectionsCommand;
76 import org.openhab.binding.bluetooth.bluegiga.internal.command.system.BlueGigaGetConnectionsResponse;
77 import org.openhab.binding.bluetooth.bluegiga.internal.command.system.BlueGigaGetInfoCommand;
78 import org.openhab.binding.bluetooth.bluegiga.internal.command.system.BlueGigaGetInfoResponse;
79 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BgApiResponse;
80 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BluetoothAddressType;
81 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.GapConnectableMode;
82 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.GapDiscoverMode;
83 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.GapDiscoverableMode;
84 import org.openhab.binding.bluetooth.util.RetryException;
85 import org.openhab.binding.bluetooth.util.RetryFuture;
86 import org.openhab.core.common.ThreadPoolManager;
87 import org.openhab.core.io.transport.serial.PortInUseException;
88 import org.openhab.core.io.transport.serial.SerialPort;
89 import org.openhab.core.io.transport.serial.SerialPortIdentifier;
90 import org.openhab.core.io.transport.serial.SerialPortManager;
91 import org.openhab.core.io.transport.serial.UnsupportedCommOperationException;
92 import org.openhab.core.thing.Bridge;
93 import org.openhab.core.thing.Thing;
94 import org.openhab.core.thing.ThingStatus;
95 import org.openhab.core.thing.ThingStatusDetail;
96 import org.slf4j.Logger;
97 import org.slf4j.LoggerFactory;
98
99 /**
100  * The {@link BlueGigaBridgeHandler} is responsible for interfacing to the BlueGiga Bluetooth adapter.
101  * It provides a private interface for {@link BlueGigaBluetoothDevice}s to access the dongle and provides top
102  * level adaptor functionality for scanning and arbitration.
103  * <p>
104  * The handler provides the serial interface to the dongle via the BlueGiga BG-API library.
105  * <p>
106  * In the BlueGiga dongle, we leave scanning enabled most of the time. Normally, it's just passive scanning, and active
107  * scanning is enabled when we want to include new devices. Passive scanning is enough for us to receive beacons etc
108  * that are transmitted periodically, and active scanning will get more information which may be useful when we are
109  * including new devices.
110  *
111  * @author Chris Jackson - Initial contribution
112  * @author Kai Kreuzer - Made handler implement BlueGigaHandlerListener
113  * @author Pauli Anttila - Many improvements
114  */
115 @NonNullByDefault
116 public class BlueGigaBridgeHandler extends AbstractBluetoothBridgeHandler<BlueGigaBluetoothDevice>
117         implements BlueGigaEventListener, BlueGigaHandlerListener {
118
119     private final Logger logger = LoggerFactory.getLogger(BlueGigaBridgeHandler.class);
120
121     private final int COMMAND_TIMEOUT_MS = 5000;
122     private final int INITIALIZATION_INTERVAL_SEC = 60;
123
124     private final SerialPortManager serialPortManager;
125
126     private final ScheduledExecutorService executor = ThreadPoolManager.getScheduledPool("BlueGiga");
127
128     private BlueGigaConfiguration configuration = new BlueGigaConfiguration();
129
130     // The serial port input stream.
131     private Optional<InputStream> inputStream = Optional.empty();
132
133     // The serial port output stream.
134     private Optional<OutputStream> outputStream = Optional.empty();
135
136     // The BlueGiga API handler
137     private CompletableFuture<BlueGigaSerialHandler> serialHandler = CompletableFuture
138             .failedFuture(new IllegalStateException("Uninitialized"));
139
140     // The BlueGiga transaction manager
141     @NonNullByDefault({})
142     private CompletableFuture<BlueGigaTransactionManager> transactionManager = CompletableFuture
143             .failedFuture(new IllegalStateException("Uninitialized"));
144
145     // The maximum number of connections this interface supports
146     private int maxConnections = 0;
147
148     // Our BT address
149     private @Nullable BluetoothAddress address;
150
151     // Map of open connections
152     private final Map<Integer, BluetoothAddress> connections = new ConcurrentHashMap<>();
153
154     private volatile boolean initComplete = false;
155
156     private CompletableFuture<SerialPort> serialPortFuture = CompletableFuture
157             .failedFuture(new IllegalStateException("Uninitialized"));
158
159     private @Nullable ScheduledFuture<?> removeInactiveDevicesTask;
160     private @Nullable ScheduledFuture<?> discoveryTask;
161
162     private @Nullable Future<?> passiveScanIdleTimer;
163
164     public BlueGigaBridgeHandler(Bridge bridge, SerialPortManager serialPortManager) {
165         super(bridge);
166         this.serialPortManager = serialPortManager;
167     }
168
169     @Override
170     public void initialize() {
171         logger.info("Initializing BlueGiga");
172         super.initialize();
173         Optional<BlueGigaConfiguration> cfg = Optional.of(getConfigAs(BlueGigaConfiguration.class));
174         updateStatus(ThingStatus.UNKNOWN);
175         if (cfg.isPresent()) {
176             configuration = cfg.get();
177             serialPortFuture = RetryFuture.callWithRetry(() -> {
178                 var localFuture = serialPortFuture;
179                 logger.debug("Initialize BlueGiga");
180                 logger.debug("Using configuration: {}", configuration);
181
182                 String serialPortName = configuration.port;
183                 int baudRate = 115200;
184
185                 logger.debug("Connecting to serial port '{}'", serialPortName);
186                 try {
187                     SerialPortIdentifier portIdentifier = serialPortManager.getIdentifier(serialPortName);
188                     if (portIdentifier == null) {
189                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Port does not exist");
190                         throw new RetryException(INITIALIZATION_INTERVAL_SEC, TimeUnit.SECONDS);
191                     }
192                     SerialPort sp = portIdentifier.open("org.openhab.binding.bluetooth.bluegiga", 2000);
193                     sp.setSerialPortParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
194                             SerialPort.PARITY_NONE);
195
196                     sp.setFlowControlMode(SerialPort.FLOWCONTROL_RTSCTS_OUT);
197                     sp.enableReceiveThreshold(1);
198                     sp.enableReceiveTimeout(2000);
199
200                     // RXTX serial port library causes high CPU load
201                     // Start event listener, which will just sleep and slow down event loop
202                     sp.notifyOnDataAvailable(true);
203
204                     logger.info("Connected to serial port '{}'.", serialPortName);
205
206                     try {
207                         inputStream = Optional.of(new BufferedInputStream(sp.getInputStream()));
208                         outputStream = Optional.of(new BufferedOutputStream(sp.getOutputStream()));
209                     } catch (IOException e) {
210                         logger.error("Error getting serial streams", e);
211                         throw new RetryException(INITIALIZATION_INTERVAL_SEC, TimeUnit.SECONDS);
212                     }
213                     // if this future has been cancelled while this was running, then we
214                     // need to make sure that we close this port
215                     localFuture.whenComplete((port, th) -> {
216                         if (th != null) {
217                             // we need to shut down the port now.
218                             closeSerialPort(sp);
219                         }
220                     });
221                     return sp;
222                 } catch (PortInUseException e) {
223                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
224                             "Serial Error: Port in use");
225                     throw new RetryException(INITIALIZATION_INTERVAL_SEC, TimeUnit.SECONDS);
226                 } catch (UnsupportedCommOperationException e) {
227                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
228                             "Serial Error: Unsupported operation");
229                     throw new RetryException(INITIALIZATION_INTERVAL_SEC, TimeUnit.SECONDS);
230                 } catch (RuntimeException ex) {
231                     logger.debug("Start failed", ex);
232                     throw new RetryException(INITIALIZATION_INTERVAL_SEC, TimeUnit.SECONDS);
233                 }
234             }, executor);
235
236             serialHandler = serialPortFuture
237                     .thenApply(sp -> new BlueGigaSerialHandler(inputStream.get(), outputStream.get()));
238             transactionManager = serialHandler.thenApply(sh -> {
239                 BlueGigaTransactionManager th = new BlueGigaTransactionManager(sh, executor);
240                 sh.addHandlerListener(this);
241                 th.addEventListener(this);
242                 return th;
243             });
244             transactionManager.thenRun(() -> {
245                 try {
246                     // Stop any procedures that are running
247                     bgEndProcedure();
248
249                     // Set mode to non-discoverable etc.
250                     bgSetMode();
251
252                     // Get maximum parallel connections
253                     maxConnections = readMaxConnections().getMaxconn();
254
255                     // Close all connections so we start from a known position
256                     for (int connection = 0; connection < maxConnections; connection++) {
257                         sendCommandWithoutChecks(
258                                 new BlueGigaDisconnectCommand.CommandBuilder().withConnection(connection).build(),
259                                 BlueGigaDisconnectResponse.class);
260                     }
261
262                     // Get our Bluetooth address
263                     address = new BluetoothAddress(readAddress().getAddress());
264
265                     updateThingProperties();
266
267                     initComplete = true;
268                     updateStatus(ThingStatus.ONLINE);
269                     startScheduledTasks();
270                 } catch (BlueGigaException e) {
271                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
272                             "Initialization of BlueGiga controller failed");
273                 }
274             }).exceptionally(th -> {
275                 if (th instanceof CompletionException && th.getCause() instanceof CancellationException) {
276                     // cancellation is a normal reason for failure, so no need to print it.
277                     return null;
278                 }
279                 logger.warn("Error initializing bluegiga", th);
280                 return null;
281             });
282
283         } else {
284             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR);
285         }
286     }
287
288     @Override
289     public void dispose() {
290         logger.info("Disposing BlueGiga");
291         stop();
292         stopScheduledTasks();
293         super.dispose();
294     }
295
296     private void stop() {
297         transactionManager.thenAccept(tman -> {
298             tman.removeEventListener(this);
299             tman.close();
300         });
301         serialHandler.thenAccept(sh -> {
302             sh.removeHandlerListener(this);
303             sh.close();
304         });
305         address = null;
306         initComplete = false;
307         connections.clear();
308
309         serialPortFuture.thenAccept(this::closeSerialPort);
310         serialPortFuture.cancel(false);
311     }
312
313     private void schedulePassiveScan() {
314         cancelScheduledPassiveScan();
315         passiveScanIdleTimer = executor.schedule(() -> {
316             if (!activeScanEnabled) {
317                 logger.debug("Activate passive scan");
318                 bgEndProcedure();
319                 bgStartScanning(false, configuration.passiveScanInterval, configuration.passiveScanWindow);
320             } else {
321                 logger.debug("Ignore passive scan activation as active scan is active");
322             }
323         }, configuration.passiveScanIdleTime, TimeUnit.MILLISECONDS);
324     }
325
326     private void cancelScheduledPassiveScan() {
327         if (passiveScanIdleTimer != null) {
328             passiveScanIdleTimer.cancel(true);
329         }
330     }
331
332     private void startScheduledTasks() {
333         schedulePassiveScan();
334         discoveryTask = scheduler.scheduleWithFixedDelay(this::refreshDiscoveredDevices, 0, 10, TimeUnit.SECONDS);
335     }
336
337     private void stopScheduledTasks() {
338         cancelScheduledPassiveScan();
339         if (removeInactiveDevicesTask != null) {
340             removeInactiveDevicesTask.cancel(true);
341             removeInactiveDevicesTask = null;
342         }
343         if (discoveryTask != null) {
344             discoveryTask.cancel(true);
345             discoveryTask = null;
346         }
347     }
348
349     private BlueGigaGetConnectionsResponse readMaxConnections() throws BlueGigaException {
350         return sendCommandWithoutChecks(new BlueGigaGetConnectionsCommand(), BlueGigaGetConnectionsResponse.class);
351     }
352
353     private BlueGigaAddressGetResponse readAddress() throws BlueGigaException {
354         return sendCommandWithoutChecks(new BlueGigaAddressGetCommand(), BlueGigaAddressGetResponse.class);
355     }
356
357     private BlueGigaGetInfoResponse readInfo() throws BlueGigaException {
358         return sendCommandWithoutChecks(new BlueGigaGetInfoCommand(), BlueGigaGetInfoResponse.class);
359     }
360
361     private void updateThingProperties() throws BlueGigaException {
362         BlueGigaGetInfoResponse infoResponse = readInfo();
363
364         Map<String, String> properties = editProperties();
365         properties.put(BluetoothBindingConstants.PROPERTY_MAXCONNECTIONS, Integer.toString(maxConnections));
366         properties.put(Thing.PROPERTY_FIRMWARE_VERSION,
367                 String.format("%d.%d", infoResponse.getMajor(), infoResponse.getMinor()));
368         properties.put(Thing.PROPERTY_HARDWARE_VERSION, Integer.toString(infoResponse.getHardware()));
369         properties.put(BlueGigaAdapterConstants.PROPERTY_PROTOCOL, Integer.toString(infoResponse.getProtocolVersion()));
370         properties.put(BlueGigaAdapterConstants.PROPERTY_LINKLAYER, Integer.toString(infoResponse.getLlVersion()));
371         updateProperties(properties);
372     }
373
374     private void closeSerialPort(SerialPort sp) {
375         sp.removeEventListener();
376         try {
377             sp.disableReceiveTimeout();
378         } catch (Exception e) {
379             // Ignore all as RXTX seems to send arbitrary exceptions when BlueGiga module is detached
380         } finally {
381             outputStream.ifPresent(output -> {
382                 IOUtils.closeQuietly(output);
383             });
384             inputStream.ifPresent(input -> {
385                 IOUtils.closeQuietly(input);
386             });
387             sp.close();
388             logger.debug("Closed serial port.");
389             inputStream = Optional.empty();
390             outputStream = Optional.empty();
391         }
392     }
393
394     @Override
395     public void scanStart() {
396         super.scanStart();
397         logger.debug("Start active scan");
398         // Stop the passive scan
399         cancelScheduledPassiveScan();
400         bgEndProcedure();
401
402         // Start a active scan
403         bgStartScanning(true, configuration.activeScanInterval, configuration.activeScanWindow);
404     }
405
406     @Override
407     public void scanStop() {
408         super.scanStop();
409         logger.debug("Stop active scan");
410
411         // Stop the active scan
412         bgEndProcedure();
413
414         // Start a passive scan after idle delay
415         schedulePassiveScan();
416     }
417
418     @Override
419     public @Nullable BluetoothAddress getAddress() {
420         BluetoothAddress addr = address;
421         if (addr != null) {
422             return addr;
423         } else {
424             throw new IllegalStateException("Adapter has not been initialized yet!");
425         }
426     }
427
428     @Override
429     protected BlueGigaBluetoothDevice createDevice(BluetoothAddress address) {
430         return new BlueGigaBluetoothDevice(this, address, BluetoothAddressType.UNKNOWN);
431     }
432
433     /**
434      * Connects to a device.
435      * <p>
436      * If the device is already connected, or the attempt to connect failed, then we return false. If we have reached
437      * the maximum number of connections supported by this dongle, then we return false.
438      *
439      * @param address the device {@link BluetoothAddress} to connect to
440      * @param addressType the {@link BluetoothAddressType} of the device
441      * @return true if the connection was started
442      */
443     public boolean bgConnect(BluetoothAddress address, BluetoothAddressType addressType) {
444         // Check the connection to make sure we're not already connected to this device
445         if (connections.containsValue(address)) {
446             return false;
447         }
448
449         // FIXME: When getting here, I always found all connections to be already taken and thus the code never
450         // proceeded. Relaxing this condition did not do any obvious harm, but now guaranteed that the services are
451         // queried from the device.
452         if (connections.size() == maxConnections + 1) {
453             logger.debug("BlueGiga: Attempt to connect to {} but no connections available.", address);
454             return false;
455         }
456
457         logger.debug("BlueGiga Connect: address {}.", address);
458
459         // @formatter:off
460         BlueGigaConnectDirectCommand command = new BlueGigaConnectDirectCommand.CommandBuilder()
461                 .withAddress(address.toString())
462                 .withAddrType(addressType)
463                 .withConnIntervalMin(configuration.connIntervalMin)
464                 .withConnIntervalMax(configuration.connIntervalMax)
465                 .withLatency(configuration.connLatency)
466                 .withTimeout(configuration.connTimeout)
467                 .build();
468         // @formatter:on
469         try {
470             return sendCommand(command, BlueGigaConnectDirectResponse.class, true).getResult() == BgApiResponse.SUCCESS;
471         } catch (BlueGigaException e) {
472             logger.debug("Error occured when sending connect command to device {}, reason: {}.", address,
473                     e.getMessage());
474             return false;
475         }
476     }
477
478     /**
479      * Close a connection using {@link BlueGigaDisconnectCommand}
480      *
481      * @param connectionHandle
482      * @return
483      */
484     public boolean bgDisconnect(int connectionHandle) {
485         logger.debug("BlueGiga Disconnect: connection {}", connectionHandle);
486         BlueGigaDisconnectCommand command = new BlueGigaDisconnectCommand.CommandBuilder()
487                 .withConnection(connectionHandle).build();
488
489         try {
490             return sendCommand(command, BlueGigaDisconnectResponse.class, true).getResult() == BgApiResponse.SUCCESS;
491         } catch (BlueGigaException e) {
492             logger.debug("Error occured when sending disconnect command to device {}, reason: {}.", address,
493                     e.getMessage());
494             return false;
495         }
496     }
497
498     /**
499      * Start a read of all primary services using {@link BlueGigaReadByGroupTypeCommand}
500      *
501      * @param connectionHandle
502      * @return true if successful
503      */
504     public boolean bgFindPrimaryServices(int connectionHandle) {
505         logger.debug("BlueGiga FindPrimary: connection {}", connectionHandle);
506         // @formatter:off
507         BlueGigaReadByGroupTypeCommand command = new BlueGigaReadByGroupTypeCommand.CommandBuilder()
508                 .withConnection(connectionHandle)
509                 .withStart(1)
510                 .withEnd(65535)
511                 .withUuid(UUID.fromString("00002800-0000-1000-8000-00805F9B34FB"))
512                 .build();
513         // @formatter:on
514         try {
515             return sendCommand(command, BlueGigaReadByGroupTypeResponse.class, true)
516                     .getResult() == BgApiResponse.SUCCESS;
517         } catch (BlueGigaException e) {
518             logger.debug("Error occured when sending read primary services command to device {}, reason: {}.", address,
519                     e.getMessage());
520             return false;
521         }
522     }
523
524     /**
525      * Start a read of all characteristics using {@link BlueGigaFindInformationCommand}
526      *
527      * @param connectionHandle
528      * @return true if successful
529      */
530     public boolean bgFindCharacteristics(int connectionHandle) {
531         logger.debug("BlueGiga Find: connection {}", connectionHandle);
532         // @formatter:off
533         BlueGigaFindInformationCommand command = new BlueGigaFindInformationCommand.CommandBuilder()
534                 .withConnection(connectionHandle)
535                 .withStart(1)
536                 .withEnd(65535)
537                 .build();
538         // @formatter:on
539         try {
540             return sendCommand(command, BlueGigaFindInformationResponse.class, true)
541                     .getResult() == BgApiResponse.SUCCESS;
542         } catch (BlueGigaException e) {
543             logger.debug("Error occured when sending read characteristics command to device {}, reason: {}.", address,
544                     e.getMessage());
545             return false;
546         }
547     }
548
549     public boolean bgReadCharacteristicDeclarations(int connectionHandle) {
550         logger.debug("BlueGiga Find: connection {}", connectionHandle);
551         // @formatter:off
552         BlueGigaReadByTypeCommand command = new BlueGigaReadByTypeCommand.CommandBuilder()
553                 .withConnection(connectionHandle)
554                 .withStart(1)
555                 .withEnd(65535)
556                 .withUUID(BluetoothBindingConstants.ATTR_CHARACTERISTIC_DECLARATION)
557                 .build();
558         // @formatter:on
559         try {
560             return sendCommand(command, BlueGigaReadByTypeResponse.class, true).getResult() == BgApiResponse.SUCCESS;
561         } catch (BlueGigaException e) {
562             logger.debug("Error occured when sending read characteristics command to device {}, reason: {}.", address,
563                     e.getMessage());
564             return false;
565         }
566     }
567
568     /**
569      * Read a characteristic using {@link BlueGigaReadByHandleCommand}
570      *
571      * @param connectionHandle
572      * @param handle
573      * @return true if successful
574      */
575     public boolean bgReadCharacteristic(int connectionHandle, int handle) {
576         logger.debug("BlueGiga Read: connection {}, handle {}", connectionHandle, handle);
577         // @formatter:off
578         BlueGigaReadByHandleCommand command = new BlueGigaReadByHandleCommand.CommandBuilder()
579                 .withConnection(connectionHandle)
580                 .withChrHandle(handle)
581                 .build();
582         // @formatter:on
583         try {
584             return sendCommand(command, BlueGigaReadByHandleResponse.class, true).getResult() == BgApiResponse.SUCCESS;
585         } catch (BlueGigaException e) {
586             logger.debug("Error occured when sending read characteristics command to device {}, reason: {}.", address,
587                     e.getMessage());
588             return false;
589         }
590     }
591
592     /**
593      * Write a characteristic using {@link BlueGigaAttributeWriteCommand}
594      *
595      * @param connectionHandle
596      * @param handle
597      * @param value
598      * @return true if successful
599      */
600     public boolean bgWriteCharacteristic(int connectionHandle, int handle, int[] value) {
601         logger.debug("BlueGiga Write: connection {}, handle {}", connectionHandle, handle);
602         // @formatter:off
603         BlueGigaAttributeWriteCommand command = new BlueGigaAttributeWriteCommand.CommandBuilder()
604                 .withConnection(connectionHandle)
605                 .withAttHandle(handle)
606                 .withData(value)
607                 .build();
608         // @formatter:on
609         try {
610             return sendCommand(command, BlueGigaAttributeWriteResponse.class, true)
611                     .getResult() == BgApiResponse.SUCCESS;
612         } catch (BlueGigaException e) {
613             logger.debug("Error occured when sending write characteristics command to device {}, reason: {}.", address,
614                     e.getMessage());
615             return false;
616         }
617     }
618
619     /*
620      * The following methods are private methods for handling the BlueGiga protocol
621      */
622     private boolean bgEndProcedure() {
623         try {
624             return sendCommandWithoutChecks(new BlueGigaEndProcedureCommand(), BlueGigaEndProcedureResponse.class)
625                     .getResult() == BgApiResponse.SUCCESS;
626         } catch (BlueGigaException e) {
627             logger.debug("Error occured when sending end procedure command.");
628             return false;
629         }
630     }
631
632     private boolean bgSetMode() {
633         try {
634             // @formatter:off
635             BlueGigaSetModeCommand command = new BlueGigaSetModeCommand.CommandBuilder()
636                     .withConnect(GapConnectableMode.GAP_NON_CONNECTABLE)
637                     .withDiscover(GapDiscoverableMode.GAP_NON_DISCOVERABLE)
638                     .build();
639             // @formatter:on
640             return sendCommandWithoutChecks(command, BlueGigaSetModeResponse.class)
641                     .getResult() == BgApiResponse.SUCCESS;
642         } catch (BlueGigaException e) {
643             logger.debug("Error occured when sending set mode command, reason: {}", e.getMessage());
644             return false;
645         }
646     }
647
648     /**
649      * Starts scanning on the dongle
650      *
651      * @param active true for active scanning
652      */
653     private boolean bgStartScanning(boolean active, int interval, int window) {
654         try {
655             // @formatter:off
656             BlueGigaSetScanParametersCommand scanCommand = new BlueGigaSetScanParametersCommand.CommandBuilder()
657                     .withActiveScanning(active)
658                     .withScanInterval(interval)
659                     .withScanWindow(window)
660                     .build();
661             // @formatter:on
662             if (sendCommand(scanCommand, BlueGigaSetScanParametersResponse.class, false)
663                     .getResult() == BgApiResponse.SUCCESS) {
664                 BlueGigaDiscoverCommand discoverCommand = new BlueGigaDiscoverCommand.CommandBuilder()
665                         .withMode(GapDiscoverMode.GAP_DISCOVER_OBSERVATION).build();
666                 if (sendCommand(discoverCommand, BlueGigaDiscoverResponse.class, false)
667                         .getResult() == BgApiResponse.SUCCESS) {
668                     logger.debug("{} scanning succesfully started.", active ? "Active" : "Passive");
669                     return true;
670                 }
671             }
672         } catch (BlueGigaException e) {
673             logger.debug("Error occured when sending start scan command, reason: {}", e.getMessage());
674         }
675         logger.debug("Scan start failed.");
676         return false;
677     }
678
679     /**
680      * Send command only if initialization phase is successfully done
681      */
682     private <T extends BlueGigaResponse> T sendCommand(BlueGigaCommand command, Class<T> expectedResponse,
683             boolean schedulePassiveScan) throws BlueGigaException {
684         if (!initComplete) {
685             throw new BlueGigaException("BlueGiga not initialized");
686         }
687
688         if (schedulePassiveScan) {
689             cancelScheduledPassiveScan();
690         }
691         try {
692             return sendCommandWithoutChecks(command, expectedResponse);
693         } finally {
694             if (schedulePassiveScan) {
695                 schedulePassiveScan();
696             }
697         }
698     }
699
700     /**
701      * Forcefully send command without any checks
702      */
703     private <T extends BlueGigaResponse> T sendCommandWithoutChecks(BlueGigaCommand command, Class<T> expectedResponse)
704             throws BlueGigaException {
705         BlueGigaTransactionManager manager = transactionManager.getNow(null);
706         if (manager != null) {
707             return manager.sendTransaction(command, expectedResponse, COMMAND_TIMEOUT_MS);
708         } else {
709             throw new BlueGigaException("Transaction manager missing");
710         }
711     }
712
713     /**
714      * Add an event listener for the BlueGiga events
715      *
716      * @param listener the {@link BlueGigaEventListener} to add
717      */
718     public void addEventListener(BlueGigaEventListener listener) {
719         transactionManager.thenAccept(manager -> {
720             manager.addEventListener(listener);
721         });
722     }
723
724     /**
725      * Remove an event listener for the BlueGiga events
726      *
727      * @param listener the {@link BlueGigaEventListener} to remove
728      */
729     public void removeEventListener(BlueGigaEventListener listener) {
730         transactionManager.thenAccept(manager -> {
731             manager.removeEventListener(listener);
732         });
733     }
734
735     @Override
736     public void bluegigaEventReceived(@Nullable BlueGigaResponse event) {
737         if (event instanceof BlueGigaScanResponseEvent) {
738             if (initComplete) {
739                 BlueGigaScanResponseEvent scanEvent = (BlueGigaScanResponseEvent) event;
740
741                 // We use the scan event to add any devices we hear to the devices list
742                 // The device gets created, and then manages itself for discovery etc.
743                 BluetoothAddress sender = new BluetoothAddress(scanEvent.getSender());
744                 BlueGigaBluetoothDevice device = getDevice(sender);
745                 device.setAddressType(scanEvent.getAddressType());
746                 deviceDiscovered(device);
747             } else {
748                 logger.trace("Ignore BlueGigaScanResponseEvent as initialization is not complete");
749             }
750             return;
751         }
752
753         if (event instanceof BlueGigaConnectionStatusEvent) {
754             BlueGigaConnectionStatusEvent connectionEvent = (BlueGigaConnectionStatusEvent) event;
755             connections.put(connectionEvent.getConnection(), new BluetoothAddress(connectionEvent.getAddress()));
756         }
757
758         if (event instanceof BlueGigaDisconnectedEvent) {
759             BlueGigaDisconnectedEvent disconnectedEvent = (BlueGigaDisconnectedEvent) event;
760             connections.remove(disconnectedEvent.getConnection());
761         }
762     }
763
764     @Override
765     public void bluegigaClosed(Exception reason) {
766         logger.debug("BlueGiga connection closed, request reinitialization");
767         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason.getMessage());
768         initComplete = false;
769     }
770 }