Web Bluetooth 与点对点毗连

打印 上一主题 下一主题

主题 901|帖子 901|积分 2703

前言

需求需要实现手持终端设备与 web 网页的点对点数据传输,不希望有服务器参与,想到了 web 的 USB 与 Bluetooth API,对 Web Bluetooth API 进行了研究。
蓝牙 GATT 底子知识

GATT(通用属性配置文件,蓝牙低功耗(BLE)中定义的一种规范)定义了怎样在蓝牙低功耗设备之间进行数据的传输和交互。它规定了蓝牙设备之间的数据格式、通讯协议以及数据的构造方式。通过 GATT,不同的蓝牙设备可以交换各种范例的数据,如传感器数据、设备状态信息等。
GATT 采用分层的构造布局,分为 Service(服务)、Characteristic(特性)、Property(属性)三层:


  • Service: 一个蓝牙设备可以有一个或多个服务,这些服务提供不同的功能,如 battery_service(电池服务)、heart_rate(心率服务)
  • Characteristic: 提供与服务相干的功能,比如 battery_service 服务的 battery_level 特性提供电池电量的数据
  • Property: 特性上的属性,用于操作特性值,比如 write、read 用于读写特性值
Service、Characteristic 都有一个 UUID 用于标识服务、特性,Service 的 UUID 格式固定为 0x0000[xxxx]-0000-1000-8000-00805F9B34FB,此中 [xxxx] 是可变部门,其余固定,比如电池服务的 UUID 为 0000180F-0000-1000-8000-00805f9b34fb,可简写为 0x180F,当自定义蓝牙 GATT 服务时定义的 UUID 需要采用雷同的格式。
在使用 Web Bluetooth API 时,我们通过服务名称或服务 UUID 来找到我们需要的蓝牙服务。
Web Bluetooth API

Web Bluetooth 全部接口构建在 Promise 之上,只支持在可信来源中使用(localhost 或 https),重要有以下接口(完整接口查看 MDN 文档):


  • Bluetooth:提供查询蓝牙可用性和请求访问设备的方法

    • getAvailability:返回用户代理的蓝牙可用性
    • requestDevice:请求蓝牙设备,返回一个 BluetoothDevice 实例;必须由用户手势触发,一般会弹出蓝牙选择器,如果没有可用的蓝牙选择器则默认选择匹配的第一个

  • BluetoothDevice:蓝牙设备相干接口,具有一个 gatt 属性是对 BluetoothRemoteGATTServer 实例的引用
  • BluetoothRemoteGATTServer:可以理解为对 gatt 服务器的引用,通过 gatt 服务器可以获取到他所拥有的 Service

    • connected:脚本实行环境是否已与设备毗连
    • connect:脚本实行环境毗连到 BluetoothDevice
    • disconnect:脚本实行环境断开与 BluetoothDevice 的毗连
    • getPrimaryService:通过服务别名或 UUID 获取到对应的 Service,是一个 BluetoothRemoteGATTService 实例
    • getPrimaryServices:获取多个 Service

  • BluetoothRemoteGATTService:对 Service 的引用

    • isPrimary:指示这是一个重要照旧次要的服务
    • getCharacteristic:获取指定 UUID 的 Characteristic 特性,是一个 BluetoothRemoteGATTCharacteristic 实例
    • getCharacteristics:获取多个特性

  • BluetoothRemoteGATTCharacteristic:对特性的引用

    • readValue:读取特性值
    • writeValueWithResponse:写入特性值

看一个简朴的示例:
  1. <!doctype html>
  2. <html lang="en">
  3.   <head>
  4.     <meta charset="UTF-8" />
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6.     <title>Document</title>
  7.   </head>
  8.   <body>
  9.     <button class="request">click me</button>
  10.     <script>
  11.       const request = window.document.querySelector('.request');
  12.       request.addEventListener('click', async function () {
  13.         const bluetooth = window.navigator.bluetooth;
  14.         try {
  15.           // 检查用户代理是否支持
  16.           const isSupport = await bluetooth.getAvailability();
  17.           if (!isSupport) {
  18.             return window.alert('用户代理不支持蓝牙请求');
  19.           }
  20.           // 请求蓝牙设备
  21.           const bluetoothDevice = await bluetooth.requestDevice({
  22.             /** 过滤器选项 */
  23.             filters: [
  24.               {
  25.                 /** 过滤拥有 battery_service | 0x1101 | 0000180D-0000-1000-8000-00805f9b34fb 服务的设备 */
  26.                 services: ['battery_service', 0x1101, '0000180D-0000-1000-8000-00805f9b34fb'],
  27.                 /** 过滤名称为 RedMi Note13Pro 的设备 */
  28.                 name: 'RedMi Note13Pro',
  29.                 /** 过滤名称前缀为 RedMi 的设备 */
  30.                 namePrefix: 'RedMi'
  31.               }
  32.             ],
  33.             /** 排除项,选项与 filters 相同 */
  34.             exclusionFilters: [],
  35.             /**
  36.              * 服务选项,通常需要包含此项,告诉浏览器你随后想要访问的蓝牙服务;
  37.              * 如果其他选项中没有指定服务,则必须在此处指定,否则随后访问服务时抛出异常
  38.              * */
  39.             optionalServices: ['battery_service'],
  40.             /** 匹配所有设备,一般不建议使用 */
  41.             acceptAllDevices: false
  42.           });
  43.           // gatt 服务器
  44.           const server = bluetoothDevice.gatt;
  45.           if (!server.connected) {
  46.             // 创建连接
  47.             await server.connect();
  48.           }
  49.           /** 获取电池 Service */
  50.           const batteryService = await server.getPrimaryService('battery_service');
  51.           /** 获取电池电量的 Characteristic */
  52.           const batteryLevelCharacteristic =
  53.             await batteryService.getCharacteristic('battery_level');
  54.           /** 读取 Characteristic 值,这里是电池剩余电量 */
  55.           const batteryLevelValue = await batteryLevelCharacteristic.readValue();
  56.           /** 打印剩余电量百分比 */
  57.           console.log(`Battery percentage is ${batteryLevelValue.getUint8(0)}`);
  58.         } catch (err) {
  59.           window.alert('Error: ' + err.message);
  60.         }
  61.       });
  62.     </script>
  63.   </body>
  64. </html>
复制代码
得到的效果为:

Android 自定义 GATT 服务

简朴通过上述代码搜索设备并尝试建立毗连进行通讯,会发现无法达到本身想要的效果;为了更好的理解 Web Bluetooth,自定义一个 GATT 服务实现通讯是很好的方法:
  1. package com.example.myapplication
  2. import android.Manifest
  3. import android.bluetooth.BluetoothDevice
  4. import android.bluetooth.BluetoothGatt
  5. import android.bluetooth.BluetoothGattCharacteristic
  6. import android.bluetooth.BluetoothGattServer
  7. import android.bluetooth.BluetoothGattServerCallback
  8. import android.bluetooth.BluetoothGattService
  9. import android.bluetooth.BluetoothManager
  10. import android.bluetooth.BluetoothProfile
  11. import android.bluetooth.le.AdvertiseCallback
  12. import android.bluetooth.le.AdvertiseData
  13. import android.bluetooth.le.AdvertiseSettings
  14. import android.content.Context
  15. import android.content.pm.PackageManager
  16. import android.os.Build
  17. import android.os.Bundle
  18. import android.os.ParcelUuid
  19. import android.util.Log
  20. import android.widget.Toast
  21. import androidx.activity.ComponentActivity
  22. import androidx.annotation.RequiresApi
  23. import java.util.UUID
  24. class MainActivity : ComponentActivity() {
  25.     @RequiresApi(Build.VERSION_CODES.S)
  26.     override fun onCreate(savedInstanceState: Bundle?) {
  27.         super.onCreate(savedInstanceState)
  28.         initBluetoothGATT()
  29.     }
  30.     // 初始化蓝牙 GATT 服务并开始广播
  31.     @RequiresApi(Build.VERSION_CODES.S)
  32.     private fun initBluetoothGATT() {
  33.         // 获取蓝牙管理器
  34.         val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
  35.         // 获取蓝牙适配器
  36.         val bluetoothAdapter = bluetoothManager.adapter
  37.         // 定义 gatt 服务器
  38.         var bluetoothGattServer: BluetoothGattServer? = null
  39.         // 检查权限
  40.         if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
  41.             // 定义 gatt 服务器的回调
  42.             val bluetoothGattServerCallback = object : BluetoothGattServerCallback() {
  43.                 // 与蓝牙设备的连接状态变更
  44.                 override fun onConnectionStateChange(
  45.                     device: BluetoothDevice,
  46.                     status: Int,
  47.                     newState: Int
  48.                 ) {
  49.                     if (newState == BluetoothProfile.STATE_CONNECTED) {
  50.                         Log.d(TAG, "Device connected")
  51.                     } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
  52.                         Log.d(TAG, "Device disconnected")
  53.                     }
  54.                 }
  55.                 // Service 被添加
  56.                 override fun onServiceAdded(status: Int, service: BluetoothGattService) {
  57.                     if (service.uuid == SERVICE_UUID) {
  58.                         Log.d(TAG, "Service added successfully.")
  59.                     }
  60.                 }
  61.                 // 特性读取请求
  62.                 override fun onCharacteristicReadRequest(
  63.                     device: BluetoothDevice,
  64.                     requestId: Int,
  65.                     offset: Int,
  66.                     characteristic: BluetoothGattCharacteristic
  67.                 ) {
  68.                     // 判断是否指定特性请求
  69.                     if (characteristic.uuid == CHARACTERISTIC_UUID) {
  70.                         // 处理读取请求,这里可以设置返回的数据
  71.                         val dataToSend = "Hello from custom characteristic!".toByteArray()
  72.                         // 设置特性值
  73.                         characteristic.setValue(dataToSend)
  74.                         // 检查权限,这里是避免编辑器警告
  75.                         if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
  76.                             // 发送响应,必须调用此方法,web 端的 readValue 方法才能继续执行
  77.                             bluetoothGattServer!!.sendResponse(
  78.                                 device,
  79.                                 requestId,
  80.                                 BluetoothGatt.GATT_SUCCESS,
  81.                                 offset,
  82.                                 dataToSend
  83.                             )
  84.                         }
  85.                     }
  86.                 }
  87.                 // 特性写入请求
  88.                 override fun onCharacteristicWriteRequest(
  89.                     device: BluetoothDevice,
  90.                     requestId: Int,  // 请求的特性 id
  91.                     characteristic: BluetoothGattCharacteristic,  // 特性
  92.                     preparedWrite: Boolean,
  93.                     responseNeeded: Boolean,  // 是否需要响应
  94.                     offset: Int,  // 数据偏移,数据可能是分段发送的
  95.                     value: ByteArray // 数据值
  96.                 ) {
  97.                     // 判断是否指定特性
  98.                     if (characteristic.uuid == CHARACTERISTIC_UUID) {
  99.                         characteristic.setValue(value)
  100.                     }
  101.                 }
  102.             }
  103.             // 打开一个 gatt 服务器
  104.             bluetoothGattServer = bluetoothManager.openGattServer(this, bluetoothGattServerCallback)
  105.             // 获取蓝牙广播器
  106.             val bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser
  107.             // 判断是否支持蓝牙
  108.             if (bluetoothLeAdvertiser != null) {
  109.                 // 蓝牙广播设置
  110.                 val settings = AdvertiseSettings.Builder()
  111.                     .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) // 设置低延迟模式
  112.                     .setConnectable(true) // 设置可连接
  113.                     .setTimeout(0) // 不超时关闭,除非手动关闭
  114.                     .build()
  115.                 // 蓝牙广播数据
  116.                 val data = AdvertiseData.Builder()
  117.                     .setIncludeDeviceName(true) // 设置广播时的数据包含设备名称
  118.                     .addServiceUuid(ParcelUuid(SERVICE_UUID)) // 添加自定义服务的 UUID 到广播数据中
  119.                     .build()
  120.                 // 开始广播
  121.                 bluetoothLeAdvertiser.startAdvertising(
  122.                     settings,
  123.                     data,
  124.                     object : AdvertiseCallback() {
  125.                         // 正常开始广播
  126.                         override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
  127.                             Log.d(TAG, "Advertising started successfully.")
  128.                         }
  129.                         // 无法开启广播
  130.                         override fun onStartFailure(errorCode: Int) {
  131.                             Log.e(TAG, "Advertising failed with error code: $errorCode")
  132.                         }
  133.                     })
  134.                 // 构造一个自定义 UUID 的 Service,指定为主要 Service
  135.                 val service =
  136.                     BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)
  137.                 // 构造一个自定义 UUID 的 Characteristic
  138.                 val characteristic = BluetoothGattCharacteristic(
  139.                     CHARACTERISTIC_UUID,
  140.                     // 设置读写属性
  141.                     BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_WRITE,
  142.                     // 设置读写权限
  143.                     BluetoothGattCharacteristic.PERMISSION_READ or BluetoothGattCharacteristic.PERMISSION_WRITE
  144.                 )
  145.                 // 将 Characteristic 添加至 Service
  146.                 service.addCharacteristic(characteristic)
  147.                 // 将 Service 添加至打开的 GATT 服务器
  148.                 bluetoothGattServer.addService(service)
  149.             }
  150.         } else {
  151.             // 没有权限请求权限
  152.             requestPermissions(
  153.                 arrayOf(Manifest.permission.BLUETOOTH_CONNECT),
  154.                 REQUEST_BLUETOOTH_PERMISSION_CODE
  155.             )
  156.         }
  157.     }
  158.     @Deprecated("Deprecated in Java")
  159.     @RequiresApi(Build.VERSION_CODES.S)
  160.     override fun onRequestPermissionsResult(
  161.         requestCode: Int,
  162.         permissions: Array<out String>,
  163.         grantResults: IntArray
  164.     ) {
  165.         super.onRequestPermissionsResult(requestCode, permissions, grantResults)
  166.         if (requestCode == REQUEST_BLUETOOTH_PERMISSION_CODE) {
  167.             if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  168.                 // 权限被授予,可以进行蓝牙连接操作
  169.                 initBluetoothGATT()
  170.             } else {
  171.                 // 权限被拒绝
  172.                 Toast.makeText(this, "蓝牙连接权限被拒绝", Toast.LENGTH_SHORT).show()
  173.             }
  174.         }
  175.     }
  176.     companion object {
  177.         private const val TAG = "BluetoothService"
  178.         // 请求权限 code
  179.         private const val REQUEST_BLUETOOTH_PERMISSION_CODE = 1001
  180.         // 定义服务 UUID
  181.         private val SERVICE_UUID: UUID = UUID.fromString("00009527-0000-1000-8000-00805f9b34fb")
  182.         // 定义特性 UUID
  183.         private val CHARACTERISTIC_UUID: UUID =
  184.             UUID.fromString("11009527-1100-1100-1100-110011001100")
  185.     }
  186. }
复制代码
上述代码是 Android 应用主 Activity 的代码,配合上述代码需要在 Android 应用配置清单中添加对应的权限:
  1. <!-- AndroidManifest.xml -->
  2. <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
  3. <!-- 蓝牙搜索配对 -->
  4. <uses-permission android:name="android.permission.BLUETOOTH" />
  5. <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
  6. <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  7. <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
  8. <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
  9. <!-- 操纵蓝牙的开启-->
  10. <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
  11. <!-- 如果应用必须安装在支持蓝牙的设备上,可以将下面的required的值设置为true。-->
  12. <uses-feature
  13.     android:name="android.hardware.bluetooth_le"
  14.     android:required="false" />
复制代码
在编译打开这个 Android 应用后,会开启一个 UUID 为 0x9527 的蓝牙服务,包罗一个 UUID 为 11009527-1100-1100-1100-110011001100 的特性,这个特性会在被读取时,将特性值设置为 “Hello from custom characteristic!”,并发送相应到调用方。我们用以下 web 端的代码来测试一下:
  1. <!doctype html>
  2. <html lang="en">
  3.   <head>
  4.     <meta charset="UTF-8" />
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6.     <title>Document</title>
  7.   </head>
  8.   <body>
  9.     <button class="request">click me</button>
  10.     <script>
  11.       const request = window.document.querySelector('.request');
  12.       request.addEventListener('click', async function () {
  13.         const bluetooth = window.navigator.bluetooth;
  14.         try {
  15.           const isSupport = await bluetooth.getAvailability();
  16.           if (!isSupport) {
  17.             return window.alert('用户代理不支持蓝牙请求');
  18.           }
  19.           const bluetoothDevice = await bluetooth.requestDevice({
  20.             filters: [
  21.               {
  22.                 services: [0x9527] // 过滤蓝牙服务 UUID 为 9527 的设备
  23.               }
  24.             ],
  25.             optionalServices: [0x9527] // 稍后需要操作此服务
  26.           });
  27.           const server = bluetoothDevice.gatt; // 获取对蓝牙服务器的引用
  28.           if (!server.connected) {
  29.             await server.connect(); // 连接服务器
  30.           }
  31.           const service = await server.getPrimaryService(0x9527); // 获取 9527 的蓝牙服务
  32.           const characteristic = await service.getCharacteristic(
  33.             '11009527-1100-1100-1100-110011001100'
  34.           ); // 获取 11009527-1100-1100-1100-110011001100 的特性
  35.          
  36.           // 侦听特性值变更
  37.           characteristic.addEventListener('characteristicvaluechanged', (e) => {
  38.             console.log('characteristicvaluechanged: ', e.target.value);
  39.           });
  40.           // 读取特性值
  41.           const value = await characteristic.readValue();
  42.           // 编码响应
  43.           console.log(new TextDecoder().decode(value));
  44.         } catch (err) {
  45.           window.alert('Error: ' + err.message);
  46.         }
  47.       });
  48.     </script>
  49.   </body>
  50. </html>
复制代码
查看效果:

除了被动读写外,Android 端的 gatt 服务器还支持 notifyCharacteristicChanged 方法,此方法会触发 web 端 characteristic 实例的 characteristicvaluechanged 事件获取最新的特性值,通过这种方式可以做到主动通知 web 端的效果。
   通过自定义终端应用实现自定义 GATT 服务器的方式可以完成与 web 端的点对点毗连,但是 web bluetooth 的兼容性还不足以支持完成大型的项目,稳定性也无法考证。
  原文地址:https://yuanyxh.com/articles/web_bluetooth_and_point_to_point_connection.html
参考资料



  • MDN: https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API
  • 通过 JavaScript 与蓝牙设备通讯: https://developer.chrome.com/docs/capabilities/bluetooth?hl=zh-cn
  • 一文带你熟悉蓝牙 GATT 协议: https://juejin.cn/post/7160308393503113247
– end

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

滴水恩情

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表