1

I have to send some advertise data via ble to an iOS central app. On Android peripheral side, when i create advertise i do:

    var byteArrayData = dataToSend.toByteArray(Charsets.UTF_8)

    var data = AdvertiseData.Builder().apply {
        addServiceData(
            ParcelUuid(MY_SERVICE_UUID),
            byteArrayData
        )
    }
    bleAdvertiser.startAdvertising(mySettings.build(), data.build(), myAdvertiseCallback)

but it seems that iOS is not able to read custom service data.

So, there's a way to sent some small data (few bytes) via BLE to iOS (and receive also data when Android is central and iOS is pheripheral)?

I would avoid, if is possible, to open a connection between central an peripheral.

giozh
  • 9,868
  • 30
  • 102
  • 183

1 Answers1

4

iOS is very restrictive regarding advertisement data. Both when sending and receiving it, you can only control a small subset of it. Most of it is controlled by iOS itself and — in case of the central manager role — not even forwarded to the app.

The exceptions are the Advertisement Data Retrieval Keys, applicable for the advertisementData parameter of centralManager(_:didDiscover:advertisementData:rssi:).

A more specific example is mentioned in this answer.

Update

Even though one of the keys is for service data, I don't think the data is forwarded to the app. But I might be wrong. I guess you are asking this question because the key CBAdvertisementDataServiceDataKey is not set.

Update 2

I've created a minimal Android and iOS example and got it working without any problem. I don't see no obvious problem in your Android code. So you will need to talk to your iOS colleague...

The service data is "ABC" (or 61 62 63 in hex) and the 16-bit UUID is FF01. The iOS log output is:

2019-09-05 16:39:18.987142+0200 BleScanner[18568:3982403] [Scanner] Advertisement data: FF01: <616263>

Android - MainActivity.kt

package bleadvertiser

import android.bluetooth.BluetoothManager
import android.content.Context
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

    private var peripheral: Peripheral? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onStart() {
        super.onStart()
        val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
        peripheral = Peripheral(bluetoothManager.adapter)
        peripheral?.startAdvertising()
    }
}

Android - Peripheral.kt

package bleadvertiser

import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.os.ParcelUuid
import android.util.Log
import java.util.*

private const val TAG = "Peripheral"

class Peripheral(private val bluetoothAdapter: BluetoothAdapter) {

    fun startAdvertising() {
        val advertiseSettings = AdvertiseSettings.Builder().build()
        val serviceData = "abc".toByteArray(Charsets.UTF_8)
        val advertiseData = AdvertiseData.Builder()
            .addServiceData(ParcelUuid(SERVICE_UUID), serviceData)
            .build()
        val advertiser = bluetoothAdapter.bluetoothLeAdvertiser
        advertiser.startAdvertising(advertiseSettings, advertiseData, advertiseCallback)
    }

    private val advertiseCallback = object: AdvertiseCallback() {
        override fun onStartFailure(errorCode: Int) {
            Log.w(TAG, String.format("Advertisement failure (code %d)", errorCode))
        }
        override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
            Log.i(TAG, "Advertisement started")
        }
    }

    companion object {
        val SERVICE_UUID: UUID = UUID.fromString("0000ff01-0000-1000-8000-00805F9B34FB")
    }
}

iOS - ViewController.swift

import UIKit

class ViewController: UIViewController {

    var bleScanner: BleScanner?

    override func viewDidLoad() {
        super.viewDidLoad()
        bleScanner = BleScanner()
    }
}

iOS - BleScanner.swift

import Foundation
import CoreBluetooth
import os.log

class BleScanner : NSObject {

    static let serviceUUID = CBUUID(string: "FF01")
    static let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Scanner")

    private var centralManager: CBCentralManager!
    private var scanningTimer: Timer?

    override init() {
        super.init()
        centralManager = CBCentralManager(delegate: self, queue: nil)
    }

    func startScanning() {
        scanningTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(20), repeats: false, block: { (_) in
            self.stopScanning()
        })
        centralManager.scanForPeripherals(withServices: [ BleScanner.serviceUUID ], options: nil)
    }

    func stopScanning() {
        centralManager.stopScan()
    }
}

extension BleScanner : CBCentralManagerDelegate {

    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if centralManager.state == .poweredOn {
            startScanning()
        }
    }

    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        for (key, value) in advertisementData {
            if key == CBAdvertisementDataServiceDataKey {
                let serviceData = value as! [CBUUID : NSData]
                for (uuid, data) in serviceData {
                    os_log("Advertisement data: %{public}s: %{public}s", log: BleScanner.log, type: .info, uuid.uuidString, data.debugDescription)
                }
            }
        }
    }
}
Codo
  • 75,595
  • 17
  • 168
  • 206
  • I'm asking this question because iOS central app developer (i'm not an iOS developer) told me that it's not possible to read serviceData provided by my Android peripheral app. It seems that iOS ignore completely Android advertise serviceData i put when i start advertising. – giozh Sep 05 '19 at 14:26
  • Another problem you might run into is the length of the advertisement data. I think the payload is restricted to 28 bytes. If you have a 16 byte service UUID (instead of one of the registered 2 byte UUIDs), the payload is only 10 bytes. And if you have a long device name, it further reduces the payload. So you probably want to test it once with something very small like 2 bytes. – Codo Sep 05 '19 at 14:38
  • The length is not a problem. I've do some test with between two Android (one act as a central and another one act as peripheral) and if i don't include device name into advertise, data that i should send fits well and central receive it (in any case, if serviceData is too long, advertise doesn't start). – giozh Sep 05 '19 at 14:44
  • Ok, it works :) Just a question, why you are using a portion of uuid (FF01) for scan filtering? – giozh Sep 09 '19 at 07:49
  • The filtering is to ensure only device that implement the service are discovered. FF01 will be expanded to 0000ff01-0000-1000-8000-00805F9B34FB. That's how 16 bit and 128 bit UUIDs interoperate in Bluetooth LE (see Base UUID in https://www.bluetooth.com/specifications/assigned-numbers/service-discovery/). When you attach service data to the advertisement packet, you probably use a 16 bit UUID as well. – Codo Sep 09 '19 at 08:00