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