1. 项目背景与核心功能
这个项目源于我在智能硬件开发过程中遇到的一个实际需求:需要通过手机APP同时控制两台经典蓝牙设备。市面上大多数蓝牙控制APP都只支持单设备连接,而工业场景中经常需要主从设备协同工作。比如在智能家居系统中,可能需要用手机同时控制客厅的蓝牙音响和卧室的灯光设备。
经典蓝牙(Classic Bluetooth)与低功耗蓝牙(BLE)是两种不同的技术路线。经典蓝牙支持更高的数据传输速率(最高3Mbps),适合音频传输、文件分享等场景。其核心协议包括RFCOMM(串口仿真)和SPP(串行端口协议),这正是实现稳定双机控制的基础。
关键区别:经典蓝牙的SPP协议相比BLE的GATT协议,具有更低的通信延迟和更稳定的长连接特性,这也是我选择经典蓝牙方案的主要原因。
2. 开发环境搭建
2.1 Android Studio配置要点
我使用的是Android Studio Dolphin 2021.3.1版本,这个版本在蓝牙开发方面有较好的稳定性。安装时需要注意:
-
确保勾选以下组件:
- Android SDK Platform 33
- Intel x86 Emulator Accelerator (HAXM installer)
- Android Emulator
-
解决Gradle下载超时问题:
修改项目根目录下的gradle.properties文件,添加阿里云镜像:code复制systemProp.http.proxyHost=mirrors.aliyun.com systemProp.http.proxyPort=80 systemProp.https.proxyHost=mirrors.aliyun.com systemProp.https.proxyPort=80 -
虚拟机配置技巧:
创建AVD时建议选择Pixel 3a模板,API级别选33。遇到"the emulator process for AVD"报错时,通常是因为没有启用VT-x虚拟化技术,需要在BIOS中开启。
2.2 关键依赖配置
在app/build.gradle中添加蓝牙权限和必要依赖:
groovy复制dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// 蓝牙通信核心库
implementation 'com.github.akexorcist:bluetoothspp:1.0.0'
}
3. 蓝牙双机控制实现方案
3.1 设备发现与配对流程
经典蓝牙设备发现使用的是传统的BluetoothAdapter API:
java复制BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
// 设备不支持蓝牙
return;
}
// 开启设备发现
if (!bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.startDiscovery();
}
// 注册广播接收器
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(receiver, filter);
广播接收器中处理找到的设备:
java复制private final BroadcastReceiver receiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
String deviceName = device.getName();
String deviceHardwareAddress = device.getAddress();
// 添加到设备列表
}
}
};
3.2 双机连接管理
实现双机控制的核心是维护两个独立的BluetoothSocket连接。我设计了一个连接管理类:
java复制public class DualBluetoothManager {
private BluetoothSocket primarySocket;
private BluetoothSocket secondarySocket;
public boolean connectPrimary(BluetoothDevice device) {
try {
// 通过UUID创建RFCOMM连接
primarySocket = device.createRfcommSocketToServiceRecord(
UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"));
primarySocket.connect();
return true;
} catch (IOException e) {
Log.e("BT", "Primary连接失败", e);
return false;
}
}
// 类似的secondary连接方法
}
经验之谈:两个socket必须使用不同的线程进行数据读写,否则会出现阻塞。我推荐为每个连接单独创建HandlerThread。
3.3 数据通信协议设计
为了区分两台设备,我设计了简单的帧协议:
code复制[设备ID(1字节)][数据长度(2字节)][数据内容(N字节)][校验和(1字节)]
设备ID为0x01表示主设备,0x02表示从设备。
数据发送示例:
java复制public void sendToDevice(byte deviceId, byte[] data) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write(deviceId);
output.write((byte)(data.length >> 8));
output.write((byte)data.length);
output.write(data);
byte checksum = 0;
for (byte b : output.toByteArray()) {
checksum ^= b;
}
output.write(checksum);
if (deviceId == 0x01 && primarySocket != null) {
primarySocket.getOutputStream().write(output.toByteArray());
} else if (secondarySocket != null) {
secondarySocket.getOutputStream().write(output.toByteArray());
}
}
4. 用户界面设计与实现
4.1 设备列表与连接状态
采用RecyclerView展示发现的蓝牙设备,每个Item显示:
- 设备名称
- MAC地址
- 信号强度图标
- 连接状态按钮
连接状态使用颜色标识:
- 灰色:未连接
- 蓝色:主设备连接
- 绿色:从设备连接
- 红色:连接失败
4.2 双机控制面板
核心控制界面包含:
- 设备状态区域:显示两台设备的实时状态
- 指令发送区域:可单独或同时发送控制命令
- 数据监控区域:显示接收到的数据流
布局关键代码:
xml复制<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="horizontal">
<!-- 主设备控制区 -->
<include layout="@layout/device_control_panel"
android:id="@+id/primary_panel"/>
<!-- 从设备控制区 -->
<include layout="@layout/device_control_panel"
android:id="@+id/secondary_panel"/>
</LinearLayout>
<!-- 联合控制区 -->
<include layout="@layout/combined_control_panel"/>
</LinearLayout>
5. 实战中的疑难问题解决
5.1 连接稳定性优化
在实测中发现经典蓝牙连接容易在以下场景断开:
- 设备距离过远(>10米)
- 存在Wi-Fi同频干扰
- 手机进入省电模式
解决方案:
java复制// 1. 设置连接保活
primarySocket.setKeepAlive(true);
// 2. 增加重连机制
private void ensureConnection() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> {
if (!primarySocket.isConnected()) {
connectPrimary(lastPrimaryDevice);
}
}, 0, 30, TimeUnit.SECONDS);
}
// 3. 申请WakeLock防止休眠
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::BluetoothLock");
wakeLock.acquire();
5.2 数据传输粘包处理
经典蓝牙的RFCOMM协议是流式传输,需要自行处理数据边界。我采用以下方案:
- 固定帧头(0xAA 0x55)
- 长度字段指明数据区大小
- 帧尾校验
数据解析示例:
java复制private void processInputStream(InputStream input) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] temp = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(temp)) != -1) {
buffer.write(temp, 0, bytesRead);
byte[] data = buffer.toByteArray();
int start = findFrameStart(data);
while (start >= 0) {
if (data.length >= start + 4) { // 至少包含帧头和长度
int length = ((data[start+2] & 0xFF) << 8) | (data[start+3] & 0xFF);
if (data.length >= start + 4 + length + 1) { // 完整帧
byte[] frame = Arrays.copyOfRange(data, start, start + 4 + length + 1);
if (verifyChecksum(frame)) {
dispatchFrame(frame);
}
data = Arrays.copyOfRange(data, start + 4 + length + 1, data.length);
start = findFrameStart(data);
} else {
break;
}
} else {
break;
}
}
buffer.reset();
buffer.write(data);
}
}
6. APP发布与性能优化
6.1 发布前检查清单
-
权限声明检查:
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.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/> -
ProGuard混淆规则:
code复制-keep class com.github.akexorcist.** { *; } -keep class android.bluetooth.** { *; } -
多语言支持:
在res/values-zh-rCN中添加中文翻译
6.2 性能优化指标
通过Android Profiler测试得到:
- 双机连接建立时间:<3秒
- 指令传输延迟:<100ms(5米内)
- 内存占用:<35MB
- 连续工作8小时无断开
优化措施:
- 使用对象池复用ByteBuffer
- 压缩传输数据(对>1KB的数据启用GZIP)
- 采用非阻塞IO模型
7. 扩展功能实现
7.1 指令宏录制
用户可以录制一系列操作并保存为宏指令:
java复制public class MacroCommand {
private List<CommandStep> steps = new ArrayList<>();
public void addStep(byte deviceId, byte[] command, long delay) {
steps.add(new CommandStep(deviceId, command, delay));
}
public void execute() {
for (CommandStep step : steps) {
if (step.delay > 0) {
SystemClock.sleep(step.delay);
}
sendToDevice(step.deviceId, step.command);
}
}
}
7.2 设备组合配置
支持保存常用的设备组合配置:
java复制public class DeviceProfile {
String profileName;
BluetoothDevice primaryDevice;
BluetoothDevice secondaryDevice;
List<MacroCommand> macros;
public void saveToPrefs(Context context) {
SharedPreferences prefs = context.getSharedPreferences("BluetoothProfiles", MODE_PRIVATE);
String json = new Gson().toJson(this);
prefs.edit().putString(profileName, json).apply();
}
}
8. 测试方案设计
8.1 单元测试重点
- 协议编解码测试:
java复制@Test
public void testFrameEncoding() {
byte[] testData = {0x01, 0x02, 0x03};
byte[] frame = ProtocolEncoder.encode(0x01, testData);
assertArrayEquals(new byte[]{0x01, 0x00, 0x03, 0x01, 0x02, 0x03, 0x01}, frame);
}
- 连接状态机测试:
java复制@Test
public void testConnectionState() {
BluetoothManager manager = new BluetoothManager();
manager.connectPrimary(mockDevice);
assertEquals(STATE_CONNECTED, manager.getPrimaryState());
}
8.2 真机测试场景
- 距离测试:在不同距离下测试连接稳定性
- 干扰测试:在Wi-Fi路由器旁测试抗干扰能力
- 压力测试:连续发送1000条指令检查内存泄漏
测试结果记录表示例:
| 测试项目 | 标准要求 | 实测结果 | 是否通过 |
|---|---|---|---|
| 双机同时连接 | <5秒完成 | 2.3秒 | ✓ |
| 10米传输 | 丢包率<1% | 0.8% | ✓ |
| 连续工作8小时 | 无异常断开 | 无断开 | ✓ |
9. 项目总结与改进方向
这个项目让我深入理解了经典蓝牙协议栈的实现细节。与BLE相比,经典蓝牙在传输速率和实时性方面仍有不可替代的优势,特别是在需要同时控制多台设备的场景下。
几个值得记录的教训:
- 不要在主线程执行蓝牙连接操作,Android会直接抛出NetworkOnMainThreadException
- 不同手机厂商对经典蓝牙的实现有差异,特别是华为/小米等国产ROM需要特殊适配
- 在Android 12及以上版本,必须动态申请BLUETOOTH_CONNECT权限
后续改进计划:
- 增加蓝牙设备固件OTA升级功能
- 实现设备分组和场景模式
- 支持通过NFC快速配对
