Bluetooth UUIDs and Cross-Platform Advertisements

Dave Smith
Dave Smith
Bluetooth UUIDs and Cross-Platform Advertisements

Ever since the Bluetooth Classic days, peripheral devices have exposed their connectable interfaces as services. Each service is represented by a universally unique identifier (UUID) — a 128-bit value that is often expressed as a string in the following format:

XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX

When creating a Bluetooth-enabled device, developers may use any valid 128-bit UUID they wish to represent the available services. However, to facilitate interoperability between device manufacturers and consumers, the Bluetooth Special Interest Group (SIG) defines several service UUIDs intended to be used on common device profiles. These pre-defined UUIDs are also often called Assigned Numbers.

This is where things start to get a little weird. The official Bluetooth Assigned Numbers are defined as "16-bit UUIDs" — not 128-bit. So how does that work? What’s the difference between a 16-bit and 128-bit UUID? In this article I’d like to explore this with you, and extrapolate how this can affect advertisements across the iOS and Android mobile platforms.

There is No Such Thing As a 16-Bit UUID

Read that phrase again if you need to, and lock it into your memory. Remembering this will help clear up confusion for you in the future — the term "16-bit UUID" is a misnomer. It’s not an actual UUID, but rather a shorthand notation. The Bluetooth SIG has reserved the following UUID, which is often referred to as the base UUID:

00000000-0000-1000-8000-00805F9B34FB

The upper section is used as a fill-in template where the 16-bit "short code" is placed:

0000XXXX-0000-1000-8000-00805F9B34FB

So, for example, when the Google Eddystone Protocol declares the UUID of the Eddystone service to be 0xFEAA (a 16-bit value), that is equivalent to saying the UUID is:

0000FEAA-0000-1000-8000-00805F9B34FB

As another example, the Health Thermometer Service is assigned as 0x1809, so its equivalent UUID is:

00001809-0000-1000-8000-00805F9B34FB

Make sense? So, why is this important? Let’s talk a bit more about how UUIDs are represented in Bluetooth Low Energy (LE) advertisements.

Note: It’s interesting to note that the Bluetooth spec allows for 32-bit UUID values as well. Just as with 16-bit values, this is another shortcut for the same UUID. This rarely appears in practice, but a 32-bit fill-in would look like this:

XXXXXXXX-0000-1000-8000-00805F9B34FB

UUIDs in Advertisements

Bluetooth LE devices transmit advertisement packets to notify nearby devices of their presence. A list of available Service UUIDs may be included as part of this advertisement (among other pieces of data). This is common in both non-connectable (beacon) and connectable (GATT peripheral) applications as a way of discovering the correct type of device.

Bluetooth LE advertisements are small — 31 bytes, to be exact. That means just two full-size UUID values would essentially fill up the entire payload! This is where truncating the UUID can be beneficial. So, while 16-bit UUIDs don’t exist, their shorthand is a useful way of saving space in packet frames whenever possible.

In practice, advertisement frames will contain one of four data types to represent UUIDs:

  • 0x02 - Incomplete list of 16-bit UUIDs
  • 0x03 - Complete list of 16-bit UUIDs
  • 0x06 - Incomplete list of 128-bit UUIDs
  • 0x07 - Complete list of 128-bit UUIDs

Advertisements describe the length of the UUID so the clients know how to interpret them. When a 16-bit UUID is transmitted, only the unique 2-byte value is sent. The client knows it needs to be injected into the base UUID before it will be useful — but we don’t have to waste the space in the packet frame.

Now that we understand a bit more about how advertisements transmit this data, let’s look at how the iOS and Android APIs correspond.

Scanning on Mobile Platforms

Note: The following is based on the APIs available in iOS 6.0+ and Android 5.0+

iOS scan results, delivered to a CBCentralManagerDelegate, will contain a list of CBUUID instances for any service UUIDs advertised by that device. This is a UUID wrapper for the Core Bluetooth APIs, so it will treat Bluetooth-specific UUIDs differently.

The contents of the data and UUIDString properties will vary depending on the CBUUID type. Normal 128-bit UUIDs return a 16-byte data element, while 16-bit assigned numbers will report a 2 bytes for the same attribute. iOS never really exposes to developers that a 16-bit assigned number is part of a larger standard UUID format.

In Android, the Bluetooth APIs don’t include a concept of the 16-bit assigned number. The ParcelUuid class doesn’t have a representation for anything that is not a proper (128-bit) UUID value. Because of this, when Android scans a device advertising the 0xFEAA service, it will report the service UUID as 0000FEAA-0000-1000-8000-00805F9B34FB to the application.

Advertisements on Mobile Platforms

Note: The following is based on the APIs available in iOS 6.0+ and Android 5.0+

In iOS, a CBUUID can be constructed from either a 16-bit or 128-bit string. The corresponding advertisement will placement in the appropriate lists:

// Sent as AD Type 0x03 using 2 bytes
[CBUUID UUIDWithString:@"FEAA"];
// Sent as AD Type 0x07 using 16 bytes
[CBUUID UUIDWithString:@"93FB4E84-CCF1-4ED9-BD24-053267701B25"];

Interestingly, iOS treats the full version of a 16-bit UUID in an unexpected way:

// Sent as AD Type 0x07 using 16 bytes
[CBUUID UUIDWithString:@"0000FEAA-0000-1000-8000-00805F9B34FB"];

The framework does not interpret that the UUID matches the standard base and could be truncated, so it sends more bytes than is necessary. This is not technically incorrect, but it is a bit inefficient. iOS is smart enough to know what kind of UUID it is, however, as it will not allow you to add both types (first one added wins). This is evidence that they do maintain the full UUID internally, even for 16-bit values, despite not exposing it to developers:

// This will be added to the advertisement
[CBUUID UUIDWithString:@"FEAA"];
// This will be ignored as a duplicate
[CBUUID UUIDWithString:@"0000FEAA-0000-1000-8000-00805F9B34FB"];

Android, as mentioned before, doesn’t have a special UUID type for Bluetooth. Applications that wish to include a 16-bit UUID in the payload must add it using the full-version format:

// The following throws an IllegalArgumentException
ParcelUuid.fromString("FEAA");

// Sent as AD Type 0x03 using 2 bytes
ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
// Sent as AD Type 0x07 using 16 bytes
ParcelUuid.fromString("93FB4E84-CCF1-4ED9-BD24-053267701B25");

Pushing the Limits

Both iOS and Android limit the advertising payload size for the application to 28 bytes. A 3-byte flags (AD Type 0x01) packet is always included automatically. However, each platform deals with that limit in very different ways that may affect the ability for the two platforms to communicate.

In Android, an error is thrown when the application attempts to include more than 28 bytes of data. The AdvertiseCallback.onStartFailure() will receive an ADVERTISE_FAILED_DATA_TOO_LARGE error code, and the advertisement will not begin. Because of this, Android will never advertise AD Types other than 0x03 (for 16-bit) and 0x07 (for 128-bit). This also means there really isn’t any advertisement that Android can produce which iOS won’t recognize.

Take the following Android source code, for example:

SHORT_UUID = ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
LONG_UUID = ParcelUuid.fromString("93FB4E84-CCF1-4ED9-BD24-053267701B25");

AdvertiseData data = new AdvertiseData.Builder()
        .addServiceUuid(SHORT_UUID)
        .addServiceUuid(LONG_UUID)
        .build();
leAdvertiser.startAdvertising(settings, data, callback);

This will produce an advertising packet that looks like this:

02 01 1A 11 07 25 1B 70 67 32 05 24 BD D9 4E F1 CC 84 4E FB 93 03 03 AA FE

...with the 128-bit packet in blue and the 16-bit packet in red. Adding another 128-bit UUID, like so:

SHORT_UUID = ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
LONG_UUID = ParcelUuid.fromString("93FB4E84-CCF1-4ED9-BD24-053267701B25");
LONG2_UUID = ParcelUuid.fromString("5B96CF8D-91CC-4984-9900-4BA95CB3A27F")

AdvertiseData data = new AdvertiseData.Builder()
        .addServiceUuid(SHORT_UUID)
        .addServiceUuid(LONG_UUID)
        .addServiceUuid(LONG2_UUID)
        .build();
leAdvertiser.startAdvertising(settings, data, callback);

...will throw the ADVERTISE_FAILED_DATA_TOO_LARGE error previously mentioned, without starting any advertisements.

On iOS, if the app adds too much data for the 28-byte limit, the application is not notified. Instead, the data that did not fit is pushed into a separate "overflow" area that is not broadcast (documented here). In fact, it is only accessible to other iOS devices. Since the application has no knowledge of _when this overflow occurs, it doesn’t really have a way of controlling what was put into it.

The following iOS code results in the same advertisement listed above:

CBCentralManager *manager;

CBUUID *uuidShort = [CBUUID UUIDWithString:@"FEAA"];
CBUUID *uuidLong =
    [CBUUID UUIDWithString:@"93FB4E84-CCF1-4ED9-BD24-053267701B25"];

NSDictionary *options =
    @{CBAdvertisementDataServiceUUIDsKey:@[uuidShort, uuidLong]};
[manager startAdvertising:options];

When overflows occur, iOS starts transmitting AD Types 0x02 (for 16-bit) and 0x06 (for 128-bit) as appropriate — meaning the list in the advertisement is incomplete. This time when another long UUID is added, like so:

CBCentralManager *manager;

CBUUID *uuidShort = [CBUUID UUIDWithString:@"FEAA"];
CBUUID *uuidLong =
    [CBUUID UUIDWithString:@"93FB4E84-CCF1-4ED9-BD24-053267701B25"];
CBUUID *uuidLong2 =
    [CBUUID UUIDWithString:@"5B96CF8D-91CC-4984-9900-4BA95CB3A27F"];

NSDictionary *options =
    @{CBAdvertisementDataServiceUUIDsKey:@[uuidShort, uuidLong, uuidLong2]};
[manager startAdvertising:options];

The advertisement changes into this:

02 01 1A 11 07 25 1B 70 67 32 05 24 BD D9 4E F1 CC 84 4E FB 93 03 03 AA FE 14 FF 4C 00 01 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00

Notice the new element, which does not contain the other UUID. It's a Manufacturer Data block (AD Type 0xFF). Manufacturer data is essentially an opaque blob from a Bluetooth perspective — the information could mean anything. In this case, it’s a sign to the scanning device that "overflow" has been activated and the data somehow instructs another iOS device on how to find the extra bits.

So, What is the Overflow?

It’s not explicitly documented, but there are only so many options provided by the Bluetooth specification that would provide for such a feature. Most likely, the "overflow" area iOS refers to is a scan response message. This is a polled message that advertising devices can listen for and provide additional data without a connection. Scan responses allow for whitelisting, which would let iOS filter and ignore any scan requests it receives from any devices except another iOS device.

Another extremely important point is this: when iOS applications advertise from the background state, ALL their advertised data is pushed into overflow and the advertisement is essentially empty. It will look something like the following:

02 01 1A 14 FF 4C 00 01 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00

Android cannot detect any data that is not present in the advertisement packet. This means that data overflow and background advertising will put the information out of reach for any Android device.

Effects on Service Data

Another common AD Type is the Service Data payload. This allows a small chunk of data associated with a particular service to be included in the advertisement along with its UUID. Android only supports the version of the service data block that contains a 16-bit UUID, but it does not verify that the UUID you are using matches the 16-bit format. Any UUID included with a service data element will be truncated, placing the upper 16-bit block used for a standard assigned number into the advertisement.

This may potentially lead to collisions if you aren’t aware that it is happening. Take the following Android example:

LONG_UUID = ParcelUuid.fromString("93FB4E84-CCF1-4ED9-BD24-053267701B25");
AdvertiseData data = new AdvertiseData.Builder()
        .addServiceUuid(LONG_UUID)
        .addServiceData(LONG_UUID, "ME".getBytes(Charset.forName("UTF-8")))
        .build();
leAdvertiser.startAdvertising(..., data, ...);

This will append a service data block with the AD Type 0x16 (16-bit Service Data) using 0x4E84 as the UUID bytes:

02 01 1A 11 07 25 1B 70 67 32 05 24 BD D9 4E F1 CC 84 4E FB 93 05 16 84 4E 4D 45

Now let’s assume your iOS application needs to retrieve the service data. The following code you would expect to write, won’t work:

- (void)centralManager:(CBCentralManager *)central
 didDiscoverPeripheral:(CBPeripheral *)peripheral
     advertisementData:(NSDictionary<NSString *,id> *)advertisementData
                  RSSI:(NSNumber *)RSSI {

    NSDictionary<CBUUID *, NSData *> *data =
        [advertisementData valueForKey:CBAdvertisementDataServiceDataKey];
    if (data) {
        CBUUID *uuid =
            [CBUUID UUIDWithString:@"93FB4E84-CCF1-4ED9-BD24-053267701B25"];
        //This will return nil from the dictionary
        NSLog(@"Data: %@", [data objectForKey:uuid]);
    }
}

Instead, you would need to create a matching 16-bit UUID, like so:

- (void)centralManager:(CBCentralManager *)central
 didDiscoverPeripheral:(CBPeripheral *)peripheral
     advertisementData:(NSDictionary<NSString *,id> *)advertisementData
                  RSSI:(NSNumber *)RSSI {

    NSDictionary<CBUUID *, NSData *> *data =
        [advertisementData valueForKey:CBAdvertisementDataServiceDataKey];
    if (data) {
        //This is technically "00004E84-0000-1000-8000-00805F9B34FB",
        // which is wrong in the eyes of the spec.
        CBUUID *uuid = [CBUUID UUIDWithString:@"4E84"];
        //This will return the advertised data
        NSLog(@"Data: %@", [data objectForKey:uuid]);
    }
}

So bear this in mind if you intend to place service data in your Android advertisements for other devices to retrieve. It is best, if possible, to only use service data with true SIG-assigned numbers that match the 16-bit format.

On the flip side, iOS currently does not support advertising service data. iOS applications can read service data from other devices, but they cannot include it in advertisements they produce. So this issue only exists when advertising from Android.

Here are the key things to remember:

  1. There is no such thing as a 16-bit UUID
  2. iOS "overflow" is a bad place for interoperability
  3. Android advertised service data mangles 128-bit UUIDs

If you’ve been frustrated in the past with Bluetooth LE advertisements on either platform, I’m hopeful that you’ve picked up at least one or two things to calm your nerves. If you haven’t yet embarked down the BLE journey, perhaps this information will save you from some future pain.