当前位置: 首页 > article >正文

Android Studio里的BLE数据接收策略

#本人是初次接触Android蓝牙开发,若有不对地方,欢迎指出。

#由于是讲接收数据策略(其中还包含数据发送的部分策略),因此其他问题部分不会讲述,只描述数据接收。

简介(对于客户端---手机端)

博主在处理数据接收的时候,发现BLE的数据接收有很多不一样的地方,就是BLE的数据接收是采用它自己的MTU格式,目前一次BLE数据接收只能接收20字节数据,那么我们一旦涉及到超出20字节的数据,那么BLE的接收数据是按照20字节来划分 --- 我们想要发送的数据,那么这个完整的数据被分成多个包的数据包。我们就是想处理完整的数据,而不是处理某一个单一的数据包,所以我们遇到的难点就是——怎么将接收到分开的数据包,整理成完整的数据来进行处理。

对于MTU的解释

从字面上来说,MTU 是英文 Maximum Transmission Unit 的缩写,即最大传输单元,它的单位是字节,指的是数据链路层的最大payload,由硬件网卡设置MTU,是一个硬性限制。

1.数据接收部分

引用别的博主的对于BLE的解释——这里我就简要介绍一下。

对于BLE最重要的部分就是 GATT回调(处理连接和服务发现,以及数据接收以及发送)。所以我们目前要用到 BluetoothGattCallback 以及 它里面的回调函数。

 private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {@Overridepublic void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {}@Overridepublic void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {}};

对于gattCallback 它的两个回调函数有---

@Override

onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {}

@Override
   public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {}

第一个是用于数据接收,也就是我们这个blog的重点部分,它也是一次只能接收一个20字节的数据分包。

第二个是用于处理数据是否写出,当你的数据被发送出去的时候会进行回调。(注意了,它也是遵循MTU的,它也是一次性只能发送20个字节。)

2.对于onCharacteristicChanged的处理

首先可以看看我的写的案例。

这个函数仍然是在

private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {

    //我们的 onCharacteristicChanged()在这里面进行重写。

};

onCharacteristicChanged
        @Overridepublic void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {// value为设备发送的数据,根据数据协议进行解析byte[] value = characteristic.getValue();String received = new String(value);processReceivedData(value);Log.i(TAG,"接收到数据: " + received);
//            Log.d(TAG, "收到分段数据: " + new String(value, StandardCharsets.UTF_8));
//            runOnUiThread(() -> processReceivedData(value));}

 注释的部分不用管,这个是我进行测试时候用的。

首先,我们可以看到value是为对应的BLE设备发送过来的数据,我们使用characteristic.getValue()接收,你可以看到这个函数里的变量characteristic 这个不用管,你直接使用这个函数就可以了。

然后,我们接收到的数据,还是byte,就是字节格式的数据包。

现在有两种方式来进行数据包整合:

  1. 字节直接整合为完整数据。
  2. 将字节先转换为字符串格式后再整合为字节完整数据。

根据自己的需求来进行选择,byte格式是可以转换为字节格式的。

1.对于字节直接整合为完整数据

// 首先在gattCallback这个变量外面 先申明一个全局变量
private List<byte[]> packets = new ArrayList<>();//作为数据缓存存储@Overridepublic void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {byte[] value = characteristic.getValue();packets.add(value);//  这里加入你自己的数据末尾判断if(End){byte[] Alldata = GetWholeData(packets); //组成完整的数据/*下面同时可以加入你想在数据组合完整时,调用数据处理函数(可选部分)*/}}

对于数据整合函数

// 组装数据
private byte[] GetWholeData(List<byte[]> packets) {ByteArrayOutputStream DataStream = new ByteArrayOutputStream();for (byte[] packet : packets) {DataStream.write(packet);}return DataStream.toByteArray();
}

2.将字节先转换为字符串格式后再整合为字节完整数据

以我的案例为例子来进行讲解。

        @Overridepublic void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {// value为设备发送的数据,根据数据协议进行解析byte[] value = characteristic.getValue();
//             String received = new String(value);
//            可加可不加,我用来调试的/* 主要核心函数 这个兼具数据处理*/processReceivedData(value);//            Log.i(TAG,"接收到数据: " + received);}

这个函数其实和第一个差不多,主要是数据处理部分,不相同。

对于ProcessReceivedData函数

    private final StringBuilder dataBuffer = new StringBuilder();
// 对于dataBuffer是用来进行完整数据拼包的,全局变量private void processReceivedData(byte[] data) {String packet = new String(data, StandardCharsets.UTF_8);dataBuffer.append(packet);// 前两个部分,就是用来进行接收的数据包合包操作。 /* 后面是作为数据处理部分,你可以理解为前面的操作是合包只有真正完成数据包整合后,才会真正进行数据处理部分。下面的代码作为案例,你可以自己改写。
*/while (true) {int startIndex = dataBuffer.indexOf("#");if (startIndex == -1) {dataBuffer.setLength(0);break;}int endIndex = dataBuffer.indexOf("\n", startIndex);if (endIndex == -1) {String remaining = dataBuffer.substring(startIndex);dataBuffer.setLength(0);dataBuffer.append(remaining);break;}String rawData = dataBuffer.substring(startIndex, endIndex + 1);dataBuffer.delete(0, endIndex + 1);if (!rawData.startsWith("#") || !rawData.endsWith("\n")) {Log.e(TAG, "协议格式错误: " + rawData);continue;}try {String jsonStr = rawData.substring(1, rawData.length() - 1);JSONObject json = new JSONObject(jsonStr);double lat = json.getDouble("lat");double lon = json.getDouble("lon");float angle = (float) json.getDouble("angle");if (lastLat != null && lastLon != null) {CaneVectorControl.ControlCommand command = CaneVectorControl.computeControl(lastLat, lastLon, endLat, endLon, lat, lon);String jsonCommand = "#" + command.toJson() + "\n";sendCommand(jsonCommand);Log.i(TAG, "控制指令已发送: " + jsonCommand);} else {Log.e(TAG, "用户位置未更新,无法计算指令");}} catch (JSONException e) {Log.e(TAG, "JSON解析失败: " + e.getMessage() + "\n原始数据: " + rawData);}}}
if (!rawData.startsWith("#") || !rawData.endsWith("\n")) {Log.e(TAG, "协议格式错误: " + rawData);continue;
}

这一部分是用于检测发送的数据格式是否正确。上面的代码,可以按照注释理解,不符合条件就结束循环,提前进行下一次的数据包处理。

try {String jsonStr = rawData.substring(1, rawData.length() - 1);JSONObject json = new JSONObject(jsonStr);double lat = json.getDouble("lat");double lon = json.getDouble("lon");float angle = (float) json.getDouble("angle");if (lastLat != null && lastLon != null) {CaneVectorControl.ControlCommand command = CaneVectorControl.computeControl(lastLat, lastLon, endLat, endLon, lat, lon);String jsonCommand = "#" + command.toJson() + "\n";sendCommand(jsonCommand);Log.i(TAG, "控制指令已发送: " + jsonCommand);} else {Log.e(TAG, "用户位置未更新,无法计算指令");}
} catch (JSONException e) {Log.e(TAG, "JSON解析失败: " + e.getMessage() + "\n原始数据: " + rawData);
}

对于 try{}catch{}部分就是我所提及的数据处理部分。  你可以自己更改。

对于里面的一个 sendCommand(jsonCommand) 是处理完数据后发送相应的数据。

这里我再加上我们一般都会处理完数据后发送想要的相应部分。

3.对于数据接收处理后,数据发送部分

可以看到之前博主提及过的

@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {if (status == BluetoothGatt.GATT_SUCCESS) {Log.i(TAG, "写入成功:" + new String(characteristic.getValue(), StandardCharsets.UTF_8));sendNextPacket();} else {Log.e(TAG, "写入失败,状态码: " + status);}
}

这个就是我们接下来要使用的数据发送回调。

先看我写的完整例子。

SendCommand函数

    private Queue<byte[]> writeQueue = new LinkedList<>();@SuppressLint("MissingPermission")public void sendCommand(String command) {if (bluetoothGatt == null || writeCharateristic == null) {Log.e(TAG, "GATT未连接或写特征不可用");return;}byte[] data = command.getBytes(StandardCharsets.UTF_8);int maxLength = 20; // 或 MTU - 3 // 博主使用的是HC-08 BLE 它的MTU大小为20字节// 清空队列并分包writeQueue.clear();for (int i = 0; i < data.length; i += maxLength) {int end = Math.min(data.length, i + maxLength);byte[] chunk = Arrays.copyOfRange(data, i, end);writeQueue.offer(chunk);}// 发送第一个包sendNextPacket();}

sendNextPacket()函数

    @SuppressLint("MissingPermission")private void sendNextPacket() {if (writeQueue.isEmpty()) return;byte[] chunk = writeQueue.poll();writeCharateristic.setValue(chunk);boolean success = bluetoothGatt.writeCharacteristic(writeCharateristic);Log.i(TAG, "发送分包: " + new String(chunk, StandardCharsets.UTF_8) + ",成功: " + success);// 如果写失败,考虑重试机制}

onCharacteristicWrite()函数

        @Overridepublic void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {if (status == BluetoothGatt.GATT_SUCCESS) {Log.i(TAG, "写入成功:" + new String(characteristic.getValue(), StandardCharsets.UTF_8));sendNextPacket();
// 如果是还没有发送完数据,那么就继续发送它的下一个数据包} else {Log.e(TAG, "写入失败,状态码: " + status);}}

问题的本质就是:

BLE 发送数据包时,每包最大只能发 20 字节(传统 BLE 协议限制),你的完整字符串超过这个长度就会被自动分包。

解决方案:和接收端一样,用缓存缓冲区来拼接这些碎片

我们在 发送端什么都不用改(只要发送时不要太快),只需要在 接收端缓冲所有片段,直到遇到 \n 表示完整数据包结尾。(这里可以根据你自己定义的协议更改结尾)。

以上就是我作为小白开发BLE接收数据的感悟,希望对你有所帮助。

相关文章:

Android Studio里的BLE数据接收策略

#本人是初次接触Android蓝牙开发&#xff0c;若有不对地方&#xff0c;欢迎指出。 #由于是讲接收数据策略(其中还包含数据发送的部分策略)&#xff0c;因此其他问题部分不会讲述&#xff0c;只描述数据接收。 简介(对于客户端---手机端) 博主在处理数据接收的时候&#xff0…...

【Office】Excel两列数据比较方法总结

在Excel中&#xff0c;比较两列数据是否相等有多种方法&#xff0c;以下是常用的几种方式&#xff1a; 方法1&#xff1a;使用公式&#xff08;返回TRUE/FALSE&#xff09; 在空白列&#xff08;如C列&#xff09;输入公式&#xff0c;向下填充即可逐行比较两列&#xff08;如…...

基于多模态脑电、音频与视觉信号的情感识别算法【Nature核心期刊,EAV:EEG-音频-视频数据集】

简述 理解情感状态对于开发下一代人机交互界面至关重要。社交互动中的人类行为会引发受感知输入影响的心理生理过程。因此&#xff0c;探索大脑功能与人类行为的努力或将推动具有类人特质人工智能模型的发展。这里原作者推出一个多模态情感数据集&#xff0c;包含42名参与者的3…...

【QueryServer】dbeaver使用phoenix连接Hbase(轻客户端方式)

一、轻客户端连接方式 (推荐) 演示无认证配置方式, 有认证填入下方有认证参数即可 1, 新建连接 → Hadoop/大数据 → Apache Phoenix 2, 手动配置QueryServer驱动: 填入: “类名”, “URL模版”(注意区分有无认证), “端口号”, (勾选无认证) 类名: org.apache.phoenix…...

数据湖 (特点+与数据仓库和数据沼泽的对比讲解)

数据湖就像一个“数据水库”&#xff0c;把企业所有原始数据&#xff08;结构化的表格、半结构化的日志、非结构化的图片/视频&#xff09;原样存储&#xff0c;供后续按需分析。 对比传统数据仓库&#xff1a; 数据仓库数据湖数据清洗后的结构化数据&#xff08;如Excel表格&…...

深入链表剖析:从原理到 C 语言实现,涵盖单向、双向及循环链表全解析

1 引言 在数据结构的学习中&#xff0c;链表是一种基础且极为重要的线性数据结构。与数组不同&#xff0c;链表通过指针将一系列节点连接起来&#xff0c;每个节点包含数据域和指向下一个节点的指针域。这种动态的存储方式使得链表在插入、删除等操作上具有独特的优势。本文将深…...

编码总结如下

VS2019一般的编码是UTF-8编码&#xff0c; win11操作系统的编码可能为GB2312&#xff0c;VS整个工程中使用的都是UTF-8编码&#xff0c;但是在系统内生成的其他文件夹的名字则是系统的编码 如何选择&#xff1f; Qt 项目&#xff1a;优先用 QString 和 QByteArray&#xff08;…...

《算力觉醒!ONNX Runtime + DirectML如何点燃Windows ARM设备的AI引擎》

ONNX Runtime是一个跨平台的高性能推理引擎&#xff0c;它就像是一位精通多种语言的翻译官&#xff0c;能够无缝运行来自不同深度学习框架转化为ONNX格式的模型。这种兼容性打破了框架之间的隔阂&#xff0c;让开发者可以将更多的精力投入到模型的优化和应用中。 从内部机制来…...

[9-1] USART串口协议 江协科技学习笔记(13个知识点)

1 2 3 4全双工就是两个数据线&#xff0c;半双工就是一个数据线 5 6 7 8 9 10 TTL&#xff08;Transistor-Transistor Logic&#xff09;电平是一种数字电路中常用的电平标准&#xff0c;它使用晶体管来表示逻辑状态。TTL电平通常指的是5V逻辑电平&#xff0c;其中&#xff1a;…...

Oracle基础知识(五)——ROWID ROWNUM

目录 一、ROWID 伪列 二、ROWNUM——限制查询结果集行数 1.ROWNUM使用介绍 2.使用ROWNUM进行分页查询 3.使用ROWNUM查看薪资前五位的员工 4.查询指定条数直接的数据 三、ROWNUM与ROWID不同 一、ROWID 伪列 表中的每一行在数据文件中都有一个物理地址&#xff0c;ROWID…...

简述synchronized和java.util.concurrent.locks.Lock的异同 ?

主要相同点&#xff1a; Lock能完成synchronized所实现的所有功能。 主要不同点&#xff1a; Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁&#xff0c;而Lock一定要求程序员手工释放&#xff0c;并且必须在finally从句中释放Lock还有更强大…...

OpenCV CUDA模块直方图计算------在 GPU 上计算图像直方图的函数calcHist()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 OpenCV 的 CUDA 模块 中用于在 GPU 上计算图像直方图的一个函数。 计算单通道 8-bit 图像的灰度直方图&#xff08;Histogram&#xff09;。 该函…...

EMS只是快递那个EMS吗?它跟能源有什么关系?

在刚刚落幕的深圳人工智能终端展上&#xff0c;不少企业展示了与数字能源相关的技术和服务&#xff0c;其中一项关键系统——EMS&#xff08;Energy Management System&#xff0c;能量管理系统&#xff09;频频亮相。这个看似低调的名字&#xff0c;实际上正悄然成为未来能源管…...

日志技术-LogBack、Logback快速入门、Logback配置文件、Logback日志级别

一. 日志技术 1. 程序中的日志&#xff0c;是用来记录应用程序的运行信息、状态信息、错误信息等。 2. JUL&#xff1a;(java.util.logging)这是JavaSE平台提供的官方日志框架&#xff0c;也被称为JUL。配置相对简单&#xff0c;但不够灵活&#xff0c;性能较差。 3.Logs4j&…...

修改Cinnamon主题

~/.themes/Brunnera-Dark/cinnamon/cinnamon.css 1.修改 Tooltip 圆角大小&#xff0c;边框颜色&#xff0c;背景透明度 #Tooltip { border-radius: 10px; color: rgba(255, 255, 255, 0.8); border: 1px solid rgba(255, 255, 255, 0.6); background-color: rgba(0,…...

91.评论日记

2025年5月30日20:27:06 AI画减速器图纸&#xff1f; 呜呜为什么读到机械博士毕业了才有啊 | 新迪数字2025新品发布会 | AI工业软件 | 三维CAD | 国产自主_哔哩哔哩_bilibili...

HTML5实现简洁的端午节节日网站源码

HTML5实现简洁的端午节节日网站源码 前言一、设计来源1.1 网站首页界面1.2 端午由来界面1.3 节日活动界面1.4 传统美食界面1.5 民俗文化界面1.6 登录界面1.7 注册界面 二、效果和源码2.1 动态效果2.2 源代码 结束语 HTML5实现简洁的端午节节日网站源码&#xff0c;酷炫的大气简…...

Window10+ 安装 go环境

一、 下载 golang 源码&#xff1a; 去官网下载&#xff1a; https://go.dev/dl/ &#xff0c;当前时间&#xff08;2025-05&#xff09;最新版本如下: 二、 首先在指定的磁盘下创建几个文件夹 比如在 E盘创建 software 文件夹 E:\SoftWare,然后在创建如下几个文件夹 E:\S…...

AWS WebRTC:获取ICE服务地址(part 2): ICE Agent的作用

上一篇&#xff0c;已经获取到了ICE服务地址&#xff0c;从返回结果中看&#xff0c;是两组TURN服务地址。 拿到这些地址有什么用呢&#xff1f;接下来就要说到WebRTC中ICE Agent的作用了&#xff0c;返回的服务地址会传给WebRTC最终给到ICE Agent。 ICE Agent的作用&#xf…...

一、Sqoop历史发展及原理

作者&#xff1a;IvanCodes 日期&#xff1a;2025年5月30日 专栏&#xff1a;Sqoop教程 在大数据时代&#xff0c;数据往往分散存储在各种不同类型的系统中。其中&#xff0c;传统的关系型数据库 (RDBMS) 如 MySQL, Oracle, PostgreSQL 等&#xff0c;仍然承载着大量的关键业务…...

React 编译器 RC

&#x1f916; 作者简介&#xff1a;水煮白菜王&#xff0c;一位前端劝退师 &#x1f47b; &#x1f440; 文章专栏&#xff1a; 前端专栏 &#xff0c;记录一下平时在博客写作中&#xff0c;总结出的一些开发技巧和知识归纳总结✍。 感谢支持&#x1f495;&#x1f495;&#…...

PyTorch 中mm和bmm函数的使用详解

torch.mm 是 PyTorch 中用于 二维矩阵乘法&#xff08;matrix-matrix multiplication&#xff09; 的函数&#xff0c;等价于数学中的 A B 矩阵乘积。 一、函数定义 torch.mm(input, mat2) → Tensor执行的是两个 2D Tensor&#xff08;矩阵&#xff09;的标准矩阵乘法。 in…...

关于表连接

目录 1.左连接 2.右连接 3.内连接 4.全外连接 5.笛卡尔积 -- 创建表A CREATE TABLE A(PNO VARCHAR2(10) PRIMARY KEY, PAMT NUMBER, A_DATE DATE);-- 向表A插入数据 INSERT INTO A VALUES (01001, 100, TO_DATE(2005-01-01, YYYY-MM-DD)); INSERT INTO A VALUES (010…...

【计算机网络】fork()+exec()创建新进程(僵尸进程及孤儿进程)

文章目录 一、基本概念1. fork() 系统调用2. exec() 系列函数 二、典型使用场景1. 创建子进程执行新程序2. 父子进程执行不同代码 三、核心区别与注意事项四、组合使用技巧1. 重定向子进程的输入/输出2. 创建多级子进程 五、常见问题与解决方案僵尸进程&#xff08;Zombie Proc…...

QPS 和 TPS 详解

QPS 和 TPS 是性能测试中的两个核心指标&#xff0c;用于衡量系统的吞吐能力&#xff0c;但关注点不同。以下是具体解析&#xff1a; 1. QPS&#xff08;Queries Per Second&#xff09; 定义&#xff1a;每秒查询数&#xff0c;表示系统每秒能处理的请求数量&#xff08;无论…...

Word表格怎样插入自动序号或编号

在Word文档中编辑表格时&#xff0c;经常需要为表格添加序号或编号&#xff0c;可以设置为自动序号或编号&#xff0c;当删除行时&#xff0c;编号会自动变化&#xff0c;不用手工再重新编号。如图所示。 序号数据1数据21300300230030033003004300300 一&#xff0c;建立word表…...

数据结构:导论

目录 什么是“第一性原理”&#xff1f; 什么是“数据结构”&#xff1f; 数据结构解决的根本问题是什么&#xff1f; 数据结构的两大分类 数据结构的基本操作 数据结构与算法的关系 学习数据结构的底层目标 什么是“第一性原理”&#xff1f; 在正式进入数据结构之前&…...

青少年编程与数学 02-020 C#程序设计基础 13课题、数据访问

青少年编程与数学 02-020 C#程序设计基础 13课题、数据访问 一、使用数据库1. 使用ADO.NET连接数据库连接SQL Server示例连接其他数据库 2. 使用Entity Framework (EF Core)安装EF Core示例代码 3. 数据绑定到WinForms控件DataGridView绑定简单控件绑定 4. 使用本地数据库(SQLi…...

无人机仿真环境(3维)附项目git链接

项目概述 随着无人机技术在物流、测绘、应急救援等领域的广泛应用&#xff0c;其自主导航、避障算法、路径规划及多机协同等核心技术的研究需求日益迫切。为降低实地测试成本、提高研发效率&#xff0c;本项目旨在构建一个高精度、可扩展的​​无人机三维虚拟仿真环境​​&…...

湖北理元理律师事务所:债务优化中的“生活锚点”设计

在债务重组领域&#xff0c;一个常被忽视的核心矛盾是&#xff1a;还款能力与生存需求的冲突。过度压缩生活支出还债&#xff0c;可能导致收入中断&#xff1b;放任债务膨胀&#xff0c;又加剧精神压力。湖北理元理律师事务所通过“三步平衡法”&#xff0c;尝试在法理框架内破…...