BLE - Sizuha/devdog GitHub Wiki

Bluetooth Low Energy

Peripheral / Central

  • Peripheral: サービスを提供する側。基本的にここからデータを発信する。
    • Characteristic: Peripheralの中のサービス区分。実際、これと送・受信することになる。
  • Central: Peripheralをスキャンする側。複数のPeripheralと接続可能。

Advertising

Peripheralが発信する信号、Centralがこの信号を拾う

Advertise Data

アドバタイズデータの例

No. Field Bytes 備考
1 Length 1
AD Type 1 0x01
Flags 1 必須
2 Length 1
AD Type 1 0x21
Service Data UUID 16 自由に設定
Service Data 10 自由に設定
合計 31
  • アドバタイズデータは31Bytes内にすること
  • 各FieldにはLengthAD Type情報が必要
  • UUIDの場合、16-bit/32-bit/128-bitの3種類がある
    • UUIDの長さはAD Typeによって決められる
    • 16-bit/32-bitの短縮型UUIDはBluetooth標準で決められたものを入れる
  • AD Type/UUIDなどの参考資料

スキャン応答データの例

No. Field Bytes 備考
1 Length 1
AD Type 1 0x21
Service Data UUID 16 自由に設定
Service Data 13 自由に設定
合計 31

16-bit/32-bit UUID

Bluetooth用のUUIDの短縮型

  • 16-bit: 0000XXXX-0000-1000-8000-00805f9b34fb
  • 32-bit: AAAABBBB-0000-1000-8000-00805f9b34fb

Peripheral側の実装

Android

セットアップ

mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = mBluetoothManager.getAdapter();
setupGattServer();

private void setupGattServer() {
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
        return;
    }
    if (mBluetoothGattServer != null) {
        mBluetoothGattServer.close();
    }
    mBluetoothGattServer = mBluetoothManager.openGattServer(this, gattServerCallback);

    BluetoothGattService service = new BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY);

    // Characteristicの登録
    BluetoothGattCharacteristic characteristic = new BluetoothGattCharacteristic(
            CHARACTERISTIC_UUID,
            BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_WRITE,
            BluetoothGattCharacteristic.PERMISSION_READ | BluetoothGattCharacteristic.PERMISSION_WRITE
    );
    service.addCharacteristic(characteristic);
    mBluetoothGattServer.addService(service);
}

private final BluetoothGattServerCallback gattServerCallback = new BluetoothGattServerCallback() {
    @SuppressLint("MissingPermission")
    @Override
    public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            mDevice = device;
            mBluetoothGattServer.connect(mDevice, false);
            // 接続確立時にアドバタイズを停止
            stopAdvertising();

        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            //セントラルからの切断
        }
    }

    @Override
    public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
        // CentralからのREAD_REQ

        if (!CHARACTERISTIC_UUID.equals(characteristic.getUuid()) ||
            ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED
        ) {
            return;
        }

        String responseData = null;
        // TODO 返信するデータを作成

        if (responseData != null) {
            mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, partBytes);
        } else {
            mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
        }
    }

    @Override
    public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
        // CentralからのWRITE

        if (CHARACTERISTIC_UUID.equals(characteristic.getUuid())) {
            return;
        }
        if (value == null) {
            return;
        }

        final String receivedData = new String(value);

        if (responseNeeded) {
            int responseStatus = BluetoothGatt.GATT_SUCCESS;

            if (ActivityCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                return;
            }
            mBluetoothGattServer.sendResponse(device, requestId, responseStatus, 0, null);
        }
    }
};

アドバタイズ

if (mBluetoothAdapter.isMultipleAdvertisementSupported()) {
    stopAdvertising();
    startAdvertising();
}


private void startAdvertising() {
    if (isAdvertising) {
        // Advertising is already in progress
        return;
    }

    final byte[] serviceData = new byte[] { /* TODO アドバタイズ用のデータ作成 */ };

    advertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
    AdvertiseSettings settings = new AdvertiseSettings.Builder()
            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
            .setConnectable(true)
            .setTimeout(0)
            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
            .build();

    // Advertise Packetに入れる情報をセット(31bytes内で)
    // Central側はScanでこの情報を取得する
    final AdvertiseData data = new AdvertiseData.Builder()
            .setIncludeDeviceName(false)
            //.addServiceUuid(new ParcelUuid(SERVICE_UUID))
            .addServiceData(new ParcelUuid(SERVICE_DATA_UUID), serviceData)
            .build();

    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED) {
        return;
    }

    // スキャン応答用のデータ(Option)
    byte[] scanRspData = "123456789012345678901234567".getBytes(StandardCharsets.UTF_8);
    final AdvertiseData scanRsp = new AdvertiseData.Builder()
            .setIncludeDeviceName(false)
            .addServiceData(new ParcelUuid(SERVICE_DATA_UUID_FOR_SCAN_RSP), scanRspData)
            .build();

    isAdvertising = true;
    //advertiser.startAdvertising(settings, data, advertiseCallback); // スキャン応答なし
    advertiser.startAdvertising(settings, data, scanRsp, advertiseCallback);
}

private void stopAdvertising() {
    Log.i(TAG, "stopAdvertising");

    if (!isAdvertising || advertiser == null) {
        return;
    }

    isAdvertising = false;
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADVERTISE) == PackageManager.PERMISSION_GRANTED) {
        advertiser.stopAdvertising(advertiseCallback);
    }
}

private final AdvertiseCallback advertiseCallback = new AdvertiseCallback() {
    @Override
    public void onStartSuccess(AdvertiseSettings settingsInEffect) {}

    @Override
    public void onStartFailure(int errorCode) {}
};

Central側の実装

Android

final BluetoothManager bm = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
final BluetoothAdapter adapter = bm.getAdapter();
if (adapter != null) {
    bluetoothLeScanner = adapter.getBluetoothLeScanner();
}

// スキャンフィルターの設定
final ParcelUuid sdUuid = ParcelUuid.fromString(SERVICE_DATA_UUID__STARTUP);
final ScanFilter filter = new ScanFilter.Builder()
        //.setServiceUuid(ParcelUuid.fromString(SERVICE_UUID))
        // Service DataのUUIDでフィルターをかける場合
        .setServiceData(sdUuid, new byte[] {}, null)
        .build();
final List<ScanFilter> filters = new ArrayList<>();
filters.add(filter);

// スキャン設定
final ScanSettings settings = new ScanSettings.Builder()
        .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
        .setScanMode(ScanSettings.SCAN_MODE_BALANCED)
        .setLegacy(true) // Bluetooth 4.2以前のAdvertiseだけをスキャン結果として通知する(基本)
        .build();

// スキャン開始
if (!checkBlePermission()) return;
bluetoothLeScanner.startScan(filters, settings, scanCallback);


public boolean checkBlePermission() {
    return ActivityCompat.checkSelfPermission(
            context,
            android.Manifest.permission.BLUETOOTH_CONNECT
    ) == PackageManager.PERMISSION_GRANTED;
}

private final ScanCallback scanCallback = new ScanCallback() {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        final BluetoothDevice device = result.getDevice();

        // 接続済みデバイスかを確認
        if (connectedDeviceAddress != null) {
            return;
        }

        final ScanRecord record = result.getScanRecord();
        if (record == null) return;

        // アドバタイズの固有のデータを取得する
        final byte[] serviceData = record.getServiceData(ADV_SERVICE_DATA_UUID);

        // スキャン応答のデータ
        final byte[] rspData = record.getServiceData(SCAN_RSP_SERVICE_DATA_UUID);

        final boolean enableConnect;
        if (serviceData != null) {
            // 信号の強度
            final int rssi = result.getRssi();

            // TODO ServiceDataの情報を解析して、接続するか、否かを判断
            enableConnect = true;
        } else {
            enableConnect = false;
        }

        if (!enableConnect) {
            // 接続する必要があるない場合は、ここで中断
            return;
        }

        if (!checkBlePermission()) return;

        // まだ接続されていないので、このデバイスと接続
        mBluetoothGatt = device.connectGatt(context, false, gattCallback);
        connectedDeviceAddress = device.getAddress(); // デバイスのMACアドレスを保存

        // スキャン中止
        bluetoothLeScanner.stopScan(scanCallback);                
    }
};

private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
    @Override
    public void onServiceChanged(@NonNull BluetoothGatt gatt) {
        super.onServiceChanged(gatt);
    }

    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        switch (newState) {
            case BluetoothProfile.STATE_CONNECTED: {
                if (!checkBlePermission()) {
                    return;
                }
                // スキャン中止
                bluetoothLeScanner.stopScan(scanCallback);                

                gatt.requestMtu(512); // MTUを最大値で要求
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ignored) {}

                // サービスを発見
                gatt.discoverServices();
                break;
            }

            case BluetoothProfile.STATE_DISCONNECTED: {
                connectedDeviceAddress = null;
                break;
            }

            default: break;
        }
    }

    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        if (status != BluetoothGatt.GATT_SUCCESS) {
            return;
        }

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            logger.e(e.toString());
        }

        final BluetoothGattService service = gatt.getService(serviceUuid);
        if (service == null) return;

        // Characteristic取得
        final BluetoothGattCharacteristic characteristic = service.getCharacteristic(CHAR_UUID);
        if (characteristic != null) {
            // TODO ...
        }
    }

    @Override
    public void onCharacteristicRead(
            @NonNull BluetoothGatt gatt,
            @NonNull BluetoothGattCharacteristic characteristic,
            @NonNull byte[] value,
            int status
    ) {
        // Peripheralからデータを受け取った場合
    }

    @Override
    public void onCharacteristicWrite(
            BluetoothGatt gatt,
            BluetoothGattCharacteristic characteristic,
            int status
    ) {
        // Peripheralにデータを送信した後、Peripheralか応答がある場合
    }
};
⚠️ **GitHub.com Fallback** ⚠️