蓝牙

本文档介绍了如何使用蓝牙 API、蓝牙套接字蓝牙低功耗 API 与蓝牙和蓝牙低功耗设备进行通信。

如需了解有关蓝牙的背景信息,请参阅官方蓝牙规范

清单要求

对于使用蓝牙的 Chrome 应用,请将 bluetooth 条目添加到清单中,并视情况指定您要实现的配置文件、协议或服务的 UUID,以及是否希望使用套接字和/或低功耗 API 来实现这些 API。

例如,对于套接字实现:

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

对于低功耗实现:

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

若要仅访问适配器状态、发现附近的设备以及获取有关设备的基本信息,只需提供条目本身即可:

"bluetooth": {}

适配器信息

获取适配器状态

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

chrome.bluetooth.getAdapterState(function(adapter) {
  console.log("Adapter " + 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("Adapter radio is on");
      } else {
        console.log("Adapter radio is off");
      }
    }
  });

设备信息

列出已知设备

要获取蓝牙适配器已知设备的列表,请使用 bluetooth.getDevices 方法:

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

会返回所有设备,包括已配对的设备和最近发现的设备。不会开始发现新设备(请参阅发现附近的设备)。

接收设备通知

您可以使用 bluetooth.onDeviceAddedbluetooth.onDeviceChangedbluetooth.onDeviceRemoved 事件来接收通知,而不是重复调用 bluetooth.getDevices

每当适配器发现设备或连接到适配器时,系统都会发送 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];
}

// Add listeners to receive newly found devices and updates
// to the previously known devices.
chrome.bluetooth.onDeviceAdded.addListener(updateDeviceName);
chrome.bluetooth.onDeviceChanged.addListener(updateDeviceName);
chrome.bluetooth.onDeviceRemoved.addListener(removeDeviceName);

// With the listeners in place, get the list of devices found in
// previous discovery sessions, or any currently active ones,
// along with paired devices.
chrome.bluetooth.getDevices(function(devices) {
  for (var i = 0; i < devices.length; i++) {
    updateDeviceName(devices[i]);
  }
});

// Now begin the discovery process.
chrome.bluetooth.startDiscovery(function() {
  // Stop discovery after 30 seconds.
  setTimeout(function() {
    chrome.bluetooth.stopDiscovery(function() {});
  }, 30000);
});

如果用户关闭蓝牙无线装置,那么当无线装置开启时,所有发现会话都将结束,并且不会自动恢复。如果这对您的应用很重要,您应观察 bluetooth.onAdapterStateChanged 事件。如果 discovering 属性更改为 false,您的应用将需要再次调用 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 验证适配器是否可以识别设备。

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

例如:

var uuid = '1105';
var onConnectedCallback = function() {
  if (chrome.runtime.lastError) {
    console.log("Connection failed: " + chrome.runtime.lastError.message);
  } else {
    // Profile implementation here.
  }
};

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

保留 socketId 的句柄,以便稍后将数据 (bluetoothSocket.send) 发送到此套接字。

从套接字接收和发送到套接字

从套接字接收数据和将数据发送到套接字会使用 ArrayBuffer 对象。如需了解 ArrayBuffers,请查看概览 JavaScript 类型数组以及教程如何将 ArrayBuffer 与 String 相互转换

如需发送 arrayBuffer 中的数据,请使用 bluetoothSocket.send

chrome.bluetoothSocket.send(socketId, arrayBuffer, function(bytes_sent) {
  if (chrome.runtime.lastError) {
    console.log("Send failed: " + chrome.runtime.lastError.message);
  } else {
    console.log("Sent " + bytes_sent + " bytes")
  }
})

与发送数据的方法相比,数据是在事件中接收的 (bluetoothSocket.onReceive。系统会在不暂停的情况下创建套接字(请参阅 bluetoothSocket.setPaused),因此通常会在 bluetoothSocket.createbluetoothSocket.connect 之间添加此事件的监听器。

chrome.bluetoothSocket.onRecieve.addListener(function(receiveInfo) {
  if (receiveInfo.socketId != socketId)
    return;
  // receiveInfo.data is an ArrayBuffer.
});

收到套接字错误并断开连接

要收到套接字错误(包括断开连接)通知,请向 bluetoothSocket.onReceiveError 事件添加一个监听器。

chrome.bluetoothSocket.onReceiveError.addListener(function(errorInfo) {
  // Cause is in 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;

  // Say hello...
  chrome.bluetoothSocket.send(acceptInfo.clientSocketId,
    data, onSendCallback);

  // Accepted sockets are initially paused,
  // set the onReceive listener first.
  chrome.bluetoothSocket.onReceive.addListener(onReceive);
  chrome.bluetoothSocket.setPaused(false);
});

停止接受客户端连接

要停止接受客户端连接并取消发布该服务,请使用 bluetoothSocket.disconnect

chrome.bluetoothSocket.disconnect(serverSocketId);

与低功耗设备交互

蓝牙低功耗(即 Bluetooth Smart)是一种旨在降低功耗的无线技术。蓝牙低功耗 API 允许应用在与外围设备之间的 LE 连接中实现核心角色。以下部分介绍了如何发现、连接到蓝牙低功耗外围设备并与之互动。

发现并连接到外围设备

与传统蓝牙设备一样,可以使用发现附近的设备中所述的方法发现 LE 外围设备。LE 设备通过发送名为“广告数据”的数据包使自身可被检测到,并且设备可称为处于“通告模式”。通告数据可能包含设备上可用服务的 UUID。如果 UUID 存在,则可以使用相应 bluetooth.Device 对象的 uuids 属性进行访问。

发现 LE 设备后,可以调用 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;

  // The device has a service with the desired UUID.
  chrome.bluetoothLowEnergy.connect(device.address, function () {
    if (chrome.runtime.lastError) {
      console.log('Failed to connect: ' + chrome.runtime.lastError.message);
      return;
    }

    // Connected! Do stuff...
    ...
  });
});

连接后,对应 bluetooth.Device 对象的 connected 属性的值将为 true。调用 bluetoothLowEnergy.connect 会在与设备的物理连接上通过应用建立声明。无需调用 bluetoothLowEnergy.connect 就可以存在与设备的物理连接(例如,由于其他应用的连接)。在这种情况下,虽然您的应用仍可以与设备的服务交互,但应始终调用 bluetoothLowEnergy.connect,防止其他应用断开该物理链接。

一旦您的应用不再需要连接,它可以通过调用 bluetoothLowEnergy.disconnect 移除对连接的声明:

chrome.bluetoothLowEnergy.disconnect(deviceAddress);

请注意,这不一定会销毁到设备的物理链接,因为可能还有其他应用与设备建立了有效连接。有时,设备可能会由于应用无法控制的原因断开连接(例如,设备消失,或用户通过操作系统的实用程序明确断开连接)。您的应用应观察 bluetooth.onDeviceChanged 事件,以便在连接发生更改时获取通知,并在必要时重新连接。

建立连接后,运行 Chrome 的设备会扮演所谓的“中心角色”,而远程设备则会扮演“外围设备角色”。此时,您的应用可以使用下一部分中所述的方法与设备上的服务进行交互。注意:这些 API 目前不支持充当 LE 外围设备;应用只能实现核心角色。

服务、特性和描述符

蓝牙低功耗基于一个名为属性协议 (ATT) 的简单请求-响应协议。借助 ATT,中央设备通过符合名为“通用属性配置文件”(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 字段获取。此实例 ID 可用于识别 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('No service selected!');
      // Reset UI, etc.
      ...
      return;
    }

    myService = service;

    // Get all the characteristics and descriptors and bootstrap the app.
    ...
  };

  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 事件。如果与外围设备的连接终止,Chrome 将移除所有相关服务,并发送 bluetoothLowEnergy.onServiceRemoved 事件。

某些外围设备可能会修改其服务,例如,服务特性可能会发生变化,或者服务可能会被完全添加和移除。Chrome 会使用 bluetoothLowEnergy.onServiceChangedbluetoothLowEnergy.onServiceAddedbluetoothLowEnergy.onServiceRemoved 事件通知应用这些变化。

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

    updateMyService(service);
  });

读取和写入特征的值

GATT 特征可对其服务的一个方面进行编码。中央应用通过对特征的值执行操作来读取、处理和修改外设服务的状态。特征值是一个字节序列,其含义由定义特定特征的高级规范定义。例如,心率测量特征的值编码的是用户的心率和消耗的卡路里总量,而身体传感器位置特性则编码的是心率传感器应该佩戴在身体中的什么位置。

Chrome 提供 bluetoothLowEnergy.readCharacteristicValue 方法读取特征的值:

chrome.bluetoothLowEnergy.readCharacteristicValue(chrc.instanceId,
                                                  function(result) {
  if (chrome.runtime.lastError) {
    console.log('Failed to read value: ' + chrome.runtime.lastError.message);
    return;
  }

  var bytes = new Uint8Array(result.value);

  // Do stuff with the bytes.
  ...
});

某些特性是可写的,尤其是那些充当“控制点”的特性,在控制点中写入值会产生副作用。例如,心率控制点特性用于指示心率传感器重置其消耗的总卡路里数,并且仅支持写入。为此,Chrome 提供了 bluetoothLowEnergy.writeCharacteristicValue 方法:

var myBytes = new Uint8Array([ ... ]);
chrome.bluetoothLowEnergy.writeCharacteristicValue(chrc.instanceId,
                                                   myBytes.buffer,
                                                   function() {
  if (chrome.runtime.lastError) {
    console.log('Failed to write value: ' +
                chrome.runtime.lastError.message);
    return;
  }

  // Value is written now.
});

特征描述符的行为方式相同,并且具有可读和/或可写性。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);

    // Do stuff with the bytes.
    ...
  });

即使某个特性支持通知/指示,系统在默认情况下也不会启用它们。应用应调用 bluetoothLowEnergy.startCharacteristicNotificationsbluetoothLowEnergy.stopCharacteristicNotifications 方法开始或停止接收 bluetoothLowEnergy.onCharacteristicValueChanged 事件。

  // Start receiving characteristic value notifications.
  var notifying = false;
  chrome.bluetoothLowEnergy.startCharacteristicNotifications(chrc.instanceId,
                                                             function() {
    if (chrome.runtime.lastError) {
      console.log('Failed to enable notifications: ' +
                  chrome.runtime.lastError.message);
      return;
    }

    notifying = true;
  });

  ...

  // No longer interested in notifications from this characteristic.
  if (notifying) {
    chrome.bluetoothLowEnergy.stopCharacteristicNotifications(
        chrc.instanceId);
  }

通知启动后,每当从特征收到通知或指示时,应用都会收到 bluetoothLowEnergy.onCharacteristicValueChanged。如果该特性支持读取,那么将在成功调用 bluetoothLowEnergy.readCharacteristicValue 后发送此事件。这样,应用就可以统一通过读取请求和通知触发的值更新的控制流:

  chrome.bluetoothLowEnergy.onCharacteristicValueChanged.addListener(
      function(chrc) {
    // Process the value.
    ...
  });

  chrome.bluetoothLowEnergy.startCharacteristicNotifications(chrc.instanceId,
                                                             function() {
    // Notifications started. Read the initial value.
    chrome.bluetoothLowEnergy.readCharacteristicValue(chrc.instanceId,
                                                      function(result) {
      ...
      // No need to do anything here since onCharacteristicValueChanged
      // will handle it.
    });
  });

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

注意:如果特征支持通知/指示,则将具有用于启用/停用通知的“客户端特征配置”描述符。Chrome 不允许应用写入此描述符。应用应改用 bluetoothLowEnergy.startCharacteristicNotificationsbluetoothLowEnergy.stopCharacteristicNotifications 方法来控制通知行为。