简介:这是一款基于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.gradle、AndroidManifest.xml权限声明、res/layout界面定义及完整的BluetoothChatService核心服务类。
2. 从权限声明到设备发现:SPP 协议下经典蓝牙通信的初始化闭环
2.1 清单文件中的关键权限与硬件声明必须显式配置
Android 12(API 31)起,蓝牙扫描需额外申请BLUETOOTH_SCAN运行时权限,且AndroidManifest.xml中必须声明uses-permission与uses-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_SCAN和BLUETOOTH_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_FOUND与BluetoothAdapter.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_CONNECTING | connect()调用后 | 启动ConnectThread | 显示“正在连接…”,禁用所有操作按钮 |
STATE_CONNECTED | connected()被调用 | 启动ConnectedThread,注册Handler | 显示“已连接”,启用发送框与清屏按钮 |
STATE_LISTEN | 作为服务端监听时 | 启动AcceptThread | 适用于 PC 端模拟蓝牙串口服务器场景 |
提示:源码中
BluetoothChatService通常只实现客户端角色(主动连接),若需服务端监听(如手机作为蓝牙串口服务器被 PC 连接),需补充AcceptThread并在onStartCommand中启动。
4. 调试实战:解决 hc05 模块连接不上与数据乱码的四大高频问题
4.1 HC-05 连接失败:配对、波特率与 AT 指令预置检查清单
HC-05 模块在出厂时处于从机模式,需确保以下三点:
- 物理配对完成:手机蓝牙设置中已与 HC-05 配对(PIN 码通常为
1234或0000),且状态为“已配对”而非“可用设备”; - 模块工作模式正确:HC-05 默认为“从机”,若需手机主动连接,无需更改;若模块设为“主机”,则无法被手机发现;
- 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 自动过滤显示为空白。
验证步骤:
- 修改
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()); - 对比串口助手(如 XCOM)的 HEX 显示模式,确认是否一致;
- 若 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 背景色(绿色→黄色→红色),直观提示连接质量。
本文还有配套的精品资源,点击获取