Web Bluetooth Scanning

Draft Community Group Report,

More details about this document
This version:
https://webbluetoothcg.github.io/web-bluetooth/scanning.html
Issue Tracking:
GitHub
Inline In Spec
Editor:
See contributors on GitHub
Participate:
Join the W3C Community Group
Fix the text through GitHub
public-web-bluetooth@w3.org (archives)
IRC: #web-bluetooth on W3C’s IRC

Abstract

This document describes an API to scan for nearby Bluetooth Low Energy devices in real time.

Status of this document

This specification was published by the Web Bluetooth Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

Changes to this document may be tracked at https://github.com/WebBluetoothCG/web-bluetooth/commits/gh-pages.

If you wish to make comments regarding this document, please send them to public-web-bluetooth@w3.org (subscribe, archives).

1. Introduction

This section is non-normative.

Bluetooth Low Energy (BLE) allows devices to broadcast advertisements to nearby observers. These advertisements can contain small amounts of data of a variety of types defined in [BLUETOOTH-SUPPLEMENT6].

For example, a beacon might announce that it’s next to a particular museum exhibit and is advertising with 1mW of power, which would let nearby observers know their approximate distance to that exhibit.

This specification extends [web-bluetooth] to allow websites to receive these advertisements from nearby BLE devices, with the user’s permission.

1.1. Examples

To discover what iBeacons are nearby and measure their distance, a website could use code like the following:

function recordNearbyBeacon(major, minor, pathLossVs1m) { ... }
navigator.bluetooth.requestLEScan({
  filters: [{manufacturerData: {0x004C: {dataPrefix: new Uint8Array([
    0x02, 0x15, // iBeacon identifier.
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15  // My beacon UUID.
  ])}}}],
  keepRepeatedDevices: true
}).then(() => {
  navigator.bluetooth.addEventListener('advertisementreceived', event => {
    let appleData = event.manufacturerData.get(0x004C);
    if (appleData.byteLength != 23) {
      // Isn’t an iBeacon.
      return;
    }
    let major = appleData.getUint16(18, false);
    let minor = appleData.getUint16(20, false);
    let txPowerAt1m = -appleData.getInt8(22);
    let pathLossVs1m = txPowerAt1m - event.rssi;

    recordNearbyBeacon(major, minor, pathLossVs1m);
  });
})

2. Privacy considerations

This section is non-normative.

Actively scanning for advertisements broadcasts a device address of the scanning device. If the UA’s Bluetooth system supports the privacy feature, this address rotates periodically, which prevents remote radios from tracking the user’s device. However, if the UA’s Bluetooth system does not support the privacy feature, this address is a stable unique identifier that’s difficult to change. To mitigate this, UAs should either use passive scanning, use the privacy feature in an observer, or warn the user that nearby devices will learn the identity of their device.

The ambient advertisements in a user’s area are unlikely to directly include GPS coordinates, but are likely to contain unique identifiers that could be manually correlated with particular physical locations or with particular other people. Given that, the user needs to give permission before a website gets access to nearby advertisements.

If a user has already given a site permission to know their location, it might be ok to implicitly grant access to BLE advertisements. However, BLE advertisements give away strictly less location information than full [geolocation-api] access, so UAs should allow users to grant that intermediate level of access.

BLE advertisements are usually fully public, since they’re broadcast unencrypted on 2.4GHz radio waves. However, it’s possible that a user would have a device broadcast private information in a radio-shielded room. This is probably an inappropriate use for BLE advertisements, but might argue for requiring explicit permission to scan, rather than inferring it from having permission to get a geolocation.

3. Security considerations

This section is non-normative.

Because this API doesn’t write anything, there are few if any security implications. A device in a shielded room might broadcast security-sensitive information, but we don’t have any actual attack scenarios for that.

4. Scanning for BLE advertisements

dictionary BluetoothLEScanOptions {
  sequence<BluetoothLEScanFilterInit> filters;
  boolean keepRepeatedDevices = false;
  boolean acceptAllAdvertisements = false;
};

partial interface Bluetooth {
  [SecureContext]
  Promise<BluetoothLEScan> requestLEScan(optional BluetoothLEScanOptions options = {});
};
NOTE: requestLEScan() summary

navigator.bluetooth.requestLEScan(options) starts scanning for BLE advertisements, asking the user for permission if they haven’t yet granted it.

Because this could show a prompt, it requires a secure context. Additionally, UAs are likely to require a transient user activation on its relevant global object when requestLEScan is called.

Advertising events that match a BluetoothLEScanFilter in an active BluetoothLEScan cause advertisementreceived events to be dispatched to the sending BluetoothDevice. A filter matches if the advertisement includes data equal to each present member. Usually, you’ll only include one member in each filter.

Normally scans will discard the second and subsequent advertisements from a single device to save power. If you need to receive them, set keepRepeatedDevices to true. Note that setting keepRepeatedDevices to false doesn’t guarantee you won’t get redundant events; it just allows the UA to save power by omitting them.

In the rare case that you want to receive every advertisement without filtering them, use the acceptAllAdvertisements field.

The requestLEScan(options) method, when invoked, MUST return a new promise promise and run the following steps in parallel:

  1. If this's relevant global object's associated Document is not allowed to use the policy-controlled feature named "bluetooth", reject promise with a SecurityError and abort these steps.
  2. If options.acceptAllAdvertisements is true, and options.filters is present, reject promise with a TypeError and abort these steps.

    Note: There’s no need to include filters if all advertisements are being accepted.

  3. If options.acceptAllAdvertisements is false, and options.filters is either absent or empty, reject promise with a TypeError and abort these steps.

    Note: An empty set of filters wouldn’t return any advertisements.

  4. Let filters be Array.prototype.map.call(options.filters, filter=>new BluetoothLEScanFilter(filter)) if options.filters is present, or an empty FrozenArray otherwise. If this throws an exception, reject promise with that exception and abort these steps.
  5. Request permission to use
    {
      name: "bluetooth-le-scan",
      filters: options.filters,
      keepRepeatedDevices: options.keepRepeatedDevices,
      acceptAllAdvertisements: options.acceptAllAdvertisements,
    }
    

    Note: This may require that this algorithm has a transient activation on its relevant global object when triggered.

  6. If the result is "denied", reject promise with a NotAllowedError and abort these steps.
  7. Let scan be a new BluetoothLEScan instance whose fields are initialized as in the following table:
    Field Initial value
    filters filters
    keepRepeatedDevices options.keepRepeatedDevices
    acceptAllAdvertisements options.acceptAllAdvertisements
    active true
  8. Add scan to navigator.bluetooth.[[activeScans]].
  9. Ensure the UA is scanning for BLE advertisements in a mode that will receive at least all advertisements matching any scan in any [[activeScans]] set in the whole UA.

    Find wording that allows the UA to limit its scan to only certain periods of time, to save power.

  10. If the UA fails to start scanning, remove scan from navigator.bluetooth.[[activeScans]], reject promise with one of the following errors, and abort these steps:
    The UA doesn’t support scanning for advertisements
    NotSupportedError
    Bluetooth is turned off
    InvalidStateError
    Other reasons
    UnknownError
  11. Resolve promise with scan.

4.1. Controlling a BLE scan

[Exposed=Window, SecureContext]
interface BluetoothDataFilter {
  constructor(optional BluetoothDataFilterInit init = {});
  readonly attribute ArrayBuffer dataPrefix;
  readonly attribute ArrayBuffer mask;
};

[Exposed=Window, SecureContext]
interface BluetoothManufacturerDataFilter {
  constructor(optional object init);
  readonly maplike<unsigned short, BluetoothDataFilter>;
};

[Exposed=Window, SecureContext]
interface BluetoothServiceDataFilter {
  constructor(optional object init);
  readonly maplike<UUID, BluetoothDataFilter>;
};

[Exposed=Window, SecureContext]
interface BluetoothLEScanFilter {
  constructor(optional BluetoothLEScanFilterInit init = {});
  readonly attribute DOMString? name;
  readonly attribute DOMString? namePrefix;
  readonly attribute FrozenArray<UUID> services;
  readonly attribute BluetoothManufacturerDataFilter manufacturerData;
  readonly attribute BluetoothServiceDataFilter serviceData;
};

[Exposed=Window, SecureContext]
interface BluetoothLEScan {
  readonly attribute FrozenArray<BluetoothLEScanFilter> filters;
  readonly attribute boolean keepRepeatedDevices;
  readonly attribute boolean acceptAllAdvertisements;

  readonly attribute boolean active;

  undefined stop();
};
NOTE: BluetoothLEScan members

BluetoothLEScan.stop() stops a previously-requested scan. Sites should do this as soon as possible to avoid wasting power.

The BluetoothLEScanFilter(init) constructor, when invoked MUST perform the following steps:

  1. Initialize all nullable fields to null.
  2. If no member of init is present, throw a TypeError.

    Note: A filter can’t implicitly allow all advertisements. Use acceptAllAdvertisements to explicitly do it.

  3. Initialize this.manufacturerData as new BluetoothManufacturerDataFilter(init.manufacturerData).
  4. Initialize this.serviceData as new BluetoothServiceDataFilter(init.serviceData).
  5. Initialize this.services as Array.prototype.map.call(init.services, service=>BluetoothUUID.getService(service)) if init.services is present, or an empty FrozenArray otherwise.
  6. For each other present member in init, set this’s attribute with a matching identifier to the value of the member.
  7. If any of the above calls threw an exception, propagate that exception from this constructor.

The stop() method, when invoked, MUST perform the following steps:

  1. Set this.active to false.
  2. Remove this from navigator.bluetooth.[[activeScans]].
  3. The UA SHOULD reconfigure or stop its BLE scan to save power while still receiving any advertisements that match any scan in any [[activeScans]] set in the whole UA.

The BluetoothManufacturerDataFilter(init) constructor, when invoked, MUST perform the following steps:

  1. If init is not present or init.[[OwnPropertyKeys]]() is empty, then this has no map entries. Abort these steps.
  2. Let canonicalInit be the manufacturerData field of the result of canonicalizing {manufacturerData: init}. If this throws an exception, propagate that exception from this constructor and abort these steps.
  3. this’s map entries map from each key in canonicalInit.[[OwnPropertyKeys]](), parsed as a base-10 integer, to its value of new BluetoothDataFilter(canonicalInit[key]).

The BluetoothServiceDataFilter(init) constructor, when invoked, MUST perform the following steps:

  1. If init is not present or init.[[OwnPropertyKeys]]() is empty, then this has no map entries. Abort these steps.
  2. Let canonicalInit be the serviceData field of the result of canonicalizing {serviceData: init}. If this throws an exception, propagate that exception from this constructor and abort these steps.
  3. this’s map entries map from each key in canonicalInit.[[OwnPropertyKeys]](), to its value of new BluetoothDataFilter(canonicalInit[key]).

The BluetoothDataFilter(init) constructor, when invoked, MUST perform the following steps:

  1. Let canonicalInit be the result of canonicalizing init. If this throws an exception, propagate that exception from this constructor and abort these steps.
  2. Initialize this.dataPrefix as a read only ArrayBuffer whose contents are a copy of the bytes held by canonicalInit.dataPrefix.
  3. Initialize this.mask as a read only ArrayBuffer whose contents are a copy of the bytes held by canonicalInit.mask.

4.2. Handling Document Loss of Full Activity

Operations that initiate a scan for Bluetooth devices may only run in a fully active document. When full activity is lost, scanning operations for that document need to be aborted.

When the user agent determines that a associated Document of the current settings object's relevant global object is no longer fully active, it must run these steps:
  1. For each activeScan in navigator.bluetooth.[[activeScans]], perform the following steps:

    1. Call activeScan.stop().

4.3. Permission to scan

The "bluetooth-le-scan" powerful feature’s permission-related algorithms and types are defined as follows:

permission descriptor type
dictionary BluetoothLEScanPermissionDescriptor : PermissionDescriptor {
  // These match BluetoothLEScanOptions.
  sequence<BluetoothLEScanFilterInit> filters;
  boolean keepRepeatedDevices = false;
  boolean acceptAllAdvertisements = false;
};
permission result type
[Exposed=Window, SecureContext]
interface BluetoothLEScanPermissionResult : PermissionStatus {
  attribute FrozenArray<BluetoothLEScan> scans;
};
permission query algorithm

Given a BluetoothLEScanPermissionDescriptor descriptor and a BluetoothLEScanPermissionResult result:

  1. Update result.state to descriptor’s permission state.
  2. If result.state is "denied", set result.scans to an empty FrozenArray and abort these steps.
  3. Update result.scans to a new FrozenArray containing the elements of navigator.bluetooth.[[activeScans]].

    Consider filtering the result to active scans that match the fields of the descriptor.

permission revocation algorithm
  1. For each activeScan in navigator.bluetooth.[[activeScans]]:
    1. If the permission state of
      {
        name: "bluetooth-le-scan",
        filters: activeScan.filters,
        keepRepeatedDevices: activeScan.keepRepeatedDevices
      }
      

      is not "granted", call activeScan.stop().

5. Event handling

5.1. Responding to advertising events

When the UA receives an advertising event event (consisting of an advertising packet and an optional scan response), it MUST run the following steps:

  1. Let device be the Bluetooth device that sent the advertising event.
  2. For each Bluetooth instance bluetooth in the UA, queue a task on bluetooth’s relevant settings object’s responsible event loop to do the following sub-steps:
    1. Let scans be the set of BluetoothLEScans in bluetooth.[[activeScans]] that match event.
    2. If scans is empty, abort these sub-steps.
    3. Note: the user’s permission to scan likely indicates that they intend newly-discovered devices to appear in "bluetooth"’s extra permission data, but possibly with mayUseGATT set to false.
    4. Get the BluetoothDevice representing device inside bluetooth, and let deviceObj be the result.
    5. Add each BluetoothLEScan in scans to deviceObj.[[returnedFromScans]].
    6. Fire an advertisementreceived event for event at deviceObj.

An advertising event event matches a BluetoothLEScan scan if the following steps return match:

  1. scan.acceptAllAdvertisements is false and event doesn’t match any filter in scan.filters, return no match.
  2. If scan.keepRepeatedDevices is false, there is a BluetoothDevice device that represents the same Bluetooth device as the one that sent event, and device.[[returnedFromScans]] includes scan, the UA MAY return no match.
  3. Return match.

An advertising event event matches a BluetoothLEScanFilter filter if all of the following conditions hold:

  • If filter.name is non-null, event has a Local Name equal to filter.name.

    Note: A Shortened Local Name can match a name filter.

  • If filter.namePrefix is non-null, event has a Local Name, and filter.namePrefix is a prefix of it.
  • For each uuid in filter.services, some Service UUID in event is equal to uuid.
  • For each (id, filter) in filter.manufacturerData’s map entries, some Manufacturer Specific Data in event has a Company Identifier Code of id, and whose array of bytes matches filter.
  • For each (uuid, filter) in filter.serviceData’s map entries, some Service Data in event has a UUID whose 128-bit representation is uuid, and whose array of bytes matches filter.

6. Changes to existing interfaces

Instances of Bluetooth additionally have the following internal slots:

Internal Slot Initial Value Description (non-normative)
[[activeScans]] An empty set of BluetoothLEScan instances. The contents of this set will have active equal to true.

Instances of BluetoothDevice additionally have the following internal slots:

Internal Slot Initial Value Description (non-normative)
[[returnedFromScans]] An empty set of BluetoothLEScan objects. Used to implement keepRepeatedDevices.

7. Terminology and conventions

This specification uses a few conventions and several terms from other specifications. This section lists those and links to their primary definitions.

When an algorithm in this specification uses a name defined in this or another specification, the name MUST resolve to its initial value, ignoring any changes that have been made to the name in the current execution environment. For example, when the requestLEScan() algorithm says to call Array.prototype.map.call(options.filters, filter=>new BluetoothLEScanFilter(filter)), this MUST apply the Array.prototype.map algorithm defined in [ECMAScript] with options.filters as its this parameter and filter=>new BluetoothLEScanFilter(filter) as its callbackfn parameter, regardless of any modifications that have been made to window, Array, Array.prototype, Array.prototype.map, Function, Function.prototype, BluetoothLEScanFilter, or other objects.

[BLUETOOTH42]
  1. Architecture & Terminology Overview
    1. General Description
      1. Overview of Bluetooth Low Energy Operation (defines advertising events)
  2. Core System Package [Host volume]
    1. Generic Access Profile
      1. Profile Overview
        1. Profile Roles
          1. Roles when Operating over an LE Physical Transport
            1. Observer Role
      2. Security Aspects — LE Physical Transport
        1. Privacy Feature
          1. Privacy Feature in an Observer
  3. Core System Package [Low Energy Controller volume]
    1. Link Layer Specification
      1. General Description
        1. Device Address
      2. Air Interface Packets
        1. Advertising Channel PDU
          1. Advertising PDUs
            1. ADV_IND
            2. ADV_DIRECT_IND
            3. ADV_NONCONN_IND
            4. ADV_SCAN_IND
      3. Air Interface Protocol
        1. Non-Connected States
          1. Scanning State
            1. Passive Scanning
            2. Active Scanning
[BLUETOOTH-SUPPLEMENT6]
  1. Data Types Specification
    1. Data Types Definitions and Formats
      1. Service UUID
      2. Local Name also defines Shortened Local Name and Complete Local Name
      3. Manufacturer Specific Data
      4. Service Data

Conformance

Document conventions

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[BLUETOOTH-SUPPLEMENT6]
Supplement to the Bluetooth Core Specification Version 6. 14 July 2015. URL: https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=302735
[BLUETOOTH42]
BLUETOOTH SPECIFICATION Version 4.2. 2 December 2014. URL: https://www.bluetooth.org/DocMan/handlers/DownloadDoc.ashx?doc_id=286439
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ECMAScript]
ECMAScript Language Specification. URL: https://tc39.es/ecma262/multipage/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[PERMISSIONS]
Marcos Caceres; Mike Taylor. Permissions. URL: https://w3c.github.io/permissions/
[PERMISSIONS-POLICY-1]
Ian Clelland. Permissions Policy. URL: https://w3c.github.io/webappsec-permissions-policy/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[WEB-BLUETOOTH]
Jeffrey Yasskin. Web Bluetooth. URL: https://webbluetoothcg.github.io/web-bluetooth/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Informative References

[GEOLOCATION-API]
Andrei Popescu. Geolocation API Specification 2nd Edition. URL: https://w3c.github.io/geolocation-api/

IDL Index

dictionary BluetoothLEScanOptions {
  sequence<BluetoothLEScanFilterInit> filters;
  boolean keepRepeatedDevices = false;
  boolean acceptAllAdvertisements = false;
};

partial interface Bluetooth {
  [SecureContext]
  Promise<BluetoothLEScan> requestLEScan(optional BluetoothLEScanOptions options = {});
};

[Exposed=Window, SecureContext]
interface BluetoothDataFilter {
  constructor(optional BluetoothDataFilterInit init = {});
  readonly attribute ArrayBuffer dataPrefix;
  readonly attribute ArrayBuffer mask;
};

[Exposed=Window, SecureContext]
interface BluetoothManufacturerDataFilter {
  constructor(optional object init);
  readonly maplike<unsigned short, BluetoothDataFilter>;
};

[Exposed=Window, SecureContext]
interface BluetoothServiceDataFilter {
  constructor(optional object init);
  readonly maplike<UUID, BluetoothDataFilter>;
};

[Exposed=Window, SecureContext]
interface BluetoothLEScanFilter {
  constructor(optional BluetoothLEScanFilterInit init = {});
  readonly attribute DOMString? name;
  readonly attribute DOMString? namePrefix;
  readonly attribute FrozenArray<UUID> services;
  readonly attribute BluetoothManufacturerDataFilter manufacturerData;
  readonly attribute BluetoothServiceDataFilter serviceData;
};

[Exposed=Window, SecureContext]
interface BluetoothLEScan {
  readonly attribute FrozenArray<BluetoothLEScanFilter> filters;
  readonly attribute boolean keepRepeatedDevices;
  readonly attribute boolean acceptAllAdvertisements;

  readonly attribute boolean active;

  undefined stop();
};

dictionary BluetoothLEScanPermissionDescriptor : PermissionDescriptor {
  // These match BluetoothLEScanOptions.
  sequence<BluetoothLEScanFilterInit> filters;
  boolean keepRepeatedDevices = false;
  boolean acceptAllAdvertisements = false;
};

[Exposed=Window, SecureContext]
interface BluetoothLEScanPermissionResult : PermissionStatus {
  attribute FrozenArray<BluetoothLEScan> scans;
};

Issues Index

Find wording that allows the UA to limit its scan to only certain periods of time, to save power.
Consider filtering the result to active scans that match the fields of the descriptor.