Get Advertisement Data for Ble in iOS

Get advertisement data for BLE in iOS

Unfortunately, iOS does not allow you to access the raw advertisement data. I wrote a blog post demonstrating this. While the post is specifically about iBeacons, it applies to any BLE advertisement.

EDIT: To clarify, you can read the raw manufacturer data bytes or service data bytes of non-iBeacon advertisements. It is only the iBeacon advertisements that have their manufacturer data bytes hidden by CoreLocation. See here: Obtaining Bluetooth LE scan response data with iOS

The equivalent MacOS CoreLocation methods do allow this, so it is probably an intentional security or power saving restriction on iOS.

Ble: Send advertise data to iOS from Android

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)
}
}
}
}
}

iOS omits manufacturer data from advertisement in background mode

An app simply cannot read raw BLE manufacturer advertisement data when in the background on iOS -- the operating system prohibits it.

Two exceptions to this rule:

  1. iBeacon, which itself is implemented as a specific type of manufacturer advertisement. An app can detect iBeacons in the background on iOS, although only four bytes of readable data (encoded in the major and minor fields) are fully usable. If you can modify your device to send information this way, it will do what you want. However you must use CoreLocation APIs to detect iBeacon, as CoreBluetooth does not allow reading manufacturer data from iBeacon advertisements. If you do use CoreLocation, you cannot use the detections to establish a Bluetooth connection with CoreBluetooth as the two APIs are sandboxed.

  2. Overflow Area advertisements. Backgrounded iOS apps can read these special types of manufacturer advertisements when in the background but only if the screen is turned on. (It is often possible to force the screen on at specific times by sending a local notification.) See my blog post here for more info: http://www.davidgyoungtech.com/2020/05/07/hacking-the-overflow-area

An alternative to detecting manufacturer advertisements is to use BLE Service advertisements with attached data. For this to work, you'd need to define a 16 bit or 128 bit GATT Service UUID and send out an advert with attached data bytes. Eddystone beacon formats work this way, and allow detection in the background on iOS. This is probably the best approach if you can alter the BLE hardware.

Attaching advertising payload for iOS without pairing

Unfortunately, you cannot use CoreBluetooth APIs to attach data to advertisements. On iOS the CBAdvertisementDataServiceDataKey is read-only. While Bluetooth LE allows attaching service data , Apple effectively disallows 3rd party apps from doing this.

You do have a few options:

  1. Encode your data inside a 128-bit service UUID and advertise that. You will need to reserver a byte or two in the UUID to know that it is "your" advertisement, and therefore OK to decode the data from the other byes. This full UUID will only be advertised when your app is in the foreground visible on the screen. Let it go to the background or the screen turn off, and it will no longer advertise in that form. Similarly, receiving iOS devices must also be in the foreground with the screen on. This is because iOS disallows getting background scan results without specifying the matching service UUID up front. And because you are dynamically manipulating some of those bytes, you don't know what it will be.

  2. Do a similar kind of encoding with the 4 byte major and minor fields inside the iBeacon BLE advertisement using CoreLocation. Again, this allows you to transmit only when the app is in the foreground. Receiving, however, can happen to a limited degree in the background (for 5-10 seconds after one of your beacons first appears when combining monitoring and ranging APIs.) The big disadvantage is you only have four bytes to work with.

  3. Advertise data by manipulating the 128-bit background BLE Overflow Area Advertisement. This technique is more advanced, but advertising works in the background. Receiving works in the foreground, and partly in the background -- you can receive if the screen is at least turned on. You can read more about this technique and access free sample code in my blog post herehttp://www.davidgyoungtech.com/2020/05/07/hacking-the-overflow-area.

Accessing raw advertisement data for custom BLE device on iOS 8 using Core Bluetooth

You can do what you want if you change the Raspberry Pi to transmit a non-iBeacon format. CoreBluetooth only filters out the raw bytes of advertisements if they are iBeacon advertisements. See here: Obtaining Bluetooth LE scan response data with iOS

A simple solution is to change your iBeacon advertisement to an open-source AltBeacon advertisement. CoreLocation will no longer pick it up, but CoreBluetooth will.

Here's an example of what you get in the advertisementData NSDictionary in the CoreBluetooth centralManager:didDiscoverPeripheral:advertisementData:RSSI: callback. This example is the result of detecting an AltBeacon advertisement (an open-source beacon standard), with identifiers 2F234454-CF6D-4A0F-ADF2-F4911BA9FFA6 1 2:

{
kCBAdvDataIsConnectable = 0;
kCBAdvDataManufacturerData = <1801beac 2f234454 cf6d4a0f adf2f491 1ba9ffa6 00010002 be00>;
}

You can see how to decode the above bytes by looking at the AltBeacon spec here. Note that the above are the actual contents of the NSDictionary for a detected advertisement on iOS8 that were printed to the console using an NSLog statement.



Related Topics



Leave a reply



Submit