]> git.basschouten.com Git - openhab-addons.git/blob
d7f24cd447c893f741353dc55761e8ec17d51e5a
[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;
14
15 import java.util.Map;
16 import java.util.concurrent.ScheduledExecutorService;
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
19
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.bluetooth.BaseBluetoothDevice;
23 import org.openhab.binding.bluetooth.BluetoothAddress;
24 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
25 import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
26 import org.openhab.binding.bluetooth.BluetoothDescriptor;
27 import org.openhab.binding.bluetooth.BluetoothDevice;
28 import org.openhab.binding.bluetooth.BluetoothService;
29 import org.openhab.binding.bluetooth.bluegiga.handler.BlueGigaBridgeHandler;
30 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaEventListener;
31 import org.openhab.binding.bluetooth.bluegiga.internal.BlueGigaResponse;
32 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaAttributeValueEvent;
33 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaFindInformationFoundEvent;
34 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaGroupFoundEvent;
35 import org.openhab.binding.bluetooth.bluegiga.internal.command.attributeclient.BlueGigaProcedureCompletedEvent;
36 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaConnectionStatusEvent;
37 import org.openhab.binding.bluetooth.bluegiga.internal.command.connection.BlueGigaDisconnectedEvent;
38 import org.openhab.binding.bluetooth.bluegiga.internal.command.gap.BlueGigaScanResponseEvent;
39 import org.openhab.binding.bluetooth.bluegiga.internal.eir.EirDataType;
40 import org.openhab.binding.bluetooth.bluegiga.internal.eir.EirPacket;
41 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BgApiResponse;
42 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.BluetoothAddressType;
43 import org.openhab.binding.bluetooth.bluegiga.internal.enumeration.ConnectionStatusFlag;
44 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
45 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
46 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification.BluetoothBeaconType;
47 import org.openhab.core.common.ThreadPoolManager;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 /**
52  * An extended {@link BluetoothDevice} class to handle BlueGiga specific information
53  *
54  * @author Chris Jackson - Initial contribution
55  */
56 @NonNullByDefault
57 public class BlueGigaBluetoothDevice extends BaseBluetoothDevice implements BlueGigaEventListener {
58     private final long TIMEOUT_SEC = 60;
59
60     private final Logger logger = LoggerFactory.getLogger(BlueGigaBluetoothDevice.class);
61
62     // BlueGiga needs to know the address type when connecting
63     private BluetoothAddressType addressType = BluetoothAddressType.UNKNOWN;
64
65     // The dongle handler
66     private final BlueGigaBridgeHandler bgHandler;
67
68     // An enum to use in the state machine for interacting with the device
69     private enum BlueGigaProcedure {
70         NONE,
71         GET_SERVICES,
72         GET_CHARACTERISTICS,
73         CHARACTERISTIC_READ,
74         CHARACTERISTIC_WRITE
75     }
76
77     private BlueGigaProcedure procedureProgress = BlueGigaProcedure.NONE;
78
79     // Somewhere to remember what characteristic we're working on
80     private @Nullable BluetoothCharacteristic procedureCharacteristic;
81
82     // The connection handle if the device is connected
83     private int connection = -1;
84
85     private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool("bluetooth");
86
87     private @Nullable ScheduledFuture<?> connectTimer;
88     private @Nullable ScheduledFuture<?> procedureTimer;
89
90     private Runnable connectTimeoutTask = new Runnable() {
91         @Override
92         public void run() {
93             if (connectionState == ConnectionState.CONNECTING) {
94                 logger.debug("Connection timeout for device {}", address);
95                 connectionState = ConnectionState.DISCONNECTED;
96             }
97         }
98     };
99
100     private Runnable procedureTimeoutTask = new Runnable() {
101         @Override
102         public void run() {
103             logger.debug("Procedure {} timeout for device {}", procedureProgress, address);
104             procedureProgress = BlueGigaProcedure.NONE;
105             procedureCharacteristic = null;
106         }
107     };
108
109     /**
110      * Creates a new {@link BlueGigaBluetoothDevice} which extends {@link BluetoothDevice} for the BlueGiga
111      * implementation
112      *
113      * @param bgHandler the {@link BlueGigaBridgeHandler} that provides the link to the dongle
114      * @param address the {@link BluetoothAddress} for this device
115      * @param addressType the {@link BluetoothAddressType} of this device
116      */
117     public BlueGigaBluetoothDevice(BlueGigaBridgeHandler bgHandler, BluetoothAddress address,
118             BluetoothAddressType addressType) {
119         super(bgHandler, address);
120
121         logger.debug("Creating new BlueGiga device {}", address);
122
123         this.bgHandler = bgHandler;
124         this.addressType = addressType;
125
126         bgHandler.addEventListener(this);
127         updateLastSeenTime();
128     }
129
130     public void setAddressType(BluetoothAddressType addressType) {
131         this.addressType = addressType;
132     }
133
134     @Override
135     public boolean connect() {
136         if (connection != -1) {
137             // We're already connected
138             return false;
139         }
140
141         cancelTimer(connectTimer);
142         if (bgHandler.bgConnect(address, addressType)) {
143             connectionState = ConnectionState.CONNECTING;
144             connectTimer = startTimer(connectTimeoutTask, TIMEOUT_SEC);
145             return true;
146         } else {
147             connectionState = ConnectionState.DISCONNECTED;
148             return false;
149         }
150     }
151
152     @Override
153     public boolean disconnect() {
154         if (connection == -1) {
155             // We're already disconnected
156             return false;
157         }
158
159         return bgHandler.bgDisconnect(connection);
160     }
161
162     @Override
163     public boolean discoverServices() {
164         if (procedureProgress != BlueGigaProcedure.NONE) {
165             return false;
166         }
167
168         cancelTimer(procedureTimer);
169         if (!bgHandler.bgFindPrimaryServices(connection)) {
170             return false;
171         }
172
173         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
174         procedureProgress = BlueGigaProcedure.GET_SERVICES;
175         return true;
176     }
177
178     @Override
179     public boolean enableNotifications(BluetoothCharacteristic characteristic) {
180         // TODO will be implemented in a followup PR
181         return false;
182     }
183
184     @Override
185     public boolean disableNotifications(BluetoothCharacteristic characteristic) {
186         // TODO will be implemented in a followup PR
187         return false;
188     }
189
190     @Override
191     public boolean enableNotifications(BluetoothDescriptor descriptor) {
192         // TODO will be implemented in a followup PR
193         return false;
194     }
195
196     @Override
197     public boolean disableNotifications(BluetoothDescriptor descriptor) {
198         // TODO will be implemented in a followup PR
199         return false;
200     }
201
202     @Override
203     public boolean readCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
204         if (characteristic == null || characteristic.getHandle() == 0) {
205             return false;
206         }
207
208         if (procedureProgress != BlueGigaProcedure.NONE) {
209             return false;
210         }
211
212         cancelTimer(procedureTimer);
213         if (!bgHandler.bgReadCharacteristic(connection, characteristic.getHandle())) {
214             return false;
215         }
216         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
217         procedureProgress = BlueGigaProcedure.CHARACTERISTIC_READ;
218         procedureCharacteristic = characteristic;
219
220         return true;
221     }
222
223     @Override
224     public boolean writeCharacteristic(@Nullable BluetoothCharacteristic characteristic) {
225         if (characteristic == null || characteristic.getHandle() == 0) {
226             return false;
227         }
228
229         if (procedureProgress != BlueGigaProcedure.NONE) {
230             return false;
231         }
232
233         cancelTimer(procedureTimer);
234         if (!bgHandler.bgWriteCharacteristic(connection, characteristic.getHandle(), characteristic.getValue())) {
235             return false;
236         }
237
238         procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
239         procedureProgress = BlueGigaProcedure.CHARACTERISTIC_WRITE;
240         procedureCharacteristic = characteristic;
241
242         return true;
243     }
244
245     @Override
246     public void bluegigaEventReceived(BlueGigaResponse event) {
247         if (event instanceof BlueGigaScanResponseEvent) {
248             handleScanEvent((BlueGigaScanResponseEvent) event);
249         }
250
251         else if (event instanceof BlueGigaGroupFoundEvent) {
252             handleGroupFoundEvent((BlueGigaGroupFoundEvent) event);
253         }
254
255         else if (event instanceof BlueGigaFindInformationFoundEvent) {
256             // A Characteristic has been discovered
257             handleFindInformationFoundEvent((BlueGigaFindInformationFoundEvent) event);
258         }
259
260         else if (event instanceof BlueGigaProcedureCompletedEvent) {
261             handleProcedureCompletedEvent((BlueGigaProcedureCompletedEvent) event);
262         }
263
264         else if (event instanceof BlueGigaConnectionStatusEvent) {
265             handleConnectionStatusEvent((BlueGigaConnectionStatusEvent) event);
266         }
267
268         else if (event instanceof BlueGigaDisconnectedEvent) {
269             handleDisconnectedEvent((BlueGigaDisconnectedEvent) event);
270         }
271
272         else if (event instanceof BlueGigaAttributeValueEvent) {
273             handleAttributeValueEvent((BlueGigaAttributeValueEvent) event);
274         }
275     }
276
277     private void handleScanEvent(BlueGigaScanResponseEvent event) {
278         // Check if this is addressed to this device
279         if (!address.equals(new BluetoothAddress(event.getSender()))) {
280             return;
281         }
282
283         logger.trace("scanEvent: {}", event);
284         updateLastSeenTime();
285
286         // Set device properties
287         rssi = event.getRssi();
288         addressType = event.getAddressType();
289
290         byte[] manufacturerData = null;
291
292         // If the packet contains data, then process it and add anything relevant to the device...
293         if (event.getData().length > 0) {
294             EirPacket eir = new EirPacket(event.getData());
295             for (EirDataType record : eir.getRecords().keySet()) {
296                 if (logger.isTraceEnabled()) {
297                     logger.trace("  EirDataType: {}={}", record, eir.getRecord(record));
298                 }
299                 Object obj;
300                 switch (record) {
301                     case EIR_FLAGS:
302                         break;
303                     case EIR_MANUFACTURER_SPECIFIC:
304                         obj = eir.getRecord(EirDataType.EIR_MANUFACTURER_SPECIFIC);
305                         if (obj != null) {
306                             try {
307                                 @SuppressWarnings("unchecked")
308                                 Map<Short, int[]> eirRecord = (Map<Short, int[]>) obj;
309                                 Map.Entry<Short, int[]> eirEntry = eirRecord.entrySet().iterator().next();
310
311                                 manufacturer = eirEntry.getKey().intValue();
312
313                                 int[] manufacturerInt = eirEntry.getValue();
314                                 manufacturerData = new byte[manufacturerInt.length + 2];
315                                 // Convert short Company ID to bytes and add it to manufacturerData
316                                 manufacturerData[0] = (byte) (manufacturer & 0xff);
317                                 manufacturerData[1] = (byte) ((manufacturer >> 8) & 0xff);
318                                 // Add Convert int custom data nd add it to manufacturerData
319                                 for (int i = 0; i < manufacturerInt.length; i++) {
320                                     manufacturerData[i + 2] = (byte) manufacturerInt[i];
321                                 }
322                             } catch (ClassCastException e) {
323                                 logger.debug("Unsupported manufacturer specific record received from device {}",
324                                         address);
325                             }
326                         }
327                         break;
328                     case EIR_NAME_LONG:
329                     case EIR_NAME_SHORT:
330                         name = (String) eir.getRecord(record);
331                         break;
332                     case EIR_SLAVEINTERVALRANGE:
333                         break;
334                     case EIR_SVC_DATA_UUID128:
335                         break;
336                     case EIR_SVC_DATA_UUID16:
337                         break;
338                     case EIR_SVC_DATA_UUID32:
339                         break;
340                     case EIR_SVC_UUID128_INCOMPLETE:
341                     case EIR_SVC_UUID16_COMPLETE:
342                     case EIR_SVC_UUID16_INCOMPLETE:
343                     case EIR_SVC_UUID32_COMPLETE:
344                     case EIR_SVC_UUID32_INCOMPLETE:
345                     case EIR_SVC_UUID128_COMPLETE:
346                         // addServices((List<UUID>) eir.getRecord(record));
347                         break;
348                     case EIR_TXPOWER:
349                         obj = eir.getRecord(EirDataType.EIR_TXPOWER);
350                         if (obj != null) {
351                             txPower = (int) obj;
352                         }
353                         break;
354                     default:
355                         break;
356                 }
357             }
358         }
359
360         if (connectionState == ConnectionState.DISCOVERING) {
361             // TODO: It could make sense to wait with discovery for non-connectable devices until scan response is
362             // received to eventually retrieve more about the device before it gets discovered. Anyhow, devices
363             // that don't send a scan response at all also have to be supported. See also PR #6995.
364
365             // Set our state to disconnected
366             connectionState = ConnectionState.DISCONNECTED;
367             connection = -1;
368
369             // But notify listeners that the state is now DISCOVERED
370             notifyListeners(BluetoothEventType.CONNECTION_STATE,
371                     new BluetoothConnectionStatusNotification(ConnectionState.DISCOVERED));
372
373             // Notify the bridge - for inbox notifications
374             bgHandler.deviceDiscovered(this);
375         }
376
377         // Notify listeners of all scan records - for RSSI, beacon processing (etc)
378         BluetoothScanNotification scanNotification = new BluetoothScanNotification();
379         scanNotification.setRssi(event.getRssi());
380
381         switch (event.getPacketType()) {
382             case CONNECTABLE_ADVERTISEMENT:
383             case DISCOVERABLE_ADVERTISEMENT:
384             case NON_CONNECTABLE_ADVERTISEMENT:
385                 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_ADVERTISEMENT);
386                 break;
387             case SCAN_RESPONSE:
388                 scanNotification.setBeaconType(BluetoothBeaconType.BEACON_SCANRESPONSE);
389                 break;
390             default:
391                 break;
392         }
393
394         if (manufacturerData != null) {
395             scanNotification.setManufacturerData(manufacturerData);
396         }
397
398         notifyListeners(BluetoothEventType.SCAN_RECORD, scanNotification);
399     }
400
401     private void handleGroupFoundEvent(BlueGigaGroupFoundEvent event) {
402         // If this is not our connection handle then ignore.
403         if (connection != event.getConnection()) {
404             return;
405         }
406
407         logger.trace("BlueGiga Group: {} svcs={}", this, supportedServices);
408         updateLastSeenTime();
409
410         BluetoothService service = new BluetoothService(event.getUuid(), true, event.getStart(), event.getEnd());
411         addService(service);
412     }
413
414     private void handleFindInformationFoundEvent(BlueGigaFindInformationFoundEvent event) {
415         // If this is not our connection handle then ignore.
416         if (connection != event.getConnection()) {
417             return;
418         }
419
420         logger.trace("BlueGiga FindInfo: {} svcs={}", this, supportedServices);
421         updateLastSeenTime();
422
423         BluetoothCharacteristic characteristic = new BluetoothCharacteristic(event.getUuid(), event.getChrHandle());
424
425         BluetoothService service = getServiceByHandle(characteristic.getHandle());
426         if (service == null) {
427             logger.debug("BlueGiga: Unable to find service for handle {}", characteristic.getHandle());
428             return;
429         }
430         characteristic.setService(service);
431         service.addCharacteristic(characteristic);
432     }
433
434     private void handleProcedureCompletedEvent(BlueGigaProcedureCompletedEvent event) {
435         // If this is not our connection handle then ignore.
436         if (connection != event.getConnection()) {
437             return;
438         }
439
440         if (procedureProgress == BlueGigaProcedure.NONE) {
441             logger.debug("BlueGiga procedure completed but procedure is null with connection {}, address {}",
442                     connection, address);
443             return;
444         }
445
446         cancelTimer(procedureTimer);
447         updateLastSeenTime();
448
449         // The current procedure is now complete - move on...
450         switch (procedureProgress) {
451             case GET_SERVICES:
452                 // We've downloaded all services, now get the characteristics
453                 if (bgHandler.bgFindCharacteristics(connection)) {
454                     procedureTimer = startTimer(procedureTimeoutTask, TIMEOUT_SEC);
455                     procedureProgress = BlueGigaProcedure.GET_CHARACTERISTICS;
456                 } else {
457                     procedureProgress = BlueGigaProcedure.NONE;
458                 }
459                 break;
460             case GET_CHARACTERISTICS:
461                 // We've downloaded all characteristics
462                 procedureProgress = BlueGigaProcedure.NONE;
463                 notifyListeners(BluetoothEventType.SERVICES_DISCOVERED);
464                 break;
465             case CHARACTERISTIC_READ:
466                 // The read failed
467                 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, procedureCharacteristic,
468                         BluetoothCompletionStatus.ERROR);
469                 procedureProgress = BlueGigaProcedure.NONE;
470                 procedureCharacteristic = null;
471                 break;
472             case CHARACTERISTIC_WRITE:
473                 // The write completed - failure or success
474                 BluetoothCompletionStatus result = event.getResult() == BgApiResponse.SUCCESS
475                         ? BluetoothCompletionStatus.SUCCESS
476                         : BluetoothCompletionStatus.ERROR;
477                 notifyListeners(BluetoothEventType.CHARACTERISTIC_WRITE_COMPLETE, procedureCharacteristic, result);
478                 procedureProgress = BlueGigaProcedure.NONE;
479                 procedureCharacteristic = null;
480                 break;
481             default:
482                 break;
483         }
484     }
485
486     private void handleConnectionStatusEvent(BlueGigaConnectionStatusEvent event) {
487         // Check if this is addressed to this device
488         if (!address.equals(new BluetoothAddress(event.getAddress()))) {
489             return;
490         }
491
492         cancelTimer(connectTimer);
493         updateLastSeenTime();
494
495         // If we're connected, then remember the connection handle
496         if (event.getFlags().contains(ConnectionStatusFlag.CONNECTION_CONNECTED)) {
497             connectionState = ConnectionState.CONNECTED;
498             connection = event.getConnection();
499             notifyListeners(BluetoothEventType.CONNECTION_STATE,
500                     new BluetoothConnectionStatusNotification(connectionState));
501         }
502     }
503
504     private void handleDisconnectedEvent(BlueGigaDisconnectedEvent event) {
505         // If this is not our connection handle then ignore.
506         if (connection != event.getConnection()) {
507             return;
508         }
509
510         cancelTimer(procedureTimer);
511         connectionState = ConnectionState.DISCONNECTED;
512         connection = -1;
513         procedureProgress = BlueGigaProcedure.NONE;
514
515         notifyListeners(BluetoothEventType.CONNECTION_STATE,
516                 new BluetoothConnectionStatusNotification(connectionState));
517     }
518
519     private void handleAttributeValueEvent(BlueGigaAttributeValueEvent event) {
520         // If this is not our connection handle then ignore.
521         if (connection != event.getConnection()) {
522             return;
523         }
524
525         updateLastSeenTime();
526
527         BluetoothCharacteristic characteristic = getCharacteristicByHandle(event.getAttHandle());
528         if (characteristic == null) {
529             logger.debug("BlueGiga didn't find characteristic for event {}", event);
530         } else {
531             characteristic.setValue(event.getValue().clone());
532
533             // If this is the characteristic we were reading, then send a read completion
534             if (procedureProgress == BlueGigaProcedure.CHARACTERISTIC_READ && procedureCharacteristic != null
535                     && procedureCharacteristic.getHandle() == event.getAttHandle()) {
536                 procedureProgress = BlueGigaProcedure.NONE;
537                 procedureCharacteristic = null;
538                 notifyListeners(BluetoothEventType.CHARACTERISTIC_READ_COMPLETE, characteristic,
539                         BluetoothCompletionStatus.SUCCESS);
540             }
541
542             // Notify the user of the updated value
543             notifyListeners(BluetoothEventType.CHARACTERISTIC_UPDATED, characteristic);
544         }
545     }
546
547     /**
548      * Clean up and release memory.
549      */
550     @Override
551     public void dispose() {
552         if (connectionState == ConnectionState.CONNECTED) {
553             disconnect();
554         }
555         cancelTimer(connectTimer);
556         cancelTimer(procedureTimer);
557         bgHandler.removeEventListener(this);
558         procedureProgress = BlueGigaProcedure.NONE;
559         connectionState = ConnectionState.DISCOVERING;
560         connection = -1;
561     }
562
563     private void cancelTimer(@Nullable ScheduledFuture<?> task) {
564         if (task != null) {
565             task.cancel(true);
566         }
567     }
568
569     private ScheduledFuture<?> startTimer(Runnable command, long timeout) {
570         return scheduler.schedule(command, timeout, TimeUnit.SECONDS);
571     }
572 }