从零打造可落地的直流电机 PID 驱动系统(四):Android 蓝牙控制 APP 开发(新手友好版)
前言
大家好!在上一篇《从零打造可落地的直流电机 PID 驱动系统(二):增加蓝牙远程控制功能》中,我们基于STM32F103C8T6 主控 + DRV8833 驱动 + HC-05 经典蓝牙模块搭建了完整的无线电机控制系统。
与 iOS 需要 MFi 认证才能使用经典蓝牙不同,Android 原生完全支持 HC-05 等经典蓝牙设备,无需更换任何硬件,直接复用你上一篇的成品即可。本文将使用 Android 官方推荐的 Kotlin 语言和 Jetpack Compose UI 框架,从零教你开发一款功能完整的电机控制 APP,实现电机启停、无级调速、PID 参数在线修改和实时转速显示。全程代码带详细注释,零基础也能一步步跟着做。
一、开发前准备(新手必看)
1.1 硬件准备
- 已完成的直流电机 PID 驱动系统(上一篇成品,HC-05 模块无需更换)
- Android 手机 / 平板(系统版本 Android 6.0/API 23 及以上,覆盖 99% 以上现役设备)
- USB 数据线(用于电脑连接手机调试)
1.2 软件准备
- Android Studio Hedgehog 2023.1.1 及以上(官方免费下载,新手推荐最新稳定版)
- 串口助手(如 SSCOM,用于提前测试 HC-05 模块通信)
- 基础 Kotlin 语法知识(只需了解变量、函数、类的基本概念即可)
1.3 关键前提说明
验证来源:Android 官方蓝牙开发文档
- 本文使用经典蓝牙 (RFCOMM)开发,完美兼容原系统的 HC-05 模块,硬件零改动
- HC-05 默认串口波特率 9600,与上一篇 STM32 的 UART 配置完全一致
- 所有代码均基于 Android 官方标准 API 编写,无第三方依赖,稳定性有保障
2.1 核心概念
Android 经典蓝牙通信采用客户端 - 服务器架构:
- 客户端:我们的 Android 手机,主动发起连接
- 服务器:HC-05 蓝牙模块,工作在从模式,被动等待连接
- RFCOMM 通道:蓝牙串口协议,实现透明数据传输,相当于无线串口
- UUID:服务唯一标识符,HC-05 使用标准串口服务 UUID:
00001101-0000-1000-8000-00805F9B34FB(必须正确,否则连接失败)
2.2 完整通信流程
三、新建 Android 项目与权限配置
3.1 创建新项目
- 打开 Android Studio,点击 "New Project"
- 选择 "Phone and Tablet" → "Empty Activity",点击 "Next"
- 填写项目名称(如
MotorControl),语言选择 "Kotlin",最小 SDK 选择 "API 23: Android 6.0 (Marshmallow)" - 点击 "Finish",等待项目初始化完成
3.2 配置蓝牙权限(最容易踩坑的地方)
打开app/src/main/AndroidManifest.xml文件,添加以下权限(分 API 版本适配,缺一不可):
<!-- 所有Android版本通用 --> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /> <!-- Android 6.0-11 (API 23-30) 蓝牙扫描需要位置权限 --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30" /> <!-- Android 12+ (API 31+) 新蓝牙权限 --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <!-- 声明APP需要蓝牙硬件 --> <uses-feature android:name="android.hardware.bluetooth" android:required="true" />验证来源:Android 官方蓝牙权限配置指南 https://developer.android.com/guide/topics/connectivity/bluetooth/permissions
四、蓝牙管理类实现(核心代码)
新建一个 Kotlin 文件BluetoothManager.kt,这是整个 APP 的核心,负责所有蓝牙操作。使用单例模式,确保整个 APP 只有一个蓝牙连接实例。
import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothSocket import android.content.Context import android.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.util.* // 蓝牙管理单例类 object BluetoothManager { private const val TAG = "BluetoothManager" // HC-05标准串口服务UUID(固定不变) private val MY_UUID: UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB") private val bluetoothAdapter: BluetoothAdapter? = BluetoothAdapter.getDefaultAdapter() private var bluetoothSocket: BluetoothSocket? = null private var inputStream: InputStream? = null private var outputStream: OutputStream? = null private var readJob: Job? = null // 状态回调接口,用于通知UI层蓝牙状态和数据 interface BluetoothCallback { fun onConnected(device: BluetoothDevice) fun onDisconnected() fun onDataReceived(data: String) fun onError(message: String) } var callback: BluetoothCallback? = null var isConnected: Boolean = false private set // 检查蓝牙是否开启 fun isBluetoothEnabled(): Boolean { return bluetoothAdapter?.isEnabled == true } // 获取已配对设备列表 fun getPairedDevices(): List<BluetoothDevice> { return bluetoothAdapter?.bondedDevices?.toList() ?: emptyList() } // 连接指定蓝牙设备 fun connect(device: BluetoothDevice) { if (isConnected) disconnect() CoroutineScope(Dispatchers.IO).launch { try { // 创建RFCOMM Socket bluetoothSocket = device.createRfcommSocketToServiceRecord(MY_UUID) // 取消扫描(扫描会降低连接速度) bluetoothAdapter?.cancelDiscovery() // 建立连接(阻塞操作,必须在后台线程执行) bluetoothSocket?.connect() // 获取输入输出流 inputStream = bluetoothSocket?.inputStream outputStream = bluetoothSocket?.outputStream isConnected = true callback?.onConnected(device) // 启动数据接收线程 startReading() } catch (e: IOException) { Log.e(TAG, "连接失败: ${e.message}") callback?.onError("连接失败: ${