Web Bluetooth 与点对点连接

2024-09-08 04:20
文章标签 连接 web bluetooth 点对点

本文主要是介绍Web Bluetooth 与点对点连接,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

需求需要实现手持终端设备与 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: 特性上的属性,用于操作特性值,比如 writeread 用于读写特性值

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:写入特性值

看一个简单的示例:

<!doctype html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title></head><body><button class="request">click me</button><script>const request = window.document.querySelector('.request');request.addEventListener('click', async function () {const bluetooth = window.navigator.bluetooth;try {// 检查用户代理是否支持const isSupport = await bluetooth.getAvailability();if (!isSupport) {return window.alert('用户代理不支持蓝牙请求');}// 请求蓝牙设备const bluetoothDevice = await bluetooth.requestDevice({/** 过滤器选项 */filters: [{/** 过滤拥有 battery_service | 0x1101 | 0000180D-0000-1000-8000-00805f9b34fb 服务的设备 */services: ['battery_service', 0x1101, '0000180D-0000-1000-8000-00805f9b34fb'],/** 过滤名称为 RedMi Note13Pro 的设备 */name: 'RedMi Note13Pro',/** 过滤名称前缀为 RedMi 的设备 */namePrefix: 'RedMi'}],/** 排除项,选项与 filters 相同 */exclusionFilters: [],/*** 服务选项,通常需要包含此项,告诉浏览器你随后想要访问的蓝牙服务;* 如果其他选项中没有指定服务,则必须在此处指定,否则随后访问服务时抛出异常* */optionalServices: ['battery_service'],/** 匹配所有设备,一般不建议使用 */acceptAllDevices: false});// gatt 服务器const server = bluetoothDevice.gatt;if (!server.connected) {// 创建连接await server.connect();}/** 获取电池 Service */const batteryService = await server.getPrimaryService('battery_service');/** 获取电池电量的 Characteristic */const batteryLevelCharacteristic =await batteryService.getCharacteristic('battery_level');/** 读取 Characteristic 值,这里是电池剩余电量 */const batteryLevelValue = await batteryLevelCharacteristic.readValue();/** 打印剩余电量百分比 */console.log(`Battery percentage is ${batteryLevelValue.getUint8(0)}`);} catch (err) {window.alert('Error: ' + err.message);}});</script></body>
</html>

得到的效果为:

image

Android 自定义 GATT 服务

简单通过上述代码搜索设备并尝试建立连接进行通讯,会发现无法达到自己想要的效果;为了更好的理解 Web Bluetooth,自定义一个 GATT 服务实现通讯是很好的方法:

package com.example.myapplicationimport android.Manifest
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattServer
import android.bluetooth.BluetoothGattServerCallback
import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.ParcelUuid
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import java.util.UUIDclass MainActivity : ComponentActivity() {@RequiresApi(Build.VERSION_CODES.S)override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)initBluetoothGATT()}// 初始化蓝牙 GATT 服务并开始广播@RequiresApi(Build.VERSION_CODES.S)private fun initBluetoothGATT() {// 获取蓝牙管理器val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager// 获取蓝牙适配器val bluetoothAdapter = bluetoothManager.adapter// 定义 gatt 服务器var bluetoothGattServer: BluetoothGattServer? = null// 检查权限if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {// 定义 gatt 服务器的回调val bluetoothGattServerCallback = object : BluetoothGattServerCallback() {// 与蓝牙设备的连接状态变更override fun onConnectionStateChange(device: BluetoothDevice,status: Int,newState: Int) {if (newState == BluetoothProfile.STATE_CONNECTED) {Log.d(TAG, "Device connected")} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {Log.d(TAG, "Device disconnected")}}// Service 被添加override fun onServiceAdded(status: Int, service: BluetoothGattService) {if (service.uuid == SERVICE_UUID) {Log.d(TAG, "Service added successfully.")}}// 特性读取请求override fun onCharacteristicReadRequest(device: BluetoothDevice,requestId: Int,offset: Int,characteristic: BluetoothGattCharacteristic) {// 判断是否指定特性请求if (characteristic.uuid == CHARACTERISTIC_UUID) {// 处理读取请求,这里可以设置返回的数据val dataToSend = "Hello from custom characteristic!".toByteArray()// 设置特性值characteristic.setValue(dataToSend)// 检查权限,这里是避免编辑器警告if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {// 发送响应,必须调用此方法,web 端的 readValue 方法才能继续执行bluetoothGattServer!!.sendResponse(device,requestId,BluetoothGatt.GATT_SUCCESS,offset,dataToSend)}}}// 特性写入请求override fun onCharacteristicWriteRequest(device: BluetoothDevice,requestId: Int,  // 请求的特性 idcharacteristic: BluetoothGattCharacteristic,  // 特性preparedWrite: Boolean,responseNeeded: Boolean,  // 是否需要响应offset: Int,  // 数据偏移,数据可能是分段发送的value: ByteArray // 数据值) {// 判断是否指定特性if (characteristic.uuid == CHARACTERISTIC_UUID) {characteristic.setValue(value)}}}// 打开一个 gatt 服务器bluetoothGattServer = bluetoothManager.openGattServer(this, bluetoothGattServerCallback)// 获取蓝牙广播器val bluetoothLeAdvertiser = bluetoothAdapter.bluetoothLeAdvertiser// 判断是否支持蓝牙if (bluetoothLeAdvertiser != null) {// 蓝牙广播设置val settings = AdvertiseSettings.Builder().setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) // 设置低延迟模式.setConnectable(true) // 设置可连接.setTimeout(0) // 不超时关闭,除非手动关闭.build()// 蓝牙广播数据val data = AdvertiseData.Builder().setIncludeDeviceName(true) // 设置广播时的数据包含设备名称.addServiceUuid(ParcelUuid(SERVICE_UUID)) // 添加自定义服务的 UUID 到广播数据中.build()// 开始广播bluetoothLeAdvertiser.startAdvertising(settings,data,object : AdvertiseCallback() {// 正常开始广播override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {Log.d(TAG, "Advertising started successfully.")}// 无法开启广播override fun onStartFailure(errorCode: Int) {Log.e(TAG, "Advertising failed with error code: $errorCode")}})// 构造一个自定义 UUID 的 Service,指定为主要 Serviceval service =BluetoothGattService(SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY)// 构造一个自定义 UUID 的 Characteristicval characteristic = BluetoothGattCharacteristic(CHARACTERISTIC_UUID,// 设置读写属性BluetoothGattCharacteristic.PROPERTY_READ or BluetoothGattCharacteristic.PROPERTY_WRITE,// 设置读写权限BluetoothGattCharacteristic.PERMISSION_READ or BluetoothGattCharacteristic.PERMISSION_WRITE)// 将 Characteristic 添加至 Serviceservice.addCharacteristic(characteristic)// 将 Service 添加至打开的 GATT 服务器bluetoothGattServer.addService(service)}} else {// 没有权限请求权限requestPermissions(arrayOf(Manifest.permission.BLUETOOTH_CONNECT),REQUEST_BLUETOOTH_PERMISSION_CODE)}}@Deprecated("Deprecated in Java")@RequiresApi(Build.VERSION_CODES.S)override fun onRequestPermissionsResult(requestCode: Int,permissions: Array<out String>,grantResults: IntArray) {super.onRequestPermissionsResult(requestCode, permissions, grantResults)if (requestCode == REQUEST_BLUETOOTH_PERMISSION_CODE) {if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {// 权限被授予,可以进行蓝牙连接操作initBluetoothGATT()} else {// 权限被拒绝Toast.makeText(this, "蓝牙连接权限被拒绝", Toast.LENGTH_SHORT).show()}}}companion object {private const val TAG = "BluetoothService"// 请求权限 codeprivate const val REQUEST_BLUETOOTH_PERMISSION_CODE = 1001// 定义服务 UUIDprivate val SERVICE_UUID: UUID = UUID.fromString("00009527-0000-1000-8000-00805f9b34fb")// 定义特性 UUIDprivate val CHARACTERISTIC_UUID: UUID =UUID.fromString("11009527-1100-1100-1100-110011001100")}
}

上述代码是 Android 应用主 Activity 的代码,配合上述代码需要在 Android 应用配置清单中添加对应的权限:

<!-- AndroidManifest.xml --><uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- 蓝牙搜索配对 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- 操纵蓝牙的开启-->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- 如果应用必须安装在支持蓝牙的设备上,可以将下面的required的值设置为true。-->
<uses-featureandroid:name="android.hardware.bluetooth_le"android:required="false" />

在编译打开这个 Android 应用后,会开启一个 UUID 为 0x9527 的蓝牙服务,包含一个 UUID 为 11009527-1100-1100-1100-110011001100 的特性,这个特性会在被读取时,将特性值设置为 “Hello from custom characteristic!”,并发送响应到调用方。我们用以下 web 端的代码来测试一下:

<!doctype html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title></head><body><button class="request">click me</button><script>const request = window.document.querySelector('.request');request.addEventListener('click', async function () {const bluetooth = window.navigator.bluetooth;try {const isSupport = await bluetooth.getAvailability();if (!isSupport) {return window.alert('用户代理不支持蓝牙请求');}const bluetoothDevice = await bluetooth.requestDevice({filters: [{services: [0x9527] // 过滤蓝牙服务 UUID 为 9527 的设备}],optionalServices: [0x9527] // 稍后需要操作此服务});const server = bluetoothDevice.gatt; // 获取对蓝牙服务器的引用if (!server.connected) {await server.connect(); // 连接服务器}const service = await server.getPrimaryService(0x9527); // 获取 9527 的蓝牙服务const characteristic = await service.getCharacteristic('11009527-1100-1100-1100-110011001100'); // 获取 11009527-1100-1100-1100-110011001100 的特性// 侦听特性值变更characteristic.addEventListener('characteristicvaluechanged', (e) => {console.log('characteristicvaluechanged: ', e.target.value);});// 读取特性值const value = await characteristic.readValue();// 编码响应console.log(new TextDecoder().decode(value));} catch (err) {window.alert('Error: ' + err.message);}});</script></body>
</html>

查看效果:

image

除了被动读写外,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

这篇关于Web Bluetooth 与点对点连接的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1147099

相关文章

W外链微信推广短连接怎么做?

制作微信推广链接的难点分析 一、内容创作难度 制作微信推广链接时,首先需要创作有吸引力的内容。这不仅要求内容本身有趣、有价值,还要能够激起人们的分享欲望。对于许多企业和个人来说,尤其是那些缺乏创意和写作能力的人来说,这是制作微信推广链接的一大难点。 二、精准定位难度 微信用户群体庞大,不同用户的需求和兴趣各异。因此,制作推广链接时需要精准定位目标受众,以便更有效地吸引他们点击并分享链接

Java Web指的是什么

Java Web指的是使用Java技术进行Web开发的一种方式。Java在Web开发领域有着广泛的应用,主要通过Java EE(Enterprise Edition)平台来实现。  主要特点和技术包括: 1. Servlets和JSP:     Servlets 是Java编写的服务器端程序,用于处理客户端请求和生成动态网页内容。     JSP(JavaServer Pages)

BUUCTF靶场[web][极客大挑战 2019]Http、[HCTF 2018]admin

目录   [web][极客大挑战 2019]Http 考点:Referer协议、UA协议、X-Forwarded-For协议 [web][HCTF 2018]admin 考点:弱密码字典爆破 四种方法:   [web][极客大挑战 2019]Http 考点:Referer协议、UA协议、X-Forwarded-For协议 访问环境 老规矩,我们先查看源代码

Java 连接Sql sever 2008

Java 连接Sql sever 2008 /Sql sever 2008 R2 import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class TestJDBC

EasyPlayer.js网页H5 Web js播放器能力合集

最近遇到一个需求,要求做一款播放器,发现能力上跟EasyPlayer.js基本一致,满足要求: 需求 功性能 分类 需求描述 功能 预览 分屏模式 单分屏(单屏/全屏) 多分屏(2*2) 多分屏(3*3) 多分屏(4*4) 播放控制 播放(单个或全部) 暂停(暂停时展示最后一帧画面) 停止(单个或全部) 声音控制(开关/音量调节) 主辅码流切换 辅助功能 屏

实例:如何统计当前主机的连接状态和连接数

统计当前主机的连接状态和连接数 在 Linux 中,可使用 ss 命令来查看主机的网络连接状态。以下是统计当前主机连接状态和连接主机数量的具体操作。 1. 统计当前主机的连接状态 使用 ss 命令结合 grep、cut、sort 和 uniq 命令来统计当前主机的 TCP 连接状态。 ss -nta | grep -v '^State' | cut -d " " -f 1 | sort |

9.8javaweb项目总结

1.主界面用户信息显示 登录成功后,将用户信息存储在记录在 localStorage中,然后进入界面之前通过js来渲染主界面 存储用户信息 将用户信息渲染在主界面上,并且头像设置跳转,到个人资料界面 这里数据库中还没有设置相关信息 2.模糊查找 检测输入框是否有变更,有的话调用方法,进行查找 发送检测请求,然后接收的时候设置最多显示四个类似的搜索结果

JavaWeb【day09】--(Mybatis)

1. Mybatis基础操作 学习完mybatis入门后,我们继续学习mybatis基础操作。 1.1 需求 需求说明: 根据资料中提供的《tlias智能学习辅助系统》页面原型及需求,完成员工管理的需求开发。 通过分析以上的页面原型和需求,我们确定了功能列表: 查询 根据主键ID查询 条件查询 新增 更新 删除 根据主键ID删除 根据主键ID批量删除

【Go】go连接clickhouse使用TCP协议

离开你是傻是对是错 是看破是软弱 这结果是爱是恨或者是什么 如果是种解脱 怎么会还有眷恋在我心窝 那么爱你为什么                      🎵 黄品源/莫文蔚《那么爱你为什么》 package mainimport ("context""fmt""log""time""github.com/ClickHouse/clickhouse-go/v2")func main(

利用Django框架快速构建Web应用:从零到上线

随着互联网的发展,Web应用的需求日益增长,而Django作为一个高级的Python Web框架,以其强大的功能和灵活的架构,成为了众多开发者的选择。本文将指导你如何从零开始使用Django框架构建一个简单的Web应用,并将其部署到线上,让世界看到你的作品。 Django简介 Django是由Adrian Holovaty和Simon Willison于2005年开发的一个开源框架,旨在简