告别有线调试!用Android手机蓝牙SPP连接Arduino,实现无线串口通信(附完整代码)
用Android手机蓝牙SPP打造无线Arduino调试神器
每次调试Arduino项目时,拖着USB线在面包板和电脑之间来回切换,是不是觉得特别麻烦?想象一下这样的场景:当你正在调试一个温湿度监测项目,传感器数据突然异常,而你只需要掏出手机就能实时查看串口输出,甚至直接发送控制指令——这就是蓝牙SPP协议带来的无线调试革命。
1. 硬件准备与蓝牙模块选型
工欲善其事,必先利其器。在开始无线调试之旅前,我们需要选择合适的硬件组合。市面上常见的蓝牙模块主要分为两类:经典蓝牙(如HC-05、HC-06)和低功耗蓝牙(BLE)。对于串口调试这种需要持续稳定连接的场景,经典蓝牙模块是更合适的选择。
主流蓝牙模块对比表:
| 型号 | 类型 | 工作电压 | 通信距离 | 特点 |
|---|---|---|---|---|
| HC-05 | 经典蓝牙 | 3.3-6V | 10米 | 主从一体,支持AT指令配置 |
| HC-06 | 经典蓝牙 | 3.3-6V | 10米 | 从机模式,配置简单 |
| HM-10 | BLE | 3.3V | 30米 | 低功耗,兼容iOS/Android |
提示:HC-05和HC-06价格亲民且稳定可靠,是Arduino项目的理想选择。购买时注意选择带有电平转换电路的版本,可直接连接Arduino的5V引脚。
连接蓝牙模块到Arduino非常简单,只需四根线:
// Arduino与HC-05连接示意图 // Arduino HC-05 // 5V ---- VCC // GND ---- GND // TX ---- RX // RX ---- TX初次使用时,可能需要通过AT指令配置模块参数。准备一个USB转TTL模块,按以下步骤操作:
- 连接USB转TTL到HC-05(注意交叉连接TX/RX)
- 打开串口工具,设置波特率38400
- 发送AT指令测试连接(HC-05需在未配对状态下进入AT模式)
- 常用配置指令:
AT+NAME=MyBT修改设备名称AT+PSWD=1234设置配对密码AT+UART=9600,0,0设置串口参数
2. Android端蓝牙调试APP开发
有了硬件基础,接下来我们开发一个功能完整的蓝牙调试APP。使用Android Studio新建项目,添加蓝牙权限:
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>注意:从Android 6.0开始,蓝牙扫描需要位置权限。在实际应用中应该动态请求这些权限。
蓝牙操作的核心流程封装:
class BluetoothSPPHelper(private val context: Context) { private val bluetoothAdapter: BluetoothAdapter? by lazy { BluetoothAdapter.getDefaultAdapter().also { adapter -> if (adapter == null) { Toast.makeText(context, "设备不支持蓝牙", Toast.LENGTH_SHORT).show() } } } // 检查并请求开启蓝牙 fun enableBluetooth(activity: Activity, requestCode: Int) { if (bluetoothAdapter?.isEnabled == false) { val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) activity.startActivityForResult(enableBtIntent, requestCode) } } // 获取已配对设备列表 fun getPairedDevices(): List<BluetoothDevice> { return bluetoothAdapter?.bondedDevices?.toList() ?: emptyList() } // 建立SPP连接 fun connect(device: BluetoothDevice, callback: (BluetoothSocket?) -> Unit) { val uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB") // SPP标准UUID val socket = device.createRfcommSocketToServiceRecord(uuid) GlobalScope.launch(Dispatchers.IO) { try { socket.connect() withContext(Dispatchers.Main) { callback(socket) } } catch (e: IOException) { withContext(Dispatchers.Main) { callback(null) } } } } }数据收发的核心实现:
class BluetoothDataTransfer(private val socket: BluetoothSocket) { private val inputStream: InputStream = socket.inputStream private val outputStream: OutputStream = socket.outputStream private var isListening = false // 发送数据 fun send(data: String) { try { outputStream.write(data.toByteArray()) } catch (e: IOException) { Log.e("BluetoothSPP", "发送失败", e) } } // 开始接收数据 fun startListening(callback: (String) -> Unit) { if (isListening) return isListening = true GlobalScope.launch(Dispatchers.IO) { val buffer = ByteArray(1024) while (isListening) { try { val bytes = inputStream.read(buffer) if (bytes > 0) { val receivedData = String(buffer, 0, bytes) withContext(Dispatchers.Main) { callback(receivedData) } } } catch (e: IOException) { Log.e("BluetoothSPP", "接收中断", e) break } } } } // 停止接收 fun stopListening() { isListening = false } // 关闭连接 fun close() { try { stopListening() socket.close() } catch (e: IOException) { Log.e("BluetoothSPP", "关闭连接失败", e) } } }3. Arduino端代码优化与无线调试技巧
Arduino端的代码需要与Android应用配合。以下是一个完整的无线调试示例,支持接收手机指令并返回传感器数据:
#include <SoftwareSerial.h> SoftwareSerial BT(10, 11); // RX, TX void setup() { Serial.begin(9600); BT.begin(9600); pinMode(LED_BUILTIN, OUTPUT); Serial.println("蓝牙调试系统就绪"); BT.println("蓝牙调试系统就绪"); } void loop() { // 处理来自手机的指令 if (BT.available()) { String command = BT.readStringUntil('\n'); command.trim(); if (command == "LED_ON") { digitalWrite(LED_BUILTIN, HIGH); BT.println("LED已开启"); } else if (command == "LED_OFF") { digitalWrite(LED_BUILTIN, LOW); BT.println("LED已关闭"); } else if (command == "GET_TEMP") { float temp = readTemperature(); // 假设的温度读取函数 BT.print("当前温度:"); BT.println(temp); } else { BT.print("未知指令:"); BT.println(command); } } // 调试信息通过蓝牙和USB串口同时输出 static unsigned long lastSend = 0; if (millis() - lastSend > 2000) { lastSend = millis(); String debugInfo = "系统运行时间:" + String(millis()/1000) + "秒"; Serial.println(debugInfo); BT.println(debugInfo); } }提升无线调试稳定性的技巧:
- 在数据包首尾添加特殊字符(如
<数据>)作为帧标识 - 实现简单的校验和验证
- 设置超时重发机制
- 重要数据采用问答式通信(发送后等待确认)
// 改进后的数据发送函数 void sendSafe(String data) { String packet = "<" + data + "|" + checksum(data) + ">"; BT.print(packet); } // 简单的校验和计算 int checksum(String str) { int sum = 0; for (int i = 0; i < str.length(); i++) { sum += str.charAt(i); } return sum % 256; }4. 实战项目:无线环境监测系统
将所学知识整合到一个实际项目中,我们创建一个无线环境监测系统。系统通过蓝牙将传感器数据实时传输到手机,并支持远程控制。
所需材料清单:
- Arduino Uno
- HC-05蓝牙模块
- DHT22温湿度传感器
- 光敏电阻
- 面包板和连接线
电路连接示意图:
DHT22数据引脚 -- Arduino D2 光敏电阻 -- A0 HC-05 TX -- Arduino D10 (SoftwareSerial RX) HC-05 RX -- Arduino D11 (SoftwareSerial TX)完整Arduino代码:
#include <SoftwareSerial.h> #include <DHT.h> #define DHTPIN 2 #define DHTTYPE DHT22 SoftwareSerial BT(10, 11); // RX, TX DHT dht(DHTPIN, DHTTYPE); void setup() { Serial.begin(9600); BT.begin(9600); dht.begin(); pinMode(A0, INPUT); pinMode(LED_BUILTIN, OUTPUT); BT.println("环境监测系统就绪"); } void loop() { // 处理指令 if (BT.available()) { String cmd = BT.readStringUntil('\n'); cmd.trim(); if (cmd == "GET_DATA") { sendSensorData(); } else if (cmd == "LED_ON") { digitalWrite(LED_BUILTIN, HIGH); BT.println("LED开启"); } else if (cmd == "LED_OFF") { digitalWrite(LED_BUILTIN, LOW); BT.println("LED关闭"); } } // 定时发送数据 static unsigned long lastSend = 0; if (millis() - lastSend > 5000) { lastSend = millis(); sendSensorData(); } } void sendSensorData() { float h = dht.readHumidity(); float t = dht.readTemperature(); int light = analogRead(A0); if (isnan(h) || isnan(t)) { BT.println("传感器读取失败"); return; } String data = "温度:" + String(t) + "℃ " + "湿度:" + String(h) + "% " + "光照:" + String(light); BT.println(data); }配套的Android APP界面应包含以下功能区域:
- 蓝牙设备连接状态显示
- 传感器数据实时展示区域
- 历史数据曲线图
- 控制指令发送按钮
- 原始数据接收窗口
数据可视化实现示例:
// 使用MPAndroidChart库绘制实时曲线 fun setupChart() { val chart: LineChart = findViewById(R.id.chart) chart.description.isEnabled = false chart.setTouchEnabled(true) chart.setPinchZoom(true) val xAxis: XAxis = chart.xAxis xAxis.position = XAxis.XAxisPosition.BOTTOM xAxis.granularity = 1f val leftAxis: YAxis = chart.axisLeft leftAxis.axisMinimum = 0f leftAxis.axisMaximum = 100f chart.axisRight.isEnabled = false // 温度数据集 val tempEntries = ArrayList<Entry>() val tempSet = LineDataSet(tempEntries, "温度").apply { color = Color.RED setCircleColor(Color.RED) lineWidth = 2f circleRadius = 3f } // 湿度数据集 val humiEntries = ArrayList<Entry>() val humiSet = LineDataSet(humiEntries, "湿度").apply { color = Color.BLUE setCircleColor(Color.BLUE) lineWidth = 2f circleRadius = 3f } chart.data = LineData(tempSet, humiSet) } // 更新图表数据 fun updateChart(temp: Float, humi: Float) { val chart: LineChart = findViewById(R.id.chart) val data = chart.data val tempSet = data.getDataSetByIndex(0) val humiSet = data.getDataSetByIndex(1) tempSet.addEntry(Entry(tempSet.entryCount.toFloat(), temp)) humiSet.addEntry(Entry(humiSet.entryCount.toFloat(), humi)) data.notifyDataChanged() chart.notifyDataSetChanged() chart.moveViewToX(data.entryCount.toFloat()) }在项目开发过程中,我遇到最棘手的问题是蓝牙连接在Android不同版本上的兼容性问题。特别是Android 12以后对蓝牙权限管理的改变,需要特别注意以下几点:
- 在AndroidManifest.xml中声明新的蓝牙权限
- 运行时精确请求BLUETOOTH_SCAN和BLUETOOTH_CONNECT权限
- 处理用户拒绝权限的情况,提供合理的回退方案
- 后台位置权限的特殊处理
// Android 12+蓝牙权限处理 private fun checkBluetoothPermissions(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { ContextCompat.checkSelfPermission( this, Manifest.permission.BLUETOOTH_SCAN ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission( this, Manifest.permission.BLUETOOTH_CONNECT ) == PackageManager.PERMISSION_GRANTED } else { ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED } }