Android低功耗蓝牙开发总结

基础使用

权限申请

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

  • 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_VALUEmBleGatt?.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.mzph.cn/news/600355.shtml

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

相关文章

【Turtle库】圣诞树

在寒冷的冬季&#xff0c;没有什么比一棵亮丽的圣诞树更能带给我们温暖和快乐。而现在&#xff0c;我们将使用Python编程语言来绘制这样一棵美丽的圣诞树。 首先&#xff0c;我们需要导入Python的turtle模块&#xff0c;它可以帮助我们绘制图形。然后&#xff0c;我们可以定义一…

《数字图像处理》 第11章 表示和描述 学习笔记附部分例子代码(c++opencv)

表示和描述 0. 前言1. 表示1.1 边界追踪1.2 链码1.3 使用最小周长多边形的多边形近似 2. 边界描绘子2.1 一些简单的描绘子![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/45dddc76217e4fde93a11e2631b2a71a.png#pic_center 500x)2.2 形状数2.3 傅里叶描绘子2.4 统计…

第11章 GUI Page462~476 步骤二十三 步骤二十四 Undo/Redo ①为Undo/Redo做准备工作,弹出日志窗口

step23和step24合起来学习 工程一 1.主窗口类中添加新的私有成员数据&#xff1a; 2 主窗口构造函数中&#xff0c;最后一行加入&#xff0c;用于调试的Log功能 3 鼠标弹起函数&#xff0c;添加Undo动作 4 编译之后报错&#xff1a;ActionLink不是一个类型 5 新增一个头文件…

Fast DDS 官方--C++ API Reference

Fast DDS 官方--C API Reference 1 介绍2 接口2.1 DDS DCPS PIM2.1.1 Core2.1.1.1 Entity 【基类】2.1.1.2 DomainEntity2.1.1.3 Policy 【枚举】2.1.1.3.1 DataRepresentationId2.1.1.3.2 DataRepresentationQosPolicy2.1.1.3.3 DataSharingQosPolicy2.1.1.3.4 DataSharingKin…

nginx连接数和性能优化

目录 一&#xff1a;介绍 二&#xff1a;优化配置 三&#xff1a;其他优化策略 一&#xff1a;介绍 Nginx是一个高性能的HTTP和反向代理服务器&#xff0c;具有许多用于优化连接数和性能的配置选项。以下是一些关键的配置和优化建议&#xff1a; 1&#xff1a;worker_proc…

Spark概述

Spark概述 Spark是什么 Apache Spark是一个快速的&#xff0c;多用途的集群计算系统&#xff0c;相对于Hadoop MapReduce将中间结果保存在磁盘中&#xff0c;Spark使用了内存保存中间结果&#xff0c;能在数据尚未写入硬盘时在内存中进行运算Spark只是一个计算框架&#xff0c;…

Mac Install Parallels Desktop 19.1.0

资料准备 ParallelsDesktop-19.1.0-54729.dmg Parallels Desktop Activation Tool 4.0.0 [MacKed].dmg链接: https://pan.baidu.com/s/1kxUKreiKdJXQIPXAJ8LJsA?pwd6666 提取码: 6666 –来自百度网盘超级会员v7的分享 双击 ParallelsDesktop-19.1.0-54729.dmg 点击打开 …

Apple 移动设备管理常见问题解答

什么是 Apple 移动设备管理 (MDM)&#xff1f; Apple 在企业中的扩张带来了生产力更高的员工队伍以及员工真正在任何地方工作的能力。 但更多的自由、不断扩大的边界和新的操作系统也会带来挑战。 任何规模的组织都必须让每个人的设备保持最佳运行状态&#xff0c;确保硬件和网…

Android studio环境配置

1.搜索android studio下载 Android Studio - Download 2.安装 3.配置环境 配置gradle&#xff0c;gradle参考网络配置。最后根据项目需求选择不同的jdk。

flutter 五:MaterialApp

MaterialApp const MaterialApp({super.key,this.navigatorKey, //导航键this.scaffoldMessengerKey, //scaffold管理this.home, //首页Map<String, WidgetBuilder> this.routes const <String, WidgetBuilder>{}, //路由this.initialRoute, //初始路由th…

Oracle START WITH 递归语句的使用方法及示例

Oracle数据库中的START WITH语句经常与CONNECT BY子句一起使用&#xff0c;以实现对层次型数据的查询。这种查询模式非常适用于处理具有父子关系的数据&#xff0c;如组织结构、分类信息等。 理解START WITH和CONNECT BY 在层次型查询中&#xff0c;START WITH定义了层次结构…

【SpringMVC】常用注解

什么是MVC&#xff1f; MVC是一种程序分层开发模式&#xff0c;分别是Model&#xff08;模型&#xff09;&#xff0c;View&#xff08;视图&#xff09;以及Controller&#xff08;控制器&#xff09;。这样做可以将程序的用户界面和业务逻辑分离&#xff0c;使得代码具有良好…

Leetcode2966. 划分数组并满足最大差限制

Every day a Leetcode 题目来源&#xff1a;2966. 划分数组并满足最大差限制 解法1&#xff1a;排序 将数组 nums 从小到大排序&#xff0c;每三个一组插入答案&#xff0c;如果有 nums[i 2] - nums[i] > k&#xff0c;则不满足要求&#xff0c;返回空数组。 代码&…

专业实习day3、4(路由器做内网访问公网)

专业实习 代码 display ip interface brief 显示当前设备下所有接口IP undo IP地址支持覆盖&#xff0c;但是正常的命令不能覆盖必须undo&#xff08;删除&#xff09;掉 un in en 在做配置的过程中&#xff0c;设备系统一般都会出现一些提示或者告警之类的东西&#xff0c;从…

matplotlib 虚战1

EDA 入门 visualization.py import matplotlib matplotlib.use("TkAgg")import pandas as pd from matplotlib import pyplot as plt import warningswarnings.filterwarnings(ignore)df pd.read_csv("diabetes.csv")# look at the first 5 rows of the…

字节填充与0比特填充以及数据链路的基本问题

目录 字节填充&#xff1a; 比特填充&#xff1a; 数据链路有三个基本问题 1.封装成帧 2.透明传输 3.差错检测 首先介绍一下PPP的帧结构&#xff1a; 首部的第一个字段和尾部的第二个字段都是标志字段F(Flag)&#xff0c;规定为0x7E (符号“0x”表示它后面的字符是用十六…

AntV-G6 -- 将G6图表应用到项目中

1. 效果图 2. 安装依赖 npm install --save antv/g6 3. 代码 import { useEffect } from alipay/bigfish/react; import G6 from antv/g6;const data {id: root,label: 利息收入,subLabel: 3,283.456,ratio: 3,children: [{id: child-a,label: 平均利息,subLabel: 9%,ratio:…

MySQL-约束

约束是作用在表中字段的规则&#xff0c;用于限制存储在表中的数据。 约束是作用于表中的字段上的&#xff0c;我们可以在创建表/修改表的时候添加约束。 目的&#xff1a;保证数据库中数据的正确&#xff0c;有效性和完整性。 常见约束&#xff1a; 举个例子&#xff1a;假…

Wrk压测发送Post请求的正确姿势

一、Wrk简介 wrk 是一个能够在单个多核 CPU 上产生显著负载的现代 HTTP 基准测试工具。它采用了多线程设计&#xff0c;并使用了像 epoll 和 kqueue 这样的可扩展事件通知机制。此外&#xff0c;用户可以指定 LuaJIT 脚本来完成 HTTP 请求生成、响应处理和自定义报告等功能。 …