BluetoothSpecialSPP 是一款基于 Android 原生蓝牙 SPP(Serial Port Profile)协议的通信监控应用。它通过经典的 RFCOMM 通道与蓝牙串口设备建立连接,实现数据的双向收发。
本项目最初作为华东交通大学电子信息工程专业的毕业设计项目开发,因此具备两重身份:
| 参数 | 值 |
|---|---|
| 开发平台 | Android Studio + Java |
| 目标平台 | Android 7.0 (API 24) 及以上 |
| 编译环境 | Gradle 8.11.1 + AGP 8.9.0 |
| 蓝牙协议 | 经典蓝牙 SPP(UUID: 00001101-0000-1000-8000-00805f9b34fb) |
| UI 框架 | Android XML Layout + Material Design |
| 源码包名 | com.jay.bluetooth |
| APK 名称 | BlueToothSpecialSPP-1.4.21.apk |
| 项目作者 | 张杰(华东交通大学 电子信息工程) |
| 项 | 说明 |
|---|---|
| 操作系统 | Windows / macOS |
| 开发工具 | Android Studio |
| 编程语言 | Java 11 |
| 构建工具 | Gradle 8.11.1 + AGP 8.9.0 |
| compileSdk / targetSdk | 35 |
| minSdk | 24(Android 7.0) |
| 库名 | 版本 | 用途 |
|---|---|---|
| appcompat | 1.6.1 | Android 兼容库 |
| material | 1.10.0 | Material Design 组件 |
| activity | 1.8.0 | Activity API 封装 |
| constraintlayout | 2.1.4 | 约束布局(主界面核心) |
所有依赖通过 Gradle Version Catalog(libs.versions.toml)统一管理,这是官方推荐的最佳实践。
文件:MainActivity.java(约 500 行),负责蓝牙连接管理、数据收发、解析与 UI 更新。
蓝牙连接流程:
private void connectToDevice(String deviceAddress) {
new Thread(() -> {
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(deviceAddress);
BluetoothSocket socket = device.createRfcommSocketToServiceRecord(MY_UUID);
socket.connect();
inputStream = socket.getInputStream();
outputStream = socket.getOutputStream();
startReceiveThread();
runOnUiThread(() -> updateConnectionStatus(device.getName()));
}).start();
}Java
数据接收与解析:
private void startReceiveThread() {
receiveThread = new Thread(() -> {
byte[] buffer = new byte[1024];
int bytes;
while ((bytes = inputStream.read(buffer)) != -1) {
String data = new String(buffer, 0, bytes, "UTF-8");
if (data.startsWith(">")) {
// 终端显示(原始数据)
tvTerminal.append("\n" + data);
} else if (data.startsWith("#") && data.length() == 21) {
// 结构化数据解析
String temp = data.substring(1, 4);
String humi = data.substring(5, 8);
String sound = data.substring(9, 12);
String level = data.substring(13, 15);
String dust = data.substring(16, 19);
String ctrl = data.substring(20, 21);
// 告警判断
if (level.equals("I") || Double.parseDouble(dust) > 150) {
setBackgroundColor(红色);
} else {
setBackgroundColor(绿色);
}
}
}
});
receiveThread.start();
}Java
数据发送:
private void sendBluetoothCommand(String command) {
try {
outputStream.write((command + "\r\n").getBytes("UTF-8"));
outputStream.flush();
} catch (IOException e) {
Toast.makeText(this, "发送失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
}
}Java
文件:SecondActivity.java(约 320 行),负责显示已配对设备和扫描附近设备。
getBondedDevices() 获取,HashSet 去重(O(1) 快速判断)文件:BtnActivity.java(约 80 行),提供编辑界面修改按钮名称和发送内容。
| 指令 | 含义 |
|---|---|
on\r\n | 开启加湿器 |
off\r\n | 关闭加湿器 |
| 自定义消息 | 用户预设的任意指令 |
以 # 开头,固定 21 字节:
# T1 T2 T3 T4 H1 H2 H3 H4 S1 S2 S3 S4 N1 N2 N3 D1 D2 D3 D4 C
│ ───温度─── ───湿度─── ───音量─── ──等级── ───粉尘─── 开关
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20格式
| 字段 | 长度 | 位置 | 说明 |
|---|---|---|---|
| 温度 | 4 | 索引 1-4 | 单位:℃ |
| 湿度 | 4 | 索引 5-8 | 单位:% |
| 音量 | 4 | 索引 9-12 | 单位:DB |
| 噪音等级 | 3 | 索引 13-15 | I / II / III |
| 粉尘浓度 | 4 | 索引 16-19 | 单位:μg/m³ |
| 开关状态 | 1 | 索引 20 | '1'=开,其他=关 |
'f' → 按钮显示"开启",'n' → 按钮显示"关闭"应用在 AndroidManifest.xml 中声明了 9 项权限:
| 权限 | 用途 |
|---|---|
| BLUETOOTH_CONNECT | 连接蓝牙设备(Android 12+ 运行时权限) |
| BLUETOOTH | 基础蓝牙操作(兼容旧版本) |
| BLUETOOTH_ADMIN | 蓝牙管理(扫描等) |
| ACCESS_FINE_LOCATION | 精确定位(蓝牙扫描需要) |
| BLUETOOTH_SCAN | 蓝牙扫描(neverForLocation 标志) |
| INTERNET | 网络访问(预留) |
| VIBRATE | 振动 |
注意:BLUETOOTH_SCAN 使用了 neverForLocation 权限标志,表明不会将扫描结果用于定位,这是 Android 12+ 的最佳实践。
private void checkBluetoothPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+: 运行时申请 BLUETOOTH_CONNECT + BLUETOOTH_SCAN
requestPermissions(new String[]{
BLUETOOTH_CONNECT, BLUETOOTH_SCAN, ACCESS_FINE_LOCATION
}, REQUEST_BLUETOOTH_PERMISSIONS);
} else {
// 旧版本: 仅需 ACCESS_FINE_LOCATION
requestPermissions(new String[]{
ACCESS_FINE_LOCATION
}, REQUEST_LOCATION_PERMISSION);
}
}Java
| 问题 | 解决方案 |
|---|---|
| 提示"设备不支持蓝牙" | 确认手机硬件支持经典蓝牙(非 BLE),虚拟机通常不支持 |
| 扫描不到设备 | 检查目标设备是否开启且可发现;确认已授予位置权限;点击刷新重新扫描 |
| 连接失败 | 系统设置中先配对;确认设备未与其他设备连接;距离保持在 10 米内 |
| 数据显示乱码 | 确认硬件编码格式为 UTF-8,否则修改 new String(buffer, 0, bytes, "编码") |
| 中文发送无反应 | 部分蓝牙串口模块仅支持 ASCII,建议使用英文或数字 |
| 红色背景如何恢复 | 红色是告警状态,数据恢复正常后自动变回绿色 |