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