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