]> git.basschouten.com Git - openhab-addons.git/blob
e57e3d56a5d40b337e9cce00e4331e1c53c71c05
[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(getThing().getUID().getAsString(), inputStream.get(),
238                             outputStream.get()));
239             transactionManager = serialHandler.thenApply(sh -> {
240                 BlueGigaTransactionManager th = new BlueGigaTransactionManager(sh, executor);
241                 sh.addHandlerListener(this);
242                 th.addEventListener(this);
243                 return th;
244             });
245             transactionManager.thenRun(() -> {
246                 try {
247                     // Stop any procedures that are running
248                     bgEndProcedure();
249
250                     // Set mode to non-discoverable etc.
251                     bgSetMode();
252
253                     // Get maximum parallel connections
254                     maxConnections = readMaxConnections().getMaxconn();
255
256                     // Close all connections so we start from a known position
257                     for (int connection = 0; connection < maxConnections; connection++) {
258                         sendCommandWithoutChecks(
259                                 new BlueGigaDisconnectCommand.CommandBuilder().withConnection(connection).build(),
260                                 BlueGigaDisconnectResponse.class);
261                     }
262
263                     // Get our Bluetooth address
264                     address = new BluetoothAddress(readAddress().getAddress());
265
266                     updateThingProperties();
267
268                     initComplete = true;
269                     updateStatus(ThingStatus.ONLINE);
270                     startScheduledTasks();
271                 } catch (BlueGigaException e) {
272                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
273                             "Initialization of BlueGiga controller failed");
274                 }
275             }).exceptionally(th -> {
276                 if (th instanceof CompletionException && th.getCause() instanceof CancellationException) {
277                     // cancellation is a normal reason for failure, so no need to print it.
278                     return null;
279                 }
280                 logger.warn("Error initializing bluegiga", th);
281                 return null;
282             });
283
284         } else {
285             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR);
286         }
287     }
288
289     @Override
290     public void dispose() {
291         logger.info("Disposing BlueGiga");
292         stop();
293         stopScheduledTasks();
294         super.dispose();
295     }
296
297     private void stop() {
298         transactionManager.thenAccept(tman -> {
299             tman.removeEventListener(this);
300             tman.close();
301         });
302         serialHandler.thenAccept(sh -> {
303             sh.removeHandlerListener(this);
304             sh.close();
305         });
306         address = null;
307         initComplete = false;
308         connections.clear();
309
310         serialPortFuture.thenAccept(this::closeSerialPort);
311         serialPortFuture.cancel(false);
312     }
313
314     private void schedulePassiveScan() {
315         cancelScheduledPassiveScan();
316         passiveScanIdleTimer = executor.schedule(() -> {
317             if (!activeScanEnabled) {
318                 logger.debug("Activate passive scan");
319                 bgEndProcedure();
320                 bgStartScanning(false, configuration.passiveScanInterval, configuration.passiveScanWindow);
321             } else {
322                 logger.debug("Ignore passive scan activation as active scan is active");
323             }
324         }, configuration.passiveScanIdleTime, TimeUnit.MILLISECONDS);
325     }
326
327     private void cancelScheduledPassiveScan() {
328         if (passiveScanIdleTimer != null) {
329             passiveScanIdleTimer.cancel(true);
330         }
331     }
332
333     private void startScheduledTasks() {
334         schedulePassiveScan();
335         discoveryTask = scheduler.scheduleWithFixedDelay(this::refreshDiscoveredDevices, 0, 10, TimeUnit.SECONDS);
336     }
337
338     private void stopScheduledTasks() {
339         cancelScheduledPassiveScan();
340         if (removeInactiveDevicesTask != null) {
341             removeInactiveDevicesTask.cancel(true);
342             removeInactiveDevicesTask = null;
343         }
344         if (discoveryTask != null) {
345             discoveryTask.cancel(true);
346             discoveryTask = null;
347         }
348     }
349
350     private BlueGigaGetConnectionsResponse readMaxConnections() throws BlueGigaException {
351         return sendCommandWithoutChecks(new BlueGigaGetConnectionsCommand(), BlueGigaGetConnectionsResponse.class);
352     }
353
354     private BlueGigaAddressGetResponse readAddress() throws BlueGigaException {
355         return sendCommandWithoutChecks(new BlueGigaAddressGetCommand(), BlueGigaAddressGetResponse.class);
356     }
357
358     private BlueGigaGetInfoResponse readInfo() throws BlueGigaException {
359         return sendCommandWithoutChecks(new BlueGigaGetInfoCommand(), BlueGigaGetInfoResponse.class);
360     }
361
362     private void updateThingProperties() throws BlueGigaException {
363         BlueGigaGetInfoResponse infoResponse = readInfo();
364
365         Map<String, String> properties = editProperties();
366         properties.put(BluetoothBindingConstants.PROPERTY_MAXCONNECTIONS, Integer.toString(maxConnections));
367         properties.put(Thing.PROPERTY_FIRMWARE_VERSION,
368                 String.format("%d.%d", infoResponse.getMajor(), infoResponse.getMinor()));
369         properties.put(Thing.PROPERTY_HARDWARE_VERSION, Integer.toString(infoResponse.getHardware()));
370         properties.put(BlueGigaAdapterConstants.PROPERTY_PROTOCOL, Integer.toString(infoResponse.getProtocolVersion()));
371         properties.put(BlueGigaAdapterConstants.PROPERTY_LINKLAYER, Integer.toString(infoResponse.getLlVersion()));
372         updateProperties(properties);
373     }
374
375     private void closeSerialPort(SerialPort sp) {
376         sp.removeEventListener();
377         try {
378             sp.disableReceiveTimeout();
379         } catch (Exception e) {
380             // Ignore all as RXTX seems to send arbitrary exceptions when BlueGiga module is detached
381         } finally {
382             outputStream.ifPresent(output -> {
383                 IOUtils.closeQuietly(output);
384             });
385             inputStream.ifPresent(input -> {
386                 IOUtils.closeQuietly(input);
387             });
388             sp.close();
389             logger.debug("Closed serial port.");
390             inputStream = Optional.empty();
391             outputStream = Optional.empty();
392         }
393     }
394
395     @Override
396     public void scanStart() {
397         super.scanStart();
398         logger.debug("Start active scan");
399         // Stop the passive scan
400         cancelScheduledPassiveScan();
401         bgEndProcedure();
402
403         // Start a active scan
404         bgStartScanning(true, configuration.activeScanInterval, configuration.activeScanWindow);
405     }
406
407     @Override
408     public void scanStop() {
409         super.scanStop();
410         logger.debug("Stop active scan");
411
412         // Stop the active scan
413         bgEndProcedure();
414
415         // Start a passive scan after idle delay
416         schedulePassiveScan();
417     }
418
419     @Override
420     public @Nullable BluetoothAddress getAddress() {
421         BluetoothAddress addr = address;
422         if (addr != null) {
423             return addr;
424         } else {
425             throw new IllegalStateException("Adapter has not been initialized yet!");
426         }
427     }
428
429     @Override
430     protected BlueGigaBluetoothDevice createDevice(BluetoothAddress address) {
431         return new BlueGigaBluetoothDevice(this, address, BluetoothAddressType.UNKNOWN);
432     }
433
434     /**
435      * Connects to a device.
436      * <p>
437      * If the device is already connected, or the attempt to connect failed, then we return false. If we have reached
438      * the maximum number of connections supported by this dongle, then we return false.
439      *
440      * @param address the device {@link BluetoothAddress} to connect to
441      * @param addressType the {@link BluetoothAddressType} of the device
442      * @return true if the connection was started
443      */
444     public boolean bgConnect(BluetoothAddress address, BluetoothAddressType addressType) {
445         // Check the connection to make sure we're not already connected to this device
446         if (connections.containsValue(address)) {
447             return false;
448         }
449
450         // FIXME: When getting here, I always found all connections to be already taken and thus the code never
451         // proceeded. Relaxing this condition did not do any obvious harm, but now guaranteed that the services are
452         // queried from the device.
453         if (connections.size() == maxConnections + 1) {
454             logger.debug("BlueGiga: Attempt to connect to {} but no connections available.", address);
455             return false;
456         }
457
458         logger.debug("BlueGiga Connect: address {}.", address);
459
460         // @formatter:off
461         BlueGigaConnectDirectCommand command = new BlueGigaConnectDirectCommand.CommandBuilder()
462                 .withAddress(address.toString())
463                 .withAddrType(addressType)
464                 .withConnIntervalMin(configuration.connIntervalMin)
465                 .withConnIntervalMax(configuration.connIntervalMax)
466                 .withLatency(configuration.connLatency)
467                 .withTimeout(configuration.connTimeout)
468                 .build();
469         // @formatter:on
470         try {
471             return sendCommand(command, BlueGigaConnectDirectResponse.class, true).getResult() == BgApiResponse.SUCCESS;
472         } catch (BlueGigaException e) {
473             logger.debug("Error occured when sending connect command to device {}, reason: {}.", address,
474                     e.getMessage());
475             return false;
476         }
477     }
478
479     /**
480      * Close a connection using {@link BlueGigaDisconnectCommand}
481      *
482      * @param connectionHandle
483      * @return
484      */
485     public boolean bgDisconnect(int connectionHandle) {
486         logger.debug("BlueGiga Disconnect: connection {}", connectionHandle);
487         BlueGigaDisconnectCommand command = new BlueGigaDisconnectCommand.CommandBuilder()
488                 .withConnection(connectionHandle).build();
489
490         try {
491             return sendCommand(command, BlueGigaDisconnectResponse.class, true).getResult() == BgApiResponse.SUCCESS;
492         } catch (BlueGigaException e) {
493             logger.debug("Error occured when sending disconnect command to device {}, reason: {}.", address,
494                     e.getMessage());
495             return false;
496         }
497     }
498
499     /**
500      * Start a read of all primary services using {@link BlueGigaReadByGroupTypeCommand}
501      *
502      * @param connectionHandle
503      * @return true if successful
504      */
505     public boolean bgFindPrimaryServices(int connectionHandle) {
506         logger.debug("BlueGiga FindPrimary: connection {}", connectionHandle);
507         // @formatter:off
508         BlueGigaReadByGroupTypeCommand command = new BlueGigaReadByGroupTypeCommand.CommandBuilder()
509                 .withConnection(connectionHandle)
510                 .withStart(1)
511                 .withEnd(65535)
512                 .withUuid(UUID.fromString("00002800-0000-1000-8000-00805F9B34FB"))
513                 .build();
514         // @formatter:on
515         try {
516             return sendCommand(command, BlueGigaReadByGroupTypeResponse.class, true)
517                     .getResult() == BgApiResponse.SUCCESS;
518         } catch (BlueGigaException e) {
519             logger.debug("Error occured when sending read primary services command to device {}, reason: {}.", address,
520                     e.getMessage());
521             return false;
522         }
523     }
524
525     /**
526      * Start a read of all characteristics using {@link BlueGigaFindInformationCommand}
527      *
528      * @param connectionHandle
529      * @return true if successful
530      */
531     public boolean bgFindCharacteristics(int connectionHandle) {
532         logger.debug("BlueGiga Find: connection {}", connectionHandle);
533         // @formatter:off
534         BlueGigaFindInformationCommand command = new BlueGigaFindInformationCommand.CommandBuilder()
535                 .withConnection(connectionHandle)
536                 .withStart(1)
537                 .withEnd(65535)
538                 .build();
539         // @formatter:on
540         try {
541             return sendCommand(command, BlueGigaFindInformationResponse.class, true)
542                     .getResult() == BgApiResponse.SUCCESS;
543         } catch (BlueGigaException e) {
544             logger.debug("Error occured when sending read characteristics command to device {}, reason: {}.", address,
545                     e.getMessage());
546             return false;
547         }
548     }
549
550     public boolean bgReadCharacteristicDeclarations(int connectionHandle) {
551         logger.debug("BlueGiga Find: connection {}", connectionHandle);
552         // @formatter:off
553         BlueGigaReadByTypeCommand command = new BlueGigaReadByTypeCommand.CommandBuilder()
554                 .withConnection(connectionHandle)
555                 .withStart(1)
556                 .withEnd(65535)
557                 .withUUID(BluetoothBindingConstants.ATTR_CHARACTERISTIC_DECLARATION)
558                 .build();
559         // @formatter:on
560         try {
561             return sendCommand(command, BlueGigaReadByTypeResponse.class, true).getResult() == BgApiResponse.SUCCESS;
562         } catch (BlueGigaException e) {
563             logger.debug("Error occured when sending read characteristics command to device {}, reason: {}.", address,
564                     e.getMessage());
565             return false;
566         }
567     }
568
569     /**
570      * Read a characteristic using {@link BlueGigaReadByHandleCommand}
571      *
572      * @param connectionHandle
573      * @param handle
574      * @return true if successful
575      */
576     public boolean bgReadCharacteristic(int connectionHandle, int handle) {
577         logger.debug("BlueGiga Read: connection {}, handle {}", connectionHandle, handle);
578         // @formatter:off
579         BlueGigaReadByHandleCommand command = new BlueGigaReadByHandleCommand.CommandBuilder()
580                 .withConnection(connectionHandle)
581                 .withChrHandle(handle)
582                 .build();
583         // @formatter:on
584         try {
585             return sendCommand(command, BlueGigaReadByHandleResponse.class, true).getResult() == BgApiResponse.SUCCESS;
586         } catch (BlueGigaException e) {
587             logger.debug("Error occured when sending read characteristics command to device {}, reason: {}.", address,
588                     e.getMessage());
589             return false;
590         }
591     }
592
593     /**
594      * Write a characteristic using {@link BlueGigaAttributeWriteCommand}
595      *
596      * @param connectionHandle
597      * @param handle
598      * @param value
599      * @return true if successful
600      */
601     public boolean bgWriteCharacteristic(int connectionHandle, int handle, int[] value) {
602         logger.debug("BlueGiga Write: connection {}, handle {}", connectionHandle, handle);
603         // @formatter:off
604         BlueGigaAttributeWriteCommand command = new BlueGigaAttributeWriteCommand.CommandBuilder()
605                 .withConnection(connectionHandle)
606                 .withAttHandle(handle)
607                 .withData(value)
608                 .build();
609         // @formatter:on
610         try {
611             return sendCommand(command, BlueGigaAttributeWriteResponse.class, true)
612                     .getResult() == BgApiResponse.SUCCESS;
613         } catch (BlueGigaException e) {
614             logger.debug("Error occured when sending write characteristics command to device {}, reason: {}.", address,
615                     e.getMessage());
616             return false;
617         }
618     }
619
620     /*
621      * The following methods are private methods for handling the BlueGiga protocol
622      */
623     private boolean bgEndProcedure() {
624         try {
625             return sendCommandWithoutChecks(new BlueGigaEndProcedureCommand(), BlueGigaEndProcedureResponse.class)
626                     .getResult() == BgApiResponse.SUCCESS;
627         } catch (BlueGigaException e) {
628             logger.debug("Error occured when sending end procedure command.");
629             return false;
630         }
631     }
632
633     private boolean bgSetMode() {
634         try {
635             // @formatter:off
636             BlueGigaSetModeCommand command = new BlueGigaSetModeCommand.CommandBuilder()
637                     .withConnect(GapConnectableMode.GAP_NON_CONNECTABLE)
638                     .withDiscover(GapDiscoverableMode.GAP_NON_DISCOVERABLE)
639                     .build();
640             // @formatter:on
641             return sendCommandWithoutChecks(command, BlueGigaSetModeResponse.class)
642                     .getResult() == BgApiResponse.SUCCESS;
643         } catch (BlueGigaException e) {
644             logger.debug("Error occured when sending set mode command, reason: {}", e.getMessage());
645             return false;
646         }
647     }
648
649     /**
650      * Starts scanning on the dongle
651      *
652      * @param active true for active scanning
653      */
654     private boolean bgStartScanning(boolean active, int interval, int window) {
655         try {
656             // @formatter:off
657             BlueGigaSetScanParametersCommand scanCommand = new BlueGigaSetScanParametersCommand.CommandBuilder()
658                     .withActiveScanning(active)
659                     .withScanInterval(interval)
660                     .withScanWindow(window)
661                     .build();
662             // @formatter:on
663             if (sendCommand(scanCommand, BlueGigaSetScanParametersResponse.class, false)
664                     .getResult() == BgApiResponse.SUCCESS) {
665                 BlueGigaDiscoverCommand discoverCommand = new BlueGigaDiscoverCommand.CommandBuilder()
666                         .withMode(GapDiscoverMode.GAP_DISCOVER_OBSERVATION).build();
667                 if (sendCommand(discoverCommand, BlueGigaDiscoverResponse.class, false)
668                         .getResult() == BgApiResponse.SUCCESS) {
669                     logger.debug("{} scanning succesfully started.", active ? "Active" : "Passive");
670                     return true;
671                 }
672             }
673         } catch (BlueGigaException e) {
674             logger.debug("Error occured when sending start scan command, reason: {}", e.getMessage());
675         }
676         logger.debug("Scan start failed.");
677         return false;
678     }
679
680     /**
681      * Send command only if initialization phase is successfully done
682      */
683     private <T extends BlueGigaResponse> T sendCommand(BlueGigaCommand command, Class<T> expectedResponse,
684             boolean schedulePassiveScan) throws BlueGigaException {
685         if (!initComplete) {
686             throw new BlueGigaException("BlueGiga not initialized");
687         }
688
689         if (schedulePassiveScan) {
690             cancelScheduledPassiveScan();
691         }
692         try {
693             return sendCommandWithoutChecks(command, expectedResponse);
694         } finally {
695             if (schedulePassiveScan) {
696                 schedulePassiveScan();
697             }
698         }
699     }
700
701     /**
702      * Forcefully send command without any checks
703      */
704     private <T extends BlueGigaResponse> T sendCommandWithoutChecks(BlueGigaCommand command, Class<T> expectedResponse)
705             throws BlueGigaException {
706         BlueGigaTransactionManager manager = transactionManager.getNow(null);
707         if (manager != null) {
708             return manager.sendTransaction(command, expectedResponse, COMMAND_TIMEOUT_MS);
709         } else {
710             throw new BlueGigaException("Transaction manager missing");
711         }
712     }
713
714     /**
715      * Add an event listener for the BlueGiga events
716      *
717      * @param listener the {@link BlueGigaEventListener} to add
718      */
719     public void addEventListener(BlueGigaEventListener listener) {
720         transactionManager.thenAccept(manager -> {
721             manager.addEventListener(listener);
722         });
723     }
724
725     /**
726      * Remove an event listener for the BlueGiga events
727      *
728      * @param listener the {@link BlueGigaEventListener} to remove
729      */
730     public void removeEventListener(BlueGigaEventListener listener) {
731         transactionManager.thenAccept(manager -> {
732             manager.removeEventListener(listener);
733         });
734     }
735
736     @Override
737     public void bluegigaEventReceived(@Nullable BlueGigaResponse event) {
738         if (event instanceof BlueGigaScanResponseEvent) {
739             if (initComplete) {
740                 BlueGigaScanResponseEvent scanEvent = (BlueGigaScanResponseEvent) event;
741
742                 // We use the scan event to add any devices we hear to the devices list
743                 // The device gets created, and then manages itself for discovery etc.
744                 BluetoothAddress sender = new BluetoothAddress(scanEvent.getSender());
745                 BlueGigaBluetoothDevice device = getDevice(sender);
746                 device.setAddressType(scanEvent.getAddressType());
747                 deviceDiscovered(device);
748             } else {
749                 logger.trace("Ignore BlueGigaScanResponseEvent as initialization is not complete");
750             }
751             return;
752         }
753
754         if (event instanceof BlueGigaConnectionStatusEvent) {
755             BlueGigaConnectionStatusEvent connectionEvent = (BlueGigaConnectionStatusEvent) event;
756             connections.put(connectionEvent.getConnection(), new BluetoothAddress(connectionEvent.getAddress()));
757         }
758
759         if (event instanceof BlueGigaDisconnectedEvent) {
760             BlueGigaDisconnectedEvent disconnectedEvent = (BlueGigaDisconnectedEvent) event;
761             connections.remove(disconnectedEvent.getConnection());
762         }
763     }
764
765     @Override
766     public void bluegigaClosed(Exception reason) {
767         logger.debug("BlueGiga connection closed, request reinitialization");
768         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason.getMessage());
769         initComplete = false;
770     }
771 }