蓝牙

该文档描述如何使用蓝牙蓝牙套接字以及蓝牙低功耗 API 与蓝牙设备和低功耗蓝牙设备通信。

有关蓝牙的背景信息,请参见官方蓝牙规范

清单文件的要求

使用蓝牙的 Chrome 应用需要在清单文件中添加 bluetooth 项,如果适用的话还需要指定您希望实现的配置文件、协议或服务 UUID,还有您是否希望通过套接字和/或低能耗 API 实现它们。

例如套接字实现:

"bluetooth": {
  "uuids": [ "1105", "1106" ],
  "socket": true
}

低能耗实现:

"bluetooth": {
  "uuids": [ "180D", "1809", "180F" ],
  "low_energy": true
}

如果只要访问适配器状态、发现附近的设备、获取设备的基本信息,只需要指定 bluetooth。

"bluetooth": { }

适配器信息

获取适配器状态

使用 bluetooth.getAdapterState 方法获取蓝牙适配器的状态:

chrome.bluetooth.getAdapterState(function(adapter) {
  console.log("适配器 " + adapter.address + ":" + adapter.name);
});

适配器通知

适配器状态更改时会产生 bluetooth.onAdapterStateChanged 事件。例如,该事件可以用来确定适配器天线是否开启。

var powered = false;
chrome.bluetooth.getAdapterState(function(adapter) {
  powered = adapter.powered;
});

chrome.bluetooth.onAdapterStateChanged.addListener(
  function(adapter) {
    if (adapter.powered != powered) {
      powered = adapter.powered;
      if (powered) {
        console.log("适配器天线打开");
      } else {
        console.log("适配器天线关闭");
      }
    }
  });

设备信息

列举已知设备

使用 ($ref:bluetooth.getDevices) 方法获取蓝牙适配器已知的设备列表:

chrome.bluetooth.getDevices(function(devices) {
  for (var i = 0; i < devices.length; i++) {
    console.log(devices[i].address);
  }
});

返回的所有设备包括已配对的设备和最近发现的设备,该方法不会开始发现新设备的操作(参见发现附近的设备)。

接收设备通知

您不应该反复调用 bluetooth.getDevices,而应该使用 bluetooth.onDeviceAddedbluetooth.onDeviceChangedbluetooth.onDeviceRemoved 事件接收通知。

每当设备被适配器发现或连接到适配器时都会产生 bluetooth.onDeviceAdded 事件。

chrome.bluetooth.onDeviceAdded.addListener(function(device) {
  console.log(device.address);
});

为该事件添加监听器不会开始发现新设备的操作(参见发现附近的设备)。

设备更改,包括之前发现的设备进行配对,都会通过 bluetooth.onDeviceChanged 事件通知。

chrome.bluetooth.onDeviceChanged.addListener(function(device) {
  console.log(device.address);
});

最后每当配对的设备从系统中移除或发现的设备最近不在范围内时,就会产生 bluetooth.onDeviceRemoved 事件。

chrome.bluetooth.onDeviceRemoved.addListener(function(device) {
  console.log(device.address);
});

发现附近的设备

使用 bluetooth.startDiscovery 方法开始发现附近的设备。发现操作会消耗很多资源,所以完成后您应该调用 bluetooth.stopDiscovery

您的应用需要发现附近的设备时就可以调用 bluetooth.startDiscovery,不需要根据 bluetooth.AdapterStatediscovering 属性决定是否调用该方法。即使另一个应用也在发现附近的设备,调用也会成功,并确保另一个应用停止时适配器继续进行发现操作。

每一个新发现设备的有关信息都通过 bluetooth.onDeviceAdded 事件接收,最近已经发现的设备或之前已经配对或连接的设备则不会产生该事件。您应该调用 bluetooth.getDevices 获取当前设备的信息,并使用 bluetooth.onDeviceChanged 事件接收发现操作中设备信息的更改。

示例:

var device_names = {};
var updateDeviceName = function(device) {
  device_names[device.address] = device.name;
};
var removeDeviceName = function(device) {
  delete device_names[device.address];
}

// 添加监听器接收新发现的设备和已知设备的更新。
chrome.bluetooth.onDeviceAdded.addListener(updateDeviceName);
chrome.bluetooth.onDeviceChanged.addListener(updateDeviceName);
chrome.bluetooth.onDeviceRemoved.addListener(removeDeviceName);

// 设置好监听器后,获取上一次发现会话中找到的
// 设备列表、当前活动的设备以及已配对的设备。
chrome.bluetooth.getDevices(function(devices) {
  for (var i = 0; i < devices.length; i++) {
    updateDeviceName(devices[i]);
  }
});

// 现在开始发现过程。
chrome.bluetooth.startDiscovery(function() {
  // 30 秒后停止发现。
  setTimeout(function() {
    chrome.bluetooth.stopDiscovery(function() {});
  }, 30000);
});

如果用户关闭了蓝牙天线,所有发现会话都会结束,天线再次开启时也不会自动恢复。如果这种情况会影响您的应用,您应该监听 chrome.bluetooth.onAdapterStateChanged 事件。如果 discovering 属性更改为 false,您的应用需要再次调用 chrome.bluetooth.startDiscovery 恢复。请注意发现操作会消耗很多资源。

识别设备

有多种方式可以识别 bluetooth.getDevices 和相关事件返回的设备。

如果设备支持蓝牙设备 ID 规范,Device 对象中会包含该规范定义个几个字段。例如:

chrome.bluetooth.getDevices(function(devices) {
  for (var i = 0; i < devices.length; i++) {
    if (devices[0].vendorIdSource != undefined) {
      console.log(devices[0].address + ' = ' +
                  devices[0].vendorIdSource + ':' +
                  devices[0].vendorId.toString(16) + ':' +
                  devices[0].productId.toString(16) + ':' +
                  devices[0].deviceId.toString(16));
    }
  }
});

设备 ID 规范通常足以识别同一厂商特定型号甚至特定版本的设备。如果不存在,您必须依赖设备类型的有关信息,可能再加上 address 中的制造商前缀。

大部分蓝牙设备都会提供设备类型信息,以基带的分配编号文档中描述的位域格式表示,可以通过 deviceClass 属性获取。

chrome.bluetooth.getDevices(function(devices) {
  for (var i = 0; i < devices.length; i++) {
    if (devices[0].vendorIdSource != undefined) {
      console.log(devices[0].address + ' = ' +
                  devices[0].deviceClass.toString(16));
    }
  }
});

分析该字段比较复杂,所以对大部分常见的设备类型,Chrome 浏览器都会为您处理,并设置 type 字段。然而如果它不可用或不能满足您的需要,您就需要自己分析deviceClass

chrome.bluetooth.getDevices(function(devices) {
  for (var i = 0; i < devices.length; i++) {
    if (devices[0].vendorIdSource != undefined) {
      console.log(devices[0].address + ' = ' + devices[0].type);
    }
  }
});

使用 RFCOMM 和 L2CAP

Chrome 应用可以连接到支持 RFCOMM 或 L2CAP 服务的设备,包括市场上大部分主要的蓝牙设备。

连接到套接字

为了连接到设备,您需要这三者:建立连接的套接字,使用 bluetoothSocket.create 创建;您希望连接的设备地址;服务本身的 UUID。

建立连接之前,您应该使用 bluetooth.getDevice 或设备发现 API 确认适配器已经检测到该设备。

建立下层连接所需的信息通过设备上的 SDP 发现获取,包括使用 RFCOMM 还是 L2CAP 协议以及使用的通道或 PSM。

例如:

var uuid = '1105';
var onConnectedCallback = function() {
  if (chrome.runtime.lastError) {
    console.log("连接失败:" + chrome.runtime.lastError.message);
  } else {
    // 此处为配置文件的实现。
  }
};

chrome.bluetoothSocket.create(function(createInfo) {
  chrome.bluetoothSocket.connect(createInfo.socketId,
    device.address, uuid, onConnectedCallback);
});

保存 socketId 的值,以便之后向该套接字发送数据(bluetoothSocket.send)。

接收和发送套接字数据

接收和发送套接字数据时使用 ArrayBuffer 对象,请参考 JavaScript 类型化数组概述以及怎样在 ArrayBuffer 和字符串之间转换的教程了解 ArrayBuffer 的有关信息。

使用 bluetoothSocket.send 发送 arrayBuffer 中的数据:

chrome.bluetoothSocket.send(socketId, arrayBuffer, function(bytes_sent) {
  if (chrome.runtime.lastError) {
    console.log("发送失败:" + chrome.runtime.lastError.message);
  } else {
    console.log("发送了 " + bytes_sent + " 字节")
  }
})

与发送数据的方法不同,接收数据是通过 bluetoothSocket.onReceive 事件进行的。套接字创建时不处于暂停状态(参见 bluetoothSocket.setPaused),所以该事件的监听器通常在 bluetoothSocket.createbluetoothSocket.connect 之间添加。

chrome.bluetoothSocket.onRecieve.addListener(function(receiveInfo) {
  if (receiveInfo.socketId != socketId)
    return;
  // receiveInfo.data 为 ArrayBuffer 对象。
});

接收套接字错误,处理连接断开的情况

bluetoothSocket.onReceiveError 事件上添加监听器,接收套接字错误的通知,包括连接断开。

chrome.bluetoothSocket.onReceiveError.addListener(function(errorInfo) {
  // errorInfo.error 中包含原因。
  console.log(errorInfo.errorMessage);
});

断开连接套接字

使用 bluetoothSocket.disconnect 断开套接字的连接。

chrome.bluetoothSocket.disconnect(socketId);

发布服务

除了向设备发起传出连接外,Chrome 应用还可以发布服务,让支持 RFCOMM 或 L2CAP 的设备使用。

监听套接字

支持两种类型的发布服务,RFCOMM 是最常见的,覆盖了大部分设备和配置文件:

var uuid = '1105';
chrome.bluetoothSocket.create(function(createInfo) {
  chrome.bluetoothSocket.listenUsingRfcomm(createInfo.socketId,
    uuid, onListenCallback);
});

L2CAP 是另一种方式,覆盖了其他设备类型以及制造商特定的用途,例如固件上传。

var uuid = '0b87367c-f188-47cd-bc20-a5f4f70973c6';
chrome.bluetoothSocket.create(function(createInfo) {
  chrome.bluetoothSocket.listenUsingL2cap(createInfo.socketId,
    uuid, onListenCallback);
});

这两种方式都能指定可选的 bluetoothSocket.ListenOptions,分配某个通道或 PSM。回调函数通过 chrome.runtime.lastError 报告错误,否则操作成功。请保存 socketId 的值,以便之后接受该套接字的连接(bluetoothSocket.onAccept)。

接受客户端连接

客户端连接通过 bluetoothSocket.onAccept 事件接受并传递到您的应用中:

chrome.bluetoothSocket.onAccept.addListener(function(acceptInfo) {
  if (info.socketId != serverSocketId)
    return;

  // 握手
  chrome.bluetoothSocket.send(acceptInfo.clientSocketId,
    data, onSendCallback);

  // 接受的套接字一开始处于暂停状态,
  // 首先设置 onReceive 监听器。
  chrome.bluetoothSocket.onReceive.addListener(onReceive);
  chrome.bluetoothSocket.setPaused(false);
});

停止接受客户端连接

使用 bluetoothSocket.disconnect 停止接受客户端连接,取消服务的发布。

chrome.bluetoothSocket.disconnect(serverSocketId);

与低耗能设备交互

蓝牙低耗能(或蓝牙智能)是一种旨在降低功耗的无线技术,蓝牙低耗能 API 允许应用程序实现外设 LE 连接的中心角色。以下几节描述如何发现、连接以及和蓝牙低能耗外设交互。

发现并连接到外设

与传统蓝牙设备类似,LE 外设也可以使用发现附近的设备中描述的方法发现。LE 设备发送一种称为“通知数据“(Advertising Data)的数据包使自身可以被发现,此时设备处于通知模式。通知数据可能包含设备上可用的服务 UUID,如果存在的话,这些 UUID 可以通过对应 bluetooth.Device 对象的 uuids 属性访问。

发现设备后,可以调用 bluetoothLowEnergy.connect 连接到 LE 设备,应用程序就能与它的服务交互了:

chrome.bluetooth.onDeviceAdded.addListener(function(device) {
  var uuid = '0000180d-0000-1000-8000-00805f9b34fb';
  if (!device.uuids || device.uuids.indexOf(uuid) < 0)
    return;

  // 设备支持指定 UUID 的服务。
  chrome.bluetoothLowEnergy.connect(device.address, function () {
    if (chrome.runtime.lastError) {
      console.log('无法连接:' + chrome.runtime.lastError.message);
      return;
    }

    // 已连接!与设备交互……
    ...
  });
});

连接完成后,对应 bluetooth.Device 对象的 connected 属性就变为 true。调用 bluetoothLowEnergy.connect 使应用程序占有设备的物理连接。还未调用 bluetoothLowEnergy.connect 时设备的物理连接也可以存在(例如由于其他应用程序),在这种情况下,尽管您的应用程序仍然可以与设备的服务交互,它还是应该调用 bluetoothLowEnergy.connect 以免其他应用程序断开物理连接。

一旦您的应用程序不再需要连接,只要调用 bluetoothLowEnergy.disconnect 就能取消对连接的占有:

chrome.bluetoothLowEnergy.disconnect(deviceAddress);

注意,该操作不一定会撤销设备的物理连接,可能还有其他应用程序拥有设备的活动连接。有时候设备还可能因为应用程序无法控制的原因(例如设备消失或者用户通过操作系统的实用程序手动断开连接)而断开连接。您的应用程序应该监听 bluetooth.onDeviceChanged 事件,获得连接更改的通知,并在必要时重新连接。

连接完成后,运行 Chrome 浏览器的设备就处于中心角色,而远程设备则处于外设角色。在这一刻,您的应用程序就能通过下面描述的方法与设备上的服务交互。注:目前这些 API 还不支持以 LE 外设的角色表现,应用只能实现中心角色。

服务、特征和描述符

蓝牙低耗能设备基于一种简单的请求—响应协议,称为属性协议(Attribute Protocol,ATT)。通过 ATT,中心设备遵循一种特殊的蓝牙配置文件,称为通用属性配置文件(Gereric Attribute Profile,GATT),与外围设备上的属性交互。GATT 定义了以下高层概念:

  • 服务:GATT 服务代表了一系列数据及其相关联的行为,以便实现设备的某种功能。例如,心率监视器通常包含至少一种“心率服务”。GATT 服务的有关信息包含在 bluetoothLowEnergy.Service 对象中。
  • 特征:GATT 特征是一种构成 GATT 服务的基本数据元素,包含值以及定义如何访问值的属性。例如,“心率服务”有“心率测试”特征,用于获取用户的心率值。GATT 特征的有关信息包含在 bluetoothLowEnergy.Characteristic 对象中。
  • 描述符:GATT 特征描述符包含与特征有关的更多信息,GATT 特征描述符的有关信息包含在 bluetoothLowEnergy.Descriptor 对象中。

蓝牙低耗能 API 允许应用程序获取设备服务、特征和描述符的有关信息,只要分别调用 bluetoothLowEnergy.getServicesbluetoothLowEnergy.getCharacteristicsbluetoothLowEnergy.getDescriptors 即可。应用可以根据 uuid 字段是否匹配期望的 GATT UUID 过滤服务、特征和描述符。

chrome.bluetoothLowEnergy.getServices(deviceAddress, function(services) {
  ...
  for (var i = 0; i < services.length; i++) {
    if (services[i].uuid == HEART_RATE_SERVICE_UUID) {
      heartRateService = services[i];
      break;
    }
  }
  ...
});

通过该 API 访问的每一项服务、特征和描述符都有唯一的实例标识符,可以通过 instanceId 字段获取。实例标识符可用于标志 GATT 对象,并对它进行具体的操作:

chrome.bluetoothLowEnergy.getCharacteristics(heartRateService.instanceId,
                                             function(chracteristics) {
  ...
  for (var i = 0; i < characteristics.length; i++) {
    if (characteristics[i].uuid == HEART_RATE_MEASUREMENT_UUID) {
      measurementChar = characteristics[i];
      break;
    }
  }
  ...
  chrome.bluetoothLowEnergy.getDescriptors(measurementChar.instanceId,
                                           function(descriptors) {
    ...
  });
});

服务事件

设备连接后,Chrome 浏览器会发现它的服务。服务发现和移除时应用程序分别会接收到 bluetoothLowEnergy.onServiceAddedbluetoothLowEnergy.onServiceRemoved 事件:

  var initializeService = function(service) {
    if (!service) {
      console.log('没有选定的服务!');
      // 重置用户界面等。
      ...
      return;
    }

    myService = service;

    // 获取所有特征和描述符,并启动应用。
    ...
  };

  chrome.bluetoothLowEnergy.onServiceAdded.addListener(function(service) {
    if (service.uuid == MY_SERVICE_UUID)
      initializeService(service);
  });

  chrome.bluetoothLowEnergy.onServiceRemoved.addListener(function(service) {
    if (service.instanceId == myService.instanceId)
      initializeService(null);
  });

Chrome 浏览器会在后台发现服务的所有特征和描述符,也就是说 bluetoothLowEnergy.onServiceAdded 事件发送给应用时,服务的一些特征可能还没有发现。每一次通过发现操作添加特征或描述符时,应用都会收到 bluetoothLowEnergy.onServiceChanged 事件。如果从远程设备收到通知,告知 Chrome 浏览器服务定义本身已更改的话也会发送该事件。

  chrome.bluetoothLowEnergy.onServiceChanged.addListener(function(service) {
    if (service.instanceId != myService.instanceId)
      return;

    // 获取特征。
    chrome.bluetoothLowEnergy.getCharacteristics(service.instanceId,
                                                 function(result) {
      ...
      if (result.length == 0) {
        console.log('特征还未发现。');
        return;
      }

      for (var i = 0; i < result.length; i++) {
        if (result[i].uuid == MY_CHARACTERISTIC_UUID) {
          myChar = result[i];
          break;
        }

        if (!myChar) {
          console.log('相关的特征还未发现。');
          return;
        }
        ...
      }
    });
  });

读取和写入特征值

GATT 特征编码服务的某一方面。中心应用通过对特征值的操作读取、修改外设服务的状态,并根据它作出不同的反应。特征值为字节序列,其含义由定义某一特征的高层规范指定。例如,心率测试特征的值表示用户的心率以及消耗的热量,而体传感器位置特征则表示心率传感器应该穿戴在身体的哪个部位。

Chrome 浏览器提供了 bluetoothLowEnergy.readCharacteristicValue 方法读取特征值:

chrome.bluetoothLowEnergy.readCharacteristicValue(chrc.instanceId,
                                                  function(result) {
  if (chrome.runtime.lastError) {
    console.log('无法读取值:' + chrome.runtime.lastError.message);
    return;
  }

  var bytes = new Uint8Array(result.value);

  // 对字节进行处理。
  ...
});

一些特征是可写的,尤其是表现为“控制点”的那些,值的写入有副作用。例如,心率控制点特征用于告知心率传感器重置消耗的总热量,只支持写入。为了实现这一点,Chrome 浏览器提供了 bluetoothLowEnergy.writeCharacteristicValue 方法:

var myBytes = new Uint8Array([ ... ]);
chrome.bluetoothLowEnergy.writeCharacteristicValue(chrc.instanceId,
                                                   myBytes.buffer,
                                                   function() {
  if (chrome.runtime.lastError) {
    console.log('无法写入值。' +
                chrome.runtime.lastError.message);
    return;
  }

  // 值已经写入。
});

特征值的描述符与之类似,可以是可读和/或可写的。Chrome 浏览器提供了 bluetoothLowEnergy.readDescriptorValuebluetoothLowEnergy.writeDescriptorValue 方法读取和写入描述符的值。

要确认特征是否支持读或写,应用程序可以检查 bluetoothLowEnergy.Characteristic 对象的 properties 字段。尽管该字段不包含访问值的安全要求相关信息,但它确实总体描述了特征支持的值操作。

处理值通知

一些特征可能会使用通知或指示告知它的值。例如,心率测试特征既不可读,也不可写,但是会定期发送当前值的更新。应用程序可以使用 bluetoothLowEnergy.onCharacteristicValueChanged 事件监听这些通知。

  chrome.bluetoothLowEnergy.onCharacteristicValueChanged.addListener(
      function(chrc) {
    if (chrc.instanceId != myCharId)
      return;

    var bytes = new Uint8Array(chrc.value);

    // 对字节进行处理。
    ...
  });

即使特征支持通知/指示,默认情况下并没有启用。应用程序应该调用 bluetoothLowEnergy.startCharacteristicNotificationsbluetoothLowEnergy.stopCharacteristicNotifications 方法开始或停止接收 bluetoothLowEnergy.onCharacteristicValueChanged 事件。

  // 开始接收特征值通知。
  var notifying = false;
  chrome.bluetoothLowEnergy.startCharacteristicNotifications(chrc.instanceId,
                                                             function() {
    if (chrome.runtime.lastError) {
      console.log('无法启用通知:' +
                  chrome.runtime.lastError.message);
      return;
    }

    notifying = true;
  });

  ...

  // 不再希望收到该特征的通知。
  if (notifying) {
    chrome.bluetoothLowEnergy.stopCharacteristicNotifications(
        chrc.instanceId);
  }

一旦启用通知,每次从特征接收到通知或指示时,应用程序都会接收到 bluetoothLowEnergy.onCharacteristicValueChanged。如果特征支持读取,成功调用 bluetoothLowEnergy.readCharacteristicValue 后也会发送该事件。这样就能使应用统一值更新的控制流程,无论是通过读取请求还是通知触发:

  chrome.bluetoothLowEnergy.onCharacteristicValueChanged.addListener(
      function(chrc) {
    // 处理值。
    ...
  });

  chrome.bluetoothLowEnergy.startCharacteristicNotifications(chrc.instanceId,
                                                             function() {
    // 通知开始,读取初始值。
    chrome.bluetoothLowEnergy.readCharacteristicValue(chrc.instanceId,
                                                      function(result) {
      ...
      // 不需要做什么,因为 onCharacteristicValueChanged 会处理。
    });
  });

如果特征支持通知,它的 properties 字段会包含 "notify""indicate" 属性。

注意:如果特征支持通知/指示,它会包含“客户端特征配置”描述符,用于启用/禁用通知。Chrome 浏览器不允许应用写入该描述符,应用应该使用 bluetoothLowEnergy.startCharacteristicNotificationsbluetoothLowEnergy.stopCharacteristicNotifications 方法控制通知行为。