3

I'm working on a Android App which is scanning for BLE devices. Everytime I found a device, I receive: byte[] scanRecord, BluetoothDevice device, int rssi from BluetoothAdapter.startLeScan()

I then convert the byte array to a ScanRecord object: ScanRecord.parseFromBytes()

I have now the following information from my Eddystone (from toString() method).

`com.reelyactive.blesdk.support.ble.ScanRecord [mAdvertiseFlags=6, mServiceUuids=[0000feaa-0000-1000-8000-00805f9b34fb], mManufacturerSpecificData={}, mServiceData={0000feaa-0000-1000-8000-00805f9b34fb=[16, -36, 2, 107, 110, 116, 107, 46, 105, 111, 47, 101, 100, 100, 121, 115, 116, 111, 110, 101], 0000d00d-0000-1000-8000-00805f9b34fb=[67, 77, 103, 52, 50, 57, 100]}, mTxPowerLevel=-12, mDeviceName=IIS_EDDY_003] IIS_EDDY_003` 

Can someone tell me, how to identify the device as an Eddystone with this information? Service uuids maybe? I don't always know the name or the adress of the device.

Steve_Rhoades
  • 55
  • 1
  • 5

2 Answers2

6

android.bluetooth.le.ScanRecord is one of the worst APIs in Android.

If you already have a scanRecord (byte array), I recommend nv-bluetooth to extract Eddystone data. The following code snippet shows usage of nv-bluetooth.

// Parse the payload of the advertising packet.
List<ADStructure> structures =
    ADPayloadParser.getInstance().parse(scanRecord);

// For each AD structure contained in the payload.
for (ADStructure structure : structures)
{
    if (structure instanceof EddystoneUID)
    {
        // Eddystone UID
        EddystoneUID es = (EddystoneUID)structure;

        // (1) Calibrated Tx power at 0 m.
        int power = es.getTxPower();

        // (2) 10-byte Namespace ID
        byte[] namespaceId = es.getNamespaceId();
        String namespaceIdAsString = es.getNamespaceIdAsString();

        // (3) 6-byte Instance ID
        byte[] instanceId = es.getInstanceId();
        String instanceIdAsString = es.getInstanceIdAsString();

        // (4) 16-byte Beacon ID
        byte[] beaconId = es.getBeaconId();
        String beaconIdAsString = es.getBeaconIdAsString();
    }
    else if (structure instanceof EddystoneURL)
    {
        // Eddystone URL
        EddystoneURL es = (EddystoneURL)structure;

        // (1) Calibrated Tx power at 0 m.
        int power = es.getTxPower();

        // (2) URL
        URL url = es.getURL();
    }
    else if (structure instanceof EddystoneTLM)
    {
        // Eddystone TLM
        EddystoneTLM es = (EddystoneTLM)structure;

        // (1) TLM Version
        int version = es.getTLMVersion();

        // (2) Battery Voltage
        int voltage = es.getBatteryVoltage();

        // (3) Beacon Temperature
        float temperature = es.getBeaconTemperature();

        // (4) Advertisement count since power-on or reboot.
        long count = es.getAdvertisementCount();

        // (5) Elapsed time in milliseconds since power-on or reboot.
        long elapsed = es.getElapsedTime();
    }
    else if (structure instanceof IBeacon)
    {
        // iBeacon
        IBeacon iBeacon = (IBeacon)structure;

        // (1) Proximity UUID
        UUID uuid = iBeacon.getUUID();

        // (2) Major number
        int major = iBeacon.getMajor();

        // (3) Minor number
        int minor = iBeacon.getMinor();

        // (4) Tx Power
        int power = iBeacon.getPower();
    }
}

The above code implies that a scan record should be parsed as a list of AD structures. However, parseFromBytes of android.bluetooth.le.ScanRecord does not parse a scan record in the right way.

ScanRecord has the following methods (and some others):

  1. getAdvertiseFlags()
  2. getDeviceName()
  3. getManufacturerSpecificData()
  4. getServiceData()
  5. getTxPowerLevel()

These methods correspond to some AD structures. This API design is the same structure as AnimalRecord class shown below.

public class AnimalRecord
{
    public Cat getCat() { ... }
    public Dog getDog() { ... }
    public Eagle getEagle() { ... }
    ...
}

Flags, Local Name, Manufacturer Specific Data, Service Data, and Tx Power Level also should be parsed as AD structures like below.

// Parse the payload of the advertising packet.
List<ADStructure> structures =
    ADPayloadParser.getInstance().parse(scanRecord);

// For each AD structure contained in the payload.
for (ADStructure structure : structures)
{
    if (structure instanceof Flags)
    {
        // Flags
        Flags flags = (Flags)structure;
    }
    else if (structure instanceof LocalName)
    {
        // Local Name
        LocalName name = (LocalName)structure;
    }
    else if (structure instanceof ADManufacturerSpecific)
    {
        // Manufacturer Specific Data
        // Note that iBeacon is a kind of Manufacturer Specific Data
        ADManufacturerSpecific ms = (ADManufacturerSpecific)structure;
    }
    else if (structure instanceof ServiceData)
    {
        // Service Data
        // Note that Eddystone is a kind of Service Data.
        ServiceData sd = (ServiceData)structure;
    }
    else if (structure instanceof TxPowerLevel)
    {
        // TxPowerLevel
        TxPowerLevel level = (TxPowerLevel)structure;
    }
}

As commented in the code above, Eddystone is a kind of Service Data. Therefore, Eddystone UID, Eddystone URL and Eddystone TLM should have an inheritance tree like below.

ADStructure
  |
  +-- ServiceData
        |
        +-- Eddystone
              |
              +-- EddystoneUID
              +-- EddystoneURL
              +-- EddystoneTLM

I hope those who know the BLE specification very well and have good design skills will rewrite Android's BLE APIs from scratch.

Takahiko Kawasaki
  • 18,118
  • 9
  • 62
  • 105
  • Can I get EddystoneTLM from transmitted BLE advertisement packet ? Sorry that I am using bluez for raspberry, not Android. – Nam Vu Nov 06 '15 at 05:50
  • All Eddystone data can be found in BLE advertisement packets. You don't have to connect to BLE devices. Just scanning advertisement packets is enough. – Takahiko Kawasaki Nov 06 '15 at 05:58
  • I read: https://github.com/google/eddystone/blob/master/protocol-specification.md . But I do not understand how to get EddystoneTLM from it. with other beacon, I can get uuid, mac adress, major, minor, tx power ... – Nam Vu Nov 06 '15 at 09:02
0

For those who like to know how it actually works:

mServiceData={
  0000feaa-0000-1000-8000-00805f9b34fb=[
    16, -36, 2, 107, 110, 116, 107, 46, 105, 111, 47, 101, 100, 100, 121, 115, 116, 111, 110, 101
  ], 
  0000d00d-0000-1000-8000-00805f9b34fb=[
    67, 77, 103, 52, 50, 57, 100
  ]
}

The first service data package is identifiable as a EddyStone data by the top 32 bits of "0000feaa-0000-1000-8000-00805f9b34fb". When converting 0000feAA is the 16 bit EddyStone Service UUID that can be found in the Bluetooth Data Service Specification.

16-bit UUID for Members => 0xFEAA => Google

Services always emit "????????-0000-1000-8000-00805f9b34fb" with the top 32 bits of this UUID replaced by the service its alias. And in this case 'feaa' means EddyStone Service data (created/specified by Google).

So because of identifying the key we now know that the value is an EddyStone DataView. Those values need to be mapped/interpreted according to the EddyStone specifications:

https://github.com/google/eddystone/blob/master/protocol-specification.md

To extract the Frame Type (EddyStone UID, URL, TLM or EID) you take the first value of the array:

FrameType = 16; => 0x10 => EddyStone URL

To understand the remaining values we need to look at the EddyStone URL specification:

https://github.com/google/eddystone/tree/master/eddystone-url

To extract the TX Power you take the second value of the array:

TX Power = -36; => -36

To extract the URL Schema you take all remaining values and convert them to charcodes:

107 => k
110 => n
116 => t
107 => k
 46 => .
105 => i
111 => o
 47 => /
101 => e
100 => d
100 => d
121 => y
115 => s
116 => t
111 => o
110 => n
101 => e

So the URL is: 'kntk.io/eddystone'

To summarize:

The beacon advertises a EddyStone data service package recognizable by the 128bit UUID "0000feaa-0000-1000-8000-00805f9b34fb" and is using the "EddyStone URL" frame type (first value of frame) and is advertising the following url: "kntk.io/eddystone"

I hope that by breaking down this data from the question into the actual real values will be helping people ending up here to understand how Bluetooth Advertising actually works.

You can use one of the many libraries to do all these things for you, but understanding the basics can be useful...


Note: I suspect that the second package is a native Kontakt.io Service frame type being advertised by the beacon to be used internally by the Kontakt.io tools.

Wilt
  • 41,477
  • 12
  • 152
  • 203