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接收数据的感悟,希望对你有所帮助。

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

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

相关文章

【第4章 图像与视频】4.2 图像的缩放

文章目录 前言示例-图像的缩放在 Canvas 边界之外绘制图像 前言 在上节中读者已经学会了如何使用 drawImage() 方法将一幅未经缩放的图像绘制到 canvas 之中。现在我们就来看看如何用该方法在绘制图像的时候进行缩放 示例-图像的缩放 未缩放的图像&#xff0c;显示图形原有大…

[网页五子棋][用户模块]客户端开发(登录功能和注册功能)

文章目录 客户端开发登录功能htmlcsscommon.csslogin.css jQuery引入 jquery 运行程序注册功能 客户端开发 登录功能 html <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta name"viewport&…

【Doris基础】Apache Doris业务场景全解析:从实时数仓到OLAP分析的完美选择

目录 1 Doris核心能力概述 2 实时数据分析场景 2.1 实时数据仓库 2.2 实时监控与告警 3 交互式OLAP分析场景 3.1 自助式BI分析 3.2 用户行为分析 4 大数据分析场景 4.1 日志分析系统 4.2 时序数据处理 5 Doris技术架构适配性分析 5.1 适合Doris的场景特征 5.2 不适合Doris的场景…

C# 类和继承(类继承和访问继承的成员)

类继承 通过继承可以定义一个新类&#xff0c;新类纳入一个已经声明的类并进行扩展。 可以使用一个已经存在的类作为新类的基础。已存在的类称为基类&#xff08;base class&#xff09;&#xff0c;新类称 为派生类&#xff08;derived class&#xff09;。派生类成员的组成如…

ElasticSearch查询指定时间内出现的次数/2秒内出现的次数

查询指定时间内出现的次数 POST process-log/_search {"size": 0,"query": {"bool": {"filter": [{"range": {"requestTime": {"from": 1722470400000,"to": 1722556800000}}}]}},"agg…

第四十五节:目标检测与跟踪-Meanshift/Camshift 算法

引言 在计算机视觉领域,目标跟踪是实时视频分析、自动驾驶、人机交互等应用的核心技术之一。Meanshift和Camshift算法作为经典的跟踪方法,以其高效性和实用性广受关注。本文将从原理推导、OpenCV实现到实际案例,全面解析这两种算法的核心思想与技术细节。 一、Meanshift算法…

Typora-macOS 风格代码块

效果&#xff1a; 替换 Typora安装目录中 themes 文件夹下的 base.user.css 文件&#xff0c;直接替换即可&#xff0c;建议先备份。 css&#xff1a; /* 语法高亮配色 */ .CodeMirror-line .cm-number { color: #b5cea8; } /* 数字 - 浅绿色 */ .CodeMirror-line .…

【高频面试题】数组中的第K个最大元素(堆、快排进阶)

文章目录 数组中的第K个最大元素题目描述示例1示例2提示&#xff1a; 解法1&#xff08;堆维护前k大元素&#xff09;解法2 手写堆维护解法3&#xff08;快速选择算法&#xff09;例题&#xff1a;P1923 【深基9.例4】求第 k 小的数参考 数组中的第K个最大元素 题目描述 给定…

『uniapp』添加桌面长按快捷操作 shortcuts(详细图文注释)

目录 手机环境适配说明安卓效果图代码 iOS(暂未实测,没有水果开发者)总结 欢迎关注 『uniapp』 专栏&#xff0c;持续更新中 欢迎关注 『uniapp』 专栏&#xff0c;持续更新中 手机环境适配说明 个别手机系统可能需要进行特别的权限设置,否则会无法使用 桌面快捷方式: 已知的有…

PHP 垃圾回收高级特性

PHP 垃圾回收高级特性 1. 循环引用与内存泄漏 单纯的引用计数在遇到循环引用时会导致内存泄漏&#xff0c;主要原因是引用计数无法正确识别那些仅通过循环引用相互关联但实际上已经不可达的对象。 1.1 引用计数的基本原理 引用计数是一种内存管理机制&#xff0c;通过维护每…

奈雪小程序任务脚本

功能概述 该脚本用于自动完成奈雪点单小程序的每日任务&#xff0c;包括&#xff1a; 自动检测 Token 有效性自动签到&#xff08;如果未签到&#xff09;获取用户基础信息&#xff08;昵称、手机号&#xff09;查询当前奈雪币余额记录连续签到天数支持多账号执行&#xff0c…

基于cornerstone3D的dicom影像浏览器 第二十七章 设置vr相机,复位视图

文章目录 前言一、VR视图设置相机位置1. 相机位置参数2. 修改mprvr.js3. 调用流程1) 修改Toolbar3D.vue2) 修改View3d.vue3) 修改DisplayerArea3D.vue 二、所有视图复位1.复位流程说明2. 调用流程1) Toolbar3D中添加"复位"按钮&#xff0c;发送reset事件2) View3d.vu…

Opencv4 c++ 自用笔记 03 滑动条、相机与视频操作

1. 相机与视频操作 1.1 打开视频&#xff0f;相机 OpenCV 中 imread() 只能读取静态图像&#xff0c;若要读取视频文件或摄像头流&#xff0c;需要使用 VideoCapture 类&#xff1a; // 构造函数 cv::VideoCapture::VideoCapture(); cv::VideoCapture…

身份证发给别人怎么加水印?赛文奥特曼身份证添加水印教程

我们经常需要使用身份证照片进行身份验证、资料提交等操作。然而&#xff0c;直接将身份证照片发送给他人或上传到网络存在一定的信息泄露风险。为了更好地保护个人隐私&#xff0c;我们可以使用 简鹿水印助手 这款工具&#xff0c;在身份证照片上添加专属水印&#xff0c;从而…

十、【核心功能篇】项目与模块管理:前端页面开发与后端 API 联调实战

【核心功能篇】项目与模块管理&#xff1a;前端页面开发与后端 API 联调实战 前言准备工作第一部分&#xff1a;完善项目管理功能 (Project)1. 创建/编辑项目的表单对话框组件 第二部分&#xff1a;模块管理功能 (集成到项目详情页)1. 创建模块相关的 API 服务 (src/api/module…

ES分词搜索

ES的使用 前言作者使用的版本作者需求 简介ES简略介绍ik分词器简介 使用es的直接简单使用es的查询 es在java中使用备注说明 前言 作者使用的版本 es: 7.17.27spring-boot-starter-data-elasticsearch: 7.14.2 作者需求 作者接到一个业务需求&#xff0c;我们系统有份数据被…

Axure设计案例——科技感立体柱状图

想让你的数据展示告别平淡无奇&#xff0c;成为吸引全场目光的焦点吗&#xff1f;快来瞧瞧这个Axure设计的科技感立体柱状图案例&#xff01;科技感设计风格借助逼真的立体效果打破传统柱状图的平面感&#xff0c;营造出一种令人眼前一亮的视觉震撼。每一个柱状体都仿佛是真实存…

恶意npm与VS Code包窃取数据及加密货币资产

60个npm包窃取系统敏感信息 安全研究人员在npm软件包注册表中发现60个恶意组件&#xff0c;这些组件能够收集主机名、IP地址、DNS服务器和用户目录信息&#xff0c;并将其发送至Discord平台控制的终端节点。据Socket安全研究员Kirill Boychenko上周发布的报告显示&#xff0c;…

leetcode 2359. 找到离给定两个节点最近的节点

给你一个 n 个节点的 有向图 &#xff0c;节点编号为 0 到 n - 1 &#xff0c;每个节点 至多 有一条出边。 有向图用大小为 n 下标从 0 开始的数组 edges 表示&#xff0c;表示节点 i 有一条有向边指向 edges[i] 。如果节点 i 没有出边&#xff0c;那么 edges[i] -1 。 同时…

1. pytorch手写数字预测

1. pytorch手写数字预测 1.背景2.准备数据集2.定义模型3.dataloader和训练4.训练模型5.测试模型6.保存模型 1.背景 因为自身的研究方向是多模态目标跟踪&#xff0c;突然对其他的视觉方向产生了兴趣&#xff0c;所以心血来潮的回到最经典的视觉任务手写数字预测上来&#xff0…