Android低功耗蓝牙开发总结

news2024/11/16 7:32:47

基础使用

权限申请

蓝牙权限在各个版本中略有不同

  • Android 12 及以上版本,如果不需要通过蓝牙来推断位置的话,蓝牙扫描不需要开启位置权
  • Android 11 及以下版本,蓝牙扫描必须开启位置权限
  • Android 9 及以下版本,蓝牙扫描可开启粗略位置权限
<!-- Android 12 及以上版本 -->
<!-- 如果明确不需要蓝牙推断位置的话,可以通过标记 usesPermissionFlags=“neverForLocation” --> 
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
  android:usesPermissionFlags="neverForLocation"
  tools:targetApi="s" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>

<!-- Android 11 及以下版本 -->
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>

<!-- Android 9 及以下版本 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28"/>

开启扫描/停止扫描

//获取蓝牙适配器
val bleAdapter = (context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter

//监听返回数据
private val bleScanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult?) {
        if (result != null){
            Log.e("bleLog", "startScanResult = $result")
        }
    }
}

/**
 * 开启扫描
 */
bleAdapter.bluetoothLeScanner.startScan(bleScanCallback)

/**
 * 结束扫描
 */
bleAdapter.bluetoothLeScanner.stopScan(bleScanCallback)

开始连接/断开连接


private var mBleGatt : BluetoothGatt? = null

//连接过程与数据接收回调
private val bleGattCallback = object : BluetoothGattCallback() {

  //连接状态变更
  override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
    if (newState == BluetoothProfile.STATE_CONNECTED){
      //已连接
      //发现服务
      mBleGatt?.discoverServices()
    }else if (newState == BluetoothProfile.STATE_DISCONNECTED){
      //已断开连接
    }
  }

  //发现服务回调
  override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
    // 调用 mBleGatt?.discoverServices() 时触发该回调
    if (status != BluetoothGatt.GATT_SUCCESS){
      //失败
      return
    }
    //获取指定GATT服务,UUID 由远程设备提供
    val bleGattService = mBleGatt?.getService(UUID.fromString("8888888"))
    //获取指定GATT特征,UUID 由远程设备提供
    val bleGattCharacteristic = bleGattService?.getCharacteristic(UUID.fromString("777777"))
    //启用特征通知,如果远程设备修改了特征,则会触发 onCharacteristicChange() 回调
    mBleGatt?.setCharacteristicNotification(bleGattCharacteristic, true)
    //启用客户端特征配置【固定写法】
    val bleGattDescriptor = bleGattCharacteristic?.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
    bleGattDescriptor?.value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
    mBleGatt?.writeDescriptor(bleGattDescriptor)
  }

  //启用客户端特征配置结果回调
  override fun onDescriptorWrite(gatt: BluetoothGatt?, descriptor: BluetoothGattDescriptor?, status: Int) {
    if (status == BluetoothGatt.GATT_SUCCESS ){
      //此时蓝牙设备连接才算真正连接成功,即具备读写数据的能力
    }
  }

  //App修改特征回调,即 App 给设备发送数据结果回调
  override fun onCharacteristicWrite(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int
                                          ) {
    if (status == BluetoothGatt.GATT_SUCCESS){
      //数据写入完成
      // 调用 characteristic?.value 得到的 ByteArray 与 发送数据一样
    }
  }

  //远程设备修改特征描述回调,即设备给 App 发送数据
  override fun onCharacteristicChanged(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?
      ) {
    //调用 characteristic?.value 获取远程设备发送过来的数据
  }
}

/**
 * 开始连接
 * @param deviceMac 设备Mac地址
 */
val bleDevice = bleAdapter.getRemoteDevice(deviceMac)
mBleGatt = bleDevice.connectGatt(context, false, bleGattCallback, BluetoothDevice.TRANSPORT_LE)

/**
 * 断开连接
 */
mBleGatt?.disconnect()
mBleGatt?.close()

写入数据

mBleGattCharacteristic?.value = data
mBleGatt?.writeCharacteristic(mBleGattCharacteristic)

完整链路

总结

记住一个核心:蓝牙传输非常不稳定,指不定啥时候就没响应或丢包了。

连接过程

用户体验

  • Android 12 以下版本蓝牙扫描需要开启定位+授权才能使用,所以在扫描前要申请蓝牙&定位权限+判断是否开启蓝牙&定位。
  • 使用过程中,用户可能误操作关闭蓝牙,所以要监听蓝牙开关状态。
  • 蓝牙扫描添加超时机制,超时自动停止扫描。
  • 如果用列表按照信号强度展示扫描结果,建议扫描结束后再让用户选择设备,防止列表频繁跳动,导致用户误选。
  • 关于蓝牙的UI界面或操作,都需要判断当前蓝牙是否已连接。

注意点

  • 连接过程会有很多中间过程(触发连接 -> 连接回调成功后 -> 发现服务 -> …),当获取为 null 或者返回失败时,要做异常返回,防止进度卡死。
  • 同上,连接中间过程较多,防止远端设备偶现无响应,在连接过程中设置超时机制,超时判定连接失败。
  • 当存在多个 GATT 特征时,可能需要调用多次 setCharacteristicNotification() + writeDescriptor(),注意此操作不能连续调用,正确姿势:gatt1 调用完成,待 onDescriptorWrite() 回调后,gatt2 再调用。

数据收发过程

背景:我手里的远端设备是一款实时操作系统的智能穿戴设备。该设备有一个特点:只能处理一条指令,处理完成后等待下一条,如果同时来多条,则只能处理第一条。

注意点

  • 因为远端设备只能处理单条指令,所以需要维护一个优先级队列
  • 蓝牙传输有最大传输单元限制(MTU),默认最大 23 个字节,可用的只有 20 个字节,[ 23 byte(ATT) =1 byte(Opcode) + 2 byte(Handler) + 20 byte(BATT) ],所以在发送指令时要做分包处理。
  • MTU 可通过调用 requestMtu() 调整大小,具体调整多大需和远端设备协定,调用后会回调 gattCallback#onMtuChanged(),注意:发现服务的调用要在该回调中,不能在连接状态回调中。
  • 单一指令发送和回包,需要加超时机制。即调用发送指令时开始超时倒计时,当触发 onCharacteristicChanged() 时并判断为指令回包,则移除倒计时。如果 onCharacteristicWrite() 返回失败或超时未回包,则移除倒计时并返回失败。
  • 单一指令发送并伴随多条回包,需要加 watchDog 机制。即调用发送指令时开始“养狗”,当有远端设备回包时“喂狗”,回包全部完成时“杀狗”,如果 onCharacteristicWrite() 返回失败或到时间没有“喂狗”,则“杀狗”并返回失败。

可能用到的知识

进制转换

Android Studio 打印日志或断点时,会自动将 16 进制 转成 10 进制进行显示。

十进制 与 16进制
十进制 -> 16进制:

十进制数 除以 16 取余,然后从低往上输出。例如:1758 = 0x6DE

16进制转 -> 十进制:

位数指向的数 * 16^位数 相加之和。例如 0x2A7F = 10879

十进制 与 二进制
十进制 -> 二进制:

记住常用数转化:例如, 45 = (32 + 8 + 4 + 1) = 101101

十进制二进制
1 (2^0)01
2 (2^1)10
4 (2^2)100
8 (2^3)1000
16 (2^4)10000
32 (2^5)100000
64 (2^6)1000000
二进制 -> 十进制:

位数指向的数 * 2^位数 相加之和。例如 10010 = 18

16进制 与 二进制
16进制 -> 二进制:

按每位数单独转二进制。例如: 0x6DA2 = 110110110100010

二进制 -> 16进制:

每四位一组,每组转 16 进制,然后拼接。例如: 101010110 = 0x156

位运算
& (与)

都为 1 时才是1

|(或)

**只要有 1 **时就是 1

^ (异或)

**只有一个 1 **时才是 1

~ (取反)

1 变 0, 0 变 1

>> (右移)

除以2^右移位数。例如: 75 >> 3 = 9

<< (左移)

乘以 2^ 左移位数。例如: 75 << 3 = 600

推荐阅读

Android 12 中的新蓝牙权限
蓝牙概览 | Connectivity | Android Developers
蓝牙智能设备数据采集平台化方案 | 京东云技术团队 - 掘金
BLE低功耗蓝牙技术详解
Android蓝牙通信机制详解 - 掘金





Hi,我是“青杉”,您可以通过如下方式关注我:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1360356.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

弧形导轨和直线导轨的区别

弧形导轨和直线导轨是两种常见的导轨类型&#xff0c;都具有支撑和引导功能&#xff0c;都可以将运动的能量传递到接收端&#xff0c;实现稳定的运动。那么这两者有什么区别呢&#xff1f; 从结构上来看&#xff0c;直线导轨呈现直线的形状&#xff0c;在机器设备的运动中起到了…

Java学习苦旅(十八)——详解Java中的二叉树

本篇博客将详细讲解二叉树 文章目录 树型结构简介基本概念表示形式 二叉树概念两种特殊的二叉树二叉树的性质二叉树的存储二叉树的简单创建二叉树的遍历前中后序遍历层序遍历 结尾 树型结构 简介 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09…

Pytest——Fixture夹具的使用

一、什么是Fixture 在测试开展的过程中&#xff0c;会需要考虑到测试前的准备工作&#xff0c;以及测试后的释放操作行为。这些在Pytest中&#xff0c;会通过Fixture的方式来实现。如果说在运行pytest的测试用例的时候&#xff0c;需要调用一些数据来实现测试行为&#xff0c;…

AJAX(三)跨域

一、同源策略 同源策略最早由Netscape公司提出&#xff0c;是浏览器的一种安全策略。 同源&#xff1a;协议、域名、端口号必须完全相同。&#xff08;同一个来源&#xff09; 违背同源策略就是跨域。 AJAX发送请求时是默认要遵循同源策略的&#xff0c;不是同源策略&#…

Java 如何实现微信支付功能代码示例

微信支付是由中国的腾讯公司推出的一种移动支付方式。它允许用户通过在微信应用中绑定银行卡或其他支付方式来进行交易&#xff0c;包括在线购物、转账、付款码支付等。微信支付的特点包括便捷、安全、快速和全面&#xff0c;使用户可以随时随地完成交易。用户可以通过扫描商家…

QT自定义信号和槽

信号和槽 介绍实现创建文件对teacher的h和cpp文件进行处理对student的h和cpp文件进行处理对widget的h和cpp文件进行处理 介绍 Qt中的信号和槽是一种强大的机制&#xff0c;用于处理对象之间的通信。它们是Qt框架中实现事件驱动编程的核心部分。 信号&#xff08;Signal&#x…

48种国内外的PCB设计工具-你知道的有哪几种呢?

针对强迫症&#xff0c;非要使用最好最全的工具&#xff1b;针对死较真&#xff0c;认为自己的工具最好用&#xff1b; 工具只是工具&#xff0c;思想最重要&#xff01; 自记录&#xff1a; 无论我们设计什么样的项目&#xff0c;电子工程师都必须知道电路应该如何布局以及…

使用mysql查询当天、近一周、近一个月及近一年的数据以及各种报表查询sql

1.mysql查询当天的数据 1 select * from table where to_days(时间字段) to_days(now()); 2.mysql查询昨天的数据 1 select * from table where to_days(now( ) ) - to_days( 时间字段名) < 1 3.mysql查询近一个月的数据 1 SELECT * FROM table WHERE date(时间字段) …

Idea连接Docker在本地(Windows)开发SpringBoot

文章目录 1. 新建运行配置2. 修改运行目标3. 设置新目标Docker4. 选择运行主类5. 运行 当一些需要的服务在docker容器中运行时&#xff0c;因为docker网络等种种原因&#xff0c;不得不把在idea开发的springboot项目放到docker容器中才能做测试或者运行。 1. 新建运行配置 2. …

开启Android学习之旅-3-Android Activity

Android Activity 本文总结《第一行代码 Android》第3版的内容 环境&#xff1a; Android Studio Giraffe | 2022.3.1 Patch 3 Activity 是什么&#xff1f; Activity 简单将就是UI界面&#xff0c;包含两部分 Activity 类 和应用布局文件&#xff0c;如果是 Compose 则另说&…

kubernetes(一)概述与架构

云原生实战 语雀 官网 Kubernetes 文档 | Kubernetes 更新&#xff1a;移除 Dockershim 的常见问题 | Kubernetes B站课程&#xff1a;https://www.bilibili.com/video/BV13Q4y1C7hS/?p26 1.概述 概述 | Kubernetes 大规模容器编排系统 kubernetes具有以下特性&#xf…

【数值分析】非线性方程求根,牛顿法,牛顿下山法,matlab实现

4. 牛顿法 收敛时牛顿法的收敛速度是二阶的&#xff0c;不低于二阶。如果函数有重根&#xff0c;牛顿法一般不是二阶收敛的。 x k 1 x k − f ( x k ) f ′ ( x k ) x_{k1}x_k- \frac{f(x_k)}{f(x_k)} xk1​xk​−f′(xk​)f(xk​)​ matlab实现 %% 牛顿迭代例子 f (x) x…

x-cmd pkg | gh - GitHub 官方 CLI

目录 简介首次用户功能特点与 x-cmd gh 模块的关系相关作品进一步探索 简介 gh&#xff0c;是由 GitHub 官方使用 Go 语言开发和维护的命令行工具&#xff0c;旨在脚本或是命令行中便捷管理和操作 GitHub 的工作流程。 注意: 由于 x-cmd 提供了同名模块&#xff0c;因此使用官…

机器视觉系统选型-环境配置:报错序列不包含任何元素 的解决方法

描述 环境&#xff1a;VM4.0.0VS2015 及以上 现象&#xff1a;配置环境后&#xff0c;获取线线测量模块结果&#xff0c;报错“序列不包含任何元素”。如下图所示&#xff1a; 解答 将“\VisionMaster4.0.0\Development\V4.0.0 \ComControls\bin\x64”下整体重新拷贝。

StreamPark + PiflowX 打造新一代大数据计算处理平台

&#x1f680; 什么是PiflowX PiFlow 是一个基于分布式计算框架 Spark 开发的大数据流水线系统。该系统将数据的采集、清洗、计算、存储等各个环节封装成组件&#xff0c;以所见即所得方式进行流水线配置。简单易用&#xff0c;功能强大。它具有如下特性&#xff1a; 简单易用…

在云服务器ECS上用Python写一个搜索引擎

在云服务器ECS上用Python写一个搜索引擎 一、场景介绍二、搜索引擎的组成2.1 网页的爬取及排序2.2 用户使用搜索引擎进行搜索 三、操作步骤3.1 环境准备3.2 安装Anaconda3.3 安装Streamlit3.4 下载搜索引擎代码3.5 运行搜索引擎 四、常见问题4.1 运行setup.py时可能的问题4.2 如…

oracle 补齐数字长度 to_char踩坑

oracle的to_char网上找到的说明如下 &#xff08;1&#xff09;用作日期转换&#xff1a; to_char(date,格式); select to_date(2005-01-01 ,yyyy-MM-dd) from dual; select to_char(sysdate,yyyy-MM-dd HH24:mi:ss) from dual; &#xff08;2&#xff09;处理数字&#xf…

unity PDFRender Curved UI3.3

【PDF】PDFRender 链接&#xff1a;https://pan.baidu.com/s/1wSlmfiWTAHZKqEESxuMH6Q 提取码&#xff1a;csdn 【曲面ui】 Curved UI3.3 链接&#xff1a;https://pan.baidu.com/s/1uNZySJTW0-pPwi2FTE6fgA 提取码&#xff1a;csdn

【软件测试】2024年准备中/高级测试岗技术面试...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、软件测试基础知…

Netplan介绍

1 介绍 1.1 简介 Netplan是一个抽象网络配置描述器。通过netplan命令&#xff0c;你只需用一个 YAML文件描述每个网络接口所需配置。netplan并不关系底层管理工具是NetworkManager还是networkd。 它是一个在 Linux 系统上进行网络配置的实用程序。您创建所需接口的描述并定义…