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