Android Bluetooth Low Energy (BLE): cocinar bien, parte 1

En el último año he estado desarrollando aplicaciones Bluetooth Low Energy ( BLE ) para iOS y resultó ser bastante simple. Luego hubo que migrarlos a Android ... ¿qué tan difícil podría ser?





Puedo decirlo con seguridad: fue más difícil de lo que imaginaba, tuve que esforzarme mucho para lograr un funcionamiento estable con Android. Estudié muchos artículos de dominio público, algunos resultaron ser erróneos, muchos fueron muy útiles y me ayudaron en el asunto. En esta serie de artículos, quiero describir mis hallazgos para que no pierda mucho tiempo buscando como lo hice yo.





Características de BLE para Android

  • La documentación de Google sobre BLE es muy general , en algunos casos falta información importante o está desactualizada, las aplicaciones de muestra no muestran cómo usar BLE correctamente. Encontré solo algunas fuentes sobre cómo hacer BLE correctamente. La presentación de Stuart Kent  proporciona un excelente material de partida. Para algunos temas avanzados, hay un buen artículo  nórdico .





  • Android BLE API es una operación de bajo nivel , en aplicaciones reales necesita usar varias capas de abstracción (como, por ejemplo, se hace en iOS-CoreBluetooth). Por lo general, debe hacerlo usted mismo: cola de comandos, vinculación, mantenimiento de la conexión, manejo de errores y errores, acceso multiproceso. Las bibliotecas más famosas son  SweetBlueRxAndroidBle  y  Nordic . En mi opinión, el más fácil de aprender es el nórdico,  ver detalles aquí .





  • Los fabricantes realizan cambios en la pila BLE de Android  o los reemplazan por completo con su propia implementación. Y debemos tener en cuenta la diferencia de comportamiento para diferentes dispositivos en la aplicación. ¡Lo que funciona bien en un teléfono puede no funcionar en otros! En general, no todo es tan malo, por ejemplo, ¡la implementación de Samsung es mejor que la propia implementación de Google!





  • Android tiene varios errores conocidos (y desconocidos)  que deben solucionarse, especialmente en las versiones 4.5 y 6. Las versiones posteriores funcionan mucho mejor, pero también tienen ciertos problemas, como fallas de conexión aleatorias con el error 133 . Más sobre esto a continuación.





No pretendo haber resuelto todos los problemas, pero logré llegar a un nivel "aceptable". Comencemos con el escaneo.





Escanear dispositivos

.  BluetoothLeScanner



:





BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();

if (scanner != null) {
    scanner.startScan(filters, scanSettings, scanCallback);
    Log.d(TAG, "scan started");
}  else {
    Log.e(TAG, "could not get scanner object");
}
      
      



  filters



  scanSettings



,  scanCallback



:





private final ScanCallback scanCallback = new ScanCallback() {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        BluetoothDevice device = result.getDevice();
        // ...do whatever you want with this found device
    }

    @Override
    public void onBatchScanResults(List<ScanResult> results) {
        // Ignore for now
    }

    @Override
    public void onScanFailed(int errorCode) {
        // Ignore for now
    }
};
      
      



 ScanResult



 ,  BluetoothDevice



, . , , ScanResult



  :





  • Advertisement data - , UUID ,  filters



      UUID .





  • RSSI  - ( ).





  • … , .  ScanResult



     .





 Activity



onScanResult



  ,  Activity



  ,  onScanResult



.





null , , UUID .





UUID

, UUID: 1810.  Advertisement data UUID , . , ,  Advertisement data , .





. : , UUID  Advertisement data, .





:





UUID BLP_SERVICE_UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb");
UUID[] serviceUUIDs = new UUID[]{BLP_SERVICE_UUID};
List<ScanFilter> filters = null;
if(serviceUUIDs != null) {
    filters = new ArrayList<>();
    for (UUID serviceUUID : serviceUUIDs) {
        ScanFilter filter = new ScanFilter.Builder()
                .setServiceUuid(new ParcelUuid(serviceUUID))
                .build();
        filters.add(filter);
    }
}
scanner.startScan(filters, scanSettings, scanCallback);
      
      



UUID ( 1810



),  16-bit UUID



   128-bit UUID



 (  00001810-000000-1000-8000-000-00805f9b34fb



). UUID BASE_PART UUID, .  .





, :









  • , , Polar H7 «Polar H7 391BBB014», - «Polar H7» , «391BBB014» - . . «Polar H7», ,  ScanResult



    .    :





String[] names = new String[]{"Polar H7 391BB014"};
List<ScanFilter> filters = null;
if(names != null) {
    filters = new ArrayList<>();
    for (String name : names) {
        ScanFilter filter = new ScanFilter.Builder()
                .setDeviceName(name)
                .build();
        filters.add(filter);
    }
}
scanner.startScan(filters, scanSettings, scanCallback);
      
      



MAC-.

   . MAC- , , , . , , Bluetooth.





String[] peripheralAddresses = new String[]{"01:0A:5C:7D:D0:1A"};
// Build filters list
List<ScanFilter> filters = null;
if (peripheralAddresses != null) {
    filters = new ArrayList<>();
    for (String address : peripheralAddresses) {
        ScanFilter filter = new ScanFilter.Builder()
                .setDeviceAddress(address)
                .build();
        filters.add(filter);
    }
}
scanner.startScan(filters, scanSettings, scanByServiceUUIDCallback);
      
      



, UUID, MAC- . , . .





ScanSettings

ScanSettings



Android . , , :





ScanSettings scanSettings = new ScanSettings.Builder()
        .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
        .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
        .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
        .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
        .setReportDelay(0L)
        .build();
      
      



ScanMode

, . Bluetooth . , . 4 ,  Nordics :





  1. SCAN_MODE_LOW_POWER



    . Android 0.5, 4.5. , advertisement .





  2. SCAN_MODE_BALANCED



    . : 2, : 3, «» .





  3. SCAN_MODE_LOW_LATENCY



    . , Android , , . . .





  4. SCAN_MODE_OPPORTUNISTIC



    . , ! , , . Android , (. « »).





Callback Type

callback  ScanResult



  , 3 :





  1. CALLBACK_TYPE_ALL_MATCHES



    . Callback , advertisement . - 200-500 allback, advertisement .





  2. CALLBACK_TYPE_FIRST_MATCH



    . Callback , advertisement .





  3. CALLBACK_TYPE_MATCH_LOST



    . Callback , advertisement advertisement . .





 CALLBACK_TYPE_ALL_MATCHES



  CALLBACK_TYPE_FIRST_MATCH



. . -  CALLBACK_TYPE_ALL_MATCHES



, callback, -  CALLBACK_TYPE_FIRST_MATCH



.





Match mode

, Android «».





  1. MATCH_MODE_AGGRESSIVE



    . advertisement .





  2. MATCH_MODE_STICKY



    . , advertisement .





,  MATCH_MODE_AGGRESSIVE



, .





Number of matches

advertisement .





  1. MATCH_NUM_ONE_ADVERTISEMENT



    . .





  2. MATCH_NUM_FEW_ADVERTISEMENT



    . .





  3. MATCH_NUM_MAX_ADVERTISEMENT



    . advertisement , .





. - , 2 .





Report delay

allback . , Android  onBatchScanResults



.  onScanResult



  . , . - , MAC- ( ).





:     Samsung S6 / Samsung S6 Edge, RSSI ( ) .





Android Bluetooth

BLE «» Bluetooth . : , MAC-, (, ), (Classic, Dual, BLE) .. Android , . , . . , Android , . - MAC- !





Bluetooth , , , 3 , :





  1. Bluetooth









  2. ( )





, , - . , Samsung, Bluetooth.





, BT . , :





// Get device object for a mac address
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(peripheralAddress)
// Check if the peripheral is cached or not
int deviceType = device.getType();
if(deviceType == BluetoothDevice.DEVICE_TYPE_UNKNOWN) {
    // The peripheral is not cached
} else {
    // The peripheral is cached
}
      
      



, , . .





?

– , , , . , BLE-, , (foreground), .





, Google () :





  • c Android 8.1  .  ScanFilters



    , Android , , .  Google.  Google.





  • c Android 7 30 , Android  SCAN_MODE_OPPORTUNISTIC



    .  , ,  30 .  commit  .





  • Android 7 5 30   .





Google . ! Android , 10 , . :





  •  StackOverflow





  •  David Young





(permissions)

, . (permissions):





<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
      
      



, , .  ACCESS_COARSE_LOCATION



 Google «» .





private boolean hasPermissions() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (getApplicationContext().checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[] { Manifest.permission.ACCESS_COARSE_LOCATION }, ACCESS_COARSE_LOCATION_REQUEST);
            return false;
        }
    }
    return true;
}
      
      



. , BLE 2 : ACCESS_FINE_LOCATION



 ( API<23)  ACCESS_BACKGROUND_LOCATION



  Stackoverflow.





Android10:





<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
      
      



, Bluetooth, -  Intent



  :





BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (!bluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
      
      







BLE Activity (Fragment / Service), , (permissions) Android-Bluetooth . .





!








All Articles