news 2026/9/13 14:32:03

Android经典蓝牙SPP通信开发与调试实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android经典蓝牙SPP通信开发与调试实战指南

简介:这是一款基于Android Studio开发的蓝牙串口通信调试助手源码项目,面向Android应用开发者、嵌入式通信初学者及物联网设备联调人员,用于快速实现手机端与蓝牙串口模块(如HC-05/HC-06)的数据收发、连接管理与状态监控。资源包共946个文件,涵盖165个Java类、54个XML布局与配置文件、273个JSON配置及协议定义、232个rawproto序列化描述,以及APK构建产物(如resources-debug.ap_)、DEX字节码和Gradle构建脚本等,完整呈现从UI设计、蓝牙权限适配、设备发现(含onActivityResult设备地址提取逻辑)、Socket连接到数据解析的全链路实现。压缩包大小为14.12MB,结构规范,便于二次开发与协议扩展。已有1524人学习下载,读者可直接导入Android Studio编译运行,获取可调试的完整工程、清晰的蓝牙连接状态回调处理范式、典型异常捕获逻辑及适配Android 6.0+动态权限的实践方案。

1. 这不是普通串口工具——它是一套可深度定制的蓝牙通信底层链路验证框架

很多开发者在调试 HC-05、HC-06 或 JDY 系列蓝牙模块时,卡在“连上了但收不到数据”“发了指令没响应”“断连后重连失败”这类问题上。市面上的串口助手 App(如 SSCom、XCOM、工具侠)能快速收发 ASCII/HEX,但无法查看 BluetoothSocket 的连接状态机、无法干预 UUID 绑定逻辑、无法复现onActivityResult中设备地址传递异常、更无法修改BluetoothAdapter.getDefaultAdapter()的初始化策略。而这份「蓝牙串口助手(Android Studio源码)」恰恰填补了这个断层:它不是一个黑盒应用,而是一个完整可编译、可断点、可注入日志、可替换协议栈的 Android 蓝牙通信最小可行系统(MVP)。它基于原生 Bluetooth API(非 BLE),覆盖经典蓝牙 SPP 协议栈全流程——从startActivityForResult扫描设备、createRfcommSocketToServiceRecord建立 RFCOMM 通道、到InputStream.read()阻塞读取与OutputStream.write()异步写入。适合嵌入式联调工程师、Android 底层通信开发者、以及需要将蓝牙串口能力集成进自有 App 的中高级 Android 工程师。你拿到的不是 APK,而是可直接导入 Android Studio 的工程结构,含build.gradleAndroidManifest.xml权限声明、res/layout界面定义及完整的BluetoothChatService核心服务类。

2. 从权限声明到设备发现:SPP 协议下经典蓝牙通信的初始化闭环

2.1 清单文件中的关键权限与硬件声明必须显式配置

Android 12(API 31)起,蓝牙扫描需额外申请BLUETOOTH_SCAN运行时权限,且AndroidManifest.xml中必须声明uses-permissionuses-feature。该源码中AndroidManifest.xml的典型配置如下:

<uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-feature android:name="android.hardware.bluetooth" android:required="true" />

注意:ACCESS_FINE_LOCATION是 Android 6.0+ 扫描必需权限,即使不使用 GPS;BLUETOOTH_SCANBLUETOOTH_CONNECT在 targetSdkVersion ≥ 31 时不可省略,否则startDiscovery()直接抛 SecurityException。若目标设备为 Android 10 及以下,可移除后两项,但需保留BLUETOOTH_ADMIN

2.2 DeviceListActivity:基于 startActivityForResult 的设备选择机制解析

源码中DeviceListActivity是设备发现核心界面,其启动方式为:

Intent serverIntent = new Intent(this, DeviceListActivity.class); startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE);

该 Activity 内部通过BluetoothAdapter.startDiscovery()触发扫描,并注册BroadcastReceiver监听BluetoothDevice.ACTION_FOUNDBluetoothAdapter.ACTION_DISCOVERY_FINISHED。关键点在于结果回传逻辑:

// DeviceListActivity.java 中点击设备项时触发 Intent intent = new Intent(); intent.putExtra(DeviceListActivity.DEVICE_ADDRESS, device.getAddress()); setResult(Activity.RESULT_OK, intent); finish();

此处DEVICE_ADDRESS是自定义常量字符串(如"device_address"),用于在onActivityResult中提取 MAC 地址。常见坑点:若DeviceListActivity未正确调用setResult(),或putExtra键名拼写错误(如大小写不一致),则data.getExtras().getString(...)返回 null,导致mBluetoothAdapter.getRemoteDevice(address)IllegalArgumentException

2.3 onActivityResult 中的地址解析与远程设备获取

回到主 Activity 的onActivityResult回调(如题干所给代码段),其执行流程需严格遵循三步:

@Override public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.e(TAG, "onActivityResult"); switch (requestCode) { case REQUEST_CONNECT_DEVICE: if (resultCode == Activity.RESULT_OK) { // Step 1: 安全提取地址,避免空指针 String address = data != null ? data.getStringExtra(DeviceListActivity.DEVICE_ADDRESS) : null; if (address == null || !BluetoothAdapter.checkBluetoothAddress(address)) { Log.e(TAG, "Invalid Bluetooth address: " + address); return; } // Step 2: 获取远程设备对象(仅地址有效,不触发连接) BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); // Step 3: 启动连接线程(通常在此处 new ConnectThread(device).start()) connectToDevice(device); } break; } }
  • BluetoothAdapter.checkBluetoothAddress(address)是 Android 提供的校验方法,确保 MAC 地址格式为XX:XX:XX:XX:XX:XX
  • getRemoteDevice(address)仅生成BluetoothDevice实例,不建立物理连接,真正连接需调用device.createRfcommSocketToServiceRecord(UUID)并执行socket.connect()
  • 若跳过地址校验直接传入非法字符串,getRemoteDevice会抛出IllegalArgumentException,导致 Activity 崩溃。

2.4 UUID 绑定:SPP 协议的硬性要求与兼容性处理

SPP(Serial Port Profile)服务固定使用标准 UUID:00001101-0000-1000-8000-00805F9B34FB。源码中创建 socket 的典型写法为:

private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); ... BluetoothSocket socket = device.createRfcommSocketToServiceRecord(MY_UUID);

提示:部分国产模块(如杰理 AC692x)可能使用自定义 UUID,此时需与模块厂商确认并替换MY_UUID。若 UUID 不匹配,connect()将超时失败(IOException: read failed, socket might closed),而非立即报错。

3. 连接管理与数据收发:BluetoothChatService 的线程模型与缓冲区控制

3.1 ConnectThread:阻塞式连接与异常恢复策略

ConnectThread继承Thread,核心逻辑封装在run()方法中:

public void run() { mBluetoothAdapter.cancelDiscovery(); // 取消扫描,释放资源 try { mmSocket.connect(); // 阻塞调用,超时默认 12 秒 synchronized (BluetoothChatService.this) { mConnectThread = null; } connected(mmSocket, mmDevice); // 连接成功回调 } catch (IOException e) { connectionFailed(); // 失败处理:关闭 socket,通知 UI try { mmSocket.close(); } catch (IOException e2) { Log.e(TAG, "unable to close() socket during connection failure", e2); } } }
  • cancelDiscovery()必须在connect()前调用,否则在扫描状态下建连成功率极低;
  • connect()是同步阻塞操作,无内置超时参数,实际超时由底层协议栈决定(通常 10–15 秒),因此需在 UI 层提供“连接中…”提示并禁用按钮;
  • 异常捕获后必须显式close(),否则 socket 句柄泄漏,后续连接可能因资源耗尽失败。

3.2 ConnectedThread:双缓冲区读写与粘包处理

ConnectedThread负责长连接下的数据收发,其run()方法采用InputStream阻塞读取 +OutputStream写入模式:

public void run() { byte[] buffer = new byte[1024]; int bytes; while (true) { try { bytes = mmInStream.read(buffer); // 阻塞读取 if (bytes > 0) { // 使用 Handler 发送至主线程更新 UI mHandler.obtainMessage(BluetoothChat.MESSAGE_READ, bytes, -1, buffer) .sendToTarget(); } } catch (IOException e) { connectionLost(); // 断连处理 break; } } }
  • mmInStream.read(buffer)返回实际读取字节数,非 buffer.length,必须用bytes截取有效数据;
  • 源码中未实现粘包处理(即多条指令合并为一次read),若设备端以\n\r\n分隔指令,需在MESSAGE_READ处理逻辑中按分隔符切分buffer[0..bytes]
  • mHandler通常绑定主线程 Looper,避免在子线程直接操作 View。

3.3 数据发送:write() 方法的线程安全与缓冲区刷新

发送数据通过write(byte[])方法实现:

public void write(byte[] out) { if (D) Log.d(TAG, "write: " + out.length); OutputStream mmOutStream = this.mmOutStream; if (mmOutStream == null) return; try { mmOutStream.write(out); mmOutStream.flush(); // 关键:强制刷新输出缓冲区 } catch (IOException e) { Log.e(TAG, "Exception during write", e); } }
  • flush()调用至关重要:Android 的OutputStream默认启用缓冲,若不显式flush(),数据可能滞留在内核缓冲区,导致设备端长时间收不到指令;
  • 若需发送 HEX 字符串(如"AA5500FF"),需先转换为字节数组:hexStringToBytes("AA5500FF"),避免直接getBytes()产生 ASCII 编码乱码。

3.4 连接状态机与生命周期管理表

状态触发条件主要动作UI 响应建议
STATE_NONE初始状态显示“未连接”,启用“扫描设备”按钮
STATE_CONNECTINGconnect()调用后启动ConnectThread显示“正在连接…”,禁用所有操作按钮
STATE_CONNECTEDconnected()被调用启动ConnectedThread,注册Handler显示“已连接”,启用发送框与清屏按钮
STATE_LISTEN作为服务端监听时启动AcceptThread适用于 PC 端模拟蓝牙串口服务器场景

提示:源码中BluetoothChatService通常只实现客户端角色(主动连接),若需服务端监听(如手机作为蓝牙串口服务器被 PC 连接),需补充AcceptThread并在onStartCommand中启动。

4. 调试实战:解决 hc05 模块连接不上与数据乱码的四大高频问题

4.1 HC-05 连接失败:配对、波特率与 AT 指令预置检查清单

HC-05 模块在出厂时处于从机模式,需确保以下三点:

  1. 物理配对完成:手机蓝牙设置中已与 HC-05 配对(PIN 码通常为12340000),且状态为“已配对”而非“可用设备”;
  2. 模块工作模式正确:HC-05 默认为“从机”,若需手机主动连接,无需更改;若模块设为“主机”,则无法被手机发现;
  3. AT 指令预置验证:使用 USB-TTL 模块通过串口助手发送AT,确认返回OK;再发AT+UART?查看当前波特率(常见为9600),App 中发送数据的波特率必须与模块一致,否则表现为“能连上但收不到数据”。

注意:Android 端无波特率设置接口,波特率由蓝牙模块自身决定,App 仅负责数据帧收发。若模块波特率被误设为38400而 App 仍按9600发送,数据必然乱码。

4.2 数据乱码根因分析与十六进制收发验证法

乱码常见于以下场景:

  • 设备端发送的是二进制数据(如传感器原始值0xAA, 0x55, 0x01),但 App 以 UTF-8 解析为字符串U\u0001
  • 模块返回数据含不可见控制字符(如\0,\r,\b),TextView 自动过滤显示为空白。

验证步骤

  1. 修改MESSAGE_READ处理逻辑,将buffer[0..bytes]转为 HEX 字符串:
    StringBuilder hex = new StringBuilder(); for (int i = 0; i < bytes; i++) { hex.append(String.format("%02X ", buffer[i])); } Log.d(TAG, "HEX received: " + hex.toString());
  2. 对比串口助手(如 XCOM)的 HEX 显示模式,确认是否一致;
  3. 若 HEX 一致但字符串显示异常,说明是编码问题,应改用new String(buffer, 0, bytes, StandardCharsets.ISO_8859_1)解析二进制流。

4.3 连接后立即断开:Socket 生命周期与 Activity 重建冲突

当用户旋转屏幕或切换后台,Activity 可能被销毁重建,但ConnectedThread仍在后台运行,导致mmOutStream被关闭后write()IOException。解决方案是将BluetoothChatService提升为独立 Service(非内部类),并通过bindService()绑定,确保其生命周期独立于 Activity:

// 在 AndroidManifest.xml 中声明 <service android:name=".BluetoothChatService" android:exported="false" />

并在 Activity 中:

private BluetoothChatService mService; private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { BluetoothChatService.LocalBinder binder = (BluetoothChatService.LocalBinder) service; mService = binder.getService(); mService.setHandler(mHandler); // 注入 Handler } // ... };

4.4 日志定位法:关键 Logcat 过滤命令与典型输出解读

在终端执行以下命令,聚焦蓝牙通信链路:

adb logcat -s BluetoothChat:D BluetoothChatService:D BluetoothAdapter:D

典型正常流程日志:

D/BluetoothChat: onActivityResult D/BluetoothChat: Connecting to device: 00:11:22:33:44:55 D/BluetoothChatService: setState() 2 -> 3 D/BluetoothChatService: Connected to: 00:11:22:33:44:55 D/BluetoothChat: HEX received: AA 55 01 00 FF

若出现D/BluetoothAdapter: createRfcommSocketToServiceRecord后无后续,说明connect()阻塞中;若出现E/BluetoothChatService: Connection lost,则需检查模块供电、距离、配对状态。

5. 进阶技巧:向源码注入 CRC 校验与指令超时重发机制

5.1 在发送前添加 CRC16-CCITT 校验字节

为提升通信鲁棒性,可在write(byte[])前计算 CRC 并追加。以 CRC16-CCITT(0xFFFF 初始化,多项式 0x1021)为例:

public static byte[] addCrc16(byte[] data) { int crc = 0xFFFF; for (byte b : data) { crc ^= (b & 0xFF) << 8; for (int i = 0; i < 8; i++) { if ((crc & 0x8000) != 0) { crc = (crc << 1) ^ 0x1021; } else { crc <<= 1; } } } crc &= 0xFFFF; byte[] result = new byte[data.length + 2]; System.arraycopy(data, 0, result, 0, data.length); result[data.length] = (byte) ((crc >> 8) & 0xFF); result[data.length + 1] = (byte) (crc & 0xFF); return result; }

调用方式:

byte[] cmd = {0xAA, 0x55, 0x01}; mChatService.write(addCrc16(cmd)); // 发送带校验的指令

5.2 实现带超时的指令重发队列

针对关键指令(如设备复位、参数设置),可构建CommandQueue类,支持自动重发:

public class CommandQueue { private final Queue<Command> queue = new ConcurrentLinkedQueue<>(); private final Handler handler = new Handler(Looper.getMainLooper()); public void sendWithRetry(byte[] cmd, int maxRetries, long timeoutMs) { Command c = new Command(cmd, maxRetries, timeoutMs); queue.add(c); handler.postDelayed(c::execute, 0); } private class Command implements Runnable { private final byte[] data; private final int maxRetries; private int retries; private final long timeoutMs; Command(byte[] data, int maxRetries, long timeoutMs) { this.data = data; this.maxRetries = maxRetries; this.timeoutMs = timeoutMs; } @Override public void run() { if (retries >= maxRetries) { Log.e(TAG, "Command failed after " + maxRetries + " retries"); return; } mChatService.write(data); handler.postDelayed(() -> { if (!ackReceived()) { // 需实现 ackReceived() 检查应答 retries++; handler.post(this); // 重发 } }, timeoutMs); } } }

此机制将重发逻辑从业务代码中解耦,只需调用sendWithRetry(cmd, 3, 2000)即可实现最多 3 次、每次间隔 2 秒的自动重发,显著提升工业现场通信可靠性。

5.3 蓝牙测距辅助:利用 RSSI 值估算模块距离(仅作参考)

虽然经典蓝牙不支持精确测距,但可通过BluetoothDevice.fetchUuidsWithSdp()后读取 RSSI 作粗略判断:

// 在 DeviceListActivity 的 onReceive 中 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE); Log.d(TAG, "Device: " + device.getName() + ", RSSI: " + rssi + "dBm");

RSSI 值范围通常为-127(极弱)到0(极强),-40-60表示近距离(1 米内),-80以下建议靠近模块。注意此值受环境干扰大,仅作辅助参考,不可替代 UWB 或 BLE AoA 方案。

实际部署时,可将 RSSI 值映射为 TextView 背景色(绿色→黄色→红色),直观提示连接质量。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 14:31:47

ARM Cortex-M4上轻量级关键词唤醒模型源码深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 14:31:13

ESP32开发环境搭建:WSL2+ESP-IDF+Clangd实战指南

1. 为什么现在搭 ESP32 环境&#xff0c;绕不开 WSL2、Clangd 和 ESP-IDF 这三件套&#xff1f; 如果你最近半年内搜过“ESP32 教程”“ESP32 入门”&#xff0c;大概率会撞上一堆标题党&#xff1a;“5分钟点亮LED”“Arduino IDE 一键烧录”&#xff0c;结果一上手就卡在 i…

作者头像 李华
网站建设 2026/9/13 14:30:38

YOLOv8工业改造:基坑支护毫米级形变视觉监测系统

简介&#xff1a;本资源是一套面向计算机相关专业本科生的智慧工地安全监测毕设级项目&#xff0c;聚焦基坑支护结构变形的实时视觉感知与量化分析&#xff0c;解决传统人工巡检效率低、响应滞后等工程痛点。项目基于YOLOv8轻量模型实现高精度目标检测&#xff0c;集成可视化界…

作者头像 李华
网站建设 2026/9/13 14:30:37

VTK实现世界坐标系与惯性坐标系移动:从矩阵变换到交互实践

简介&#xff1a;基于VTK实现世界坐标系移动与惯性坐标系移动功能的C封装组件&#xff0c;面向三维交互开发人员及VTK进阶学习者&#xff0c;重点解决坐标轴拖拽、模型移动与坐标系切换等常见交互需求。资源将Widget与Representation分层封装&#xff0c;接口简洁&#xff0c;便…

作者头像 李华
网站建设 2026/9/13 14:28:47

微信聊天记录导出实用指南:3种格式永久保存

微信聊天记录导出实用指南&#xff1a;3种格式永久保存 【免费下载链接】WeChatMsg 提取微信聊天记录&#xff0c;将其导出成HTML、Word、CSV文档永久保存&#xff0c;对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/WeChatMsg …

作者头像 李华