同专项组同事协作的 Side Project,一个蓝牙自动浇花系统
概述
一个简单的 Android-Ardunio 蓝牙通信 Demo,代码位于 Github Repo.
需求分析
- 建立蓝牙连接,根据已定义的协议、同 Arduino 通信
- 解析应答数据,显示湿度或任何通信过程中异常信息
- 提供 UI 来呈现指令按钮、配对设备列表、通信日志等
通信协议
指令 | 请求 | 应答 |
---|---|---|
获取湿度/湿度阈值 | 0x57 0x55 0x01 0xFF |
0x53 0x53 0x01 <当前值> <高> <低> 0xFF |
设定阈值 | 0x57 0x55 0x02 <高> <低> 0xFF |
0x53 0x53 0x02 0xFF |
启动浇花 | 0x57 0x55 0x03 0xFF |
0x53 0x53 0x03 0xFF |
停止浇花 | 0x57 0x55 0x04 0xFF |
0x53 0x53 0x04 0xFF |
湿度值两位数据表示,校验位暂定
0xFF
程序实现
主界面 BTConnActivity
处理用户交互,BTService
负责建立蓝牙 Socket、并在单独线程管理蓝牙连接。
由于 Demo 实现得简单,因此仅将 Android 蓝牙开发的部分内容整理归档下。
Android 蓝牙应用开发
1. 权限声明
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
2. 蓝牙开发常用的类
2.1 BluetoothAdapter
代表本地的蓝牙适配器设备,BluetoothAdapter
类能让用户执行基本的蓝牙任务。例如:初始化设备的搜索、查询可匹配的设备集、使用一个已知的MAC地址来初始化一个 BluetoothDevice
类,创建一个 BluetoothServerSocket
类以监听其他设备对本机的连接请求等。
2.2 BluetoothDevice
代表远端蓝牙设备,可通过 BluetoothAdapter.getRemoteDevice(String)
创建一个表示已知MAC地址的设备(通过 BluetoothAdapter
来完成对设备的查找),或通过 Bluetooth.getBondedDevices()
返回的已匹配的设备集中得到设备对象。
2.3 BluetoothServerSocket
蓝牙端口监听接口和TCP端口类似:Socket 和 ServerSocket 类。在服务器端,使用 BluetoothServerSocket
类来创建一个监听服务端口。当一个连接被 BluetoothServerSocket
所接受,它会返回一个新的 BluetoothSocket
来管理该连接。在客户端,使用一个单独的 BluetoothSocket
类去初始化一个外接连接和管理该连接。
最常使用的蓝牙端口是 RFCOMM,它是被 Android API 支持的类型。RFCOMM 是一个面向连接,通过蓝牙模块进行的数据流传输方式,它也被称为串行端口规范(Serial Port Profile, SPP)
一些基础概念:
蓝牙 ( Bluetooth ) 是一种无线技术,用于建立带宽为2.4GHz,波长为10m的私有网络。
通道 ( Channel ) 是位于基带连接之上的逻辑连接,每个通道以多对一的方式绑定一个单一协议。多个通道可以绑定同一个协议,但是一个通道不可以绑定多个协议。
RFCOMM 协议 是为了兼容传统的串口应用,同时取代有线的通信方式,蓝牙协议需要提供与有线串口一致的通信接口而开发出的协议。
设备配对 ( Pairing of Device ) 蓝牙设备可以选择通过验证以提供某种特殊服务,蓝牙验证一般使用 PIN 码 (最长为16个字符的 ASCII 字符串),用户需要在两个设备中输入相同的PIN码。用户输入 PIN 码后,两个设备会生成一个连接密匙 ( link key ),接着连接密钥可以存储在设备或存储器中。连接时两个设备会使用该连接密钥,该过程称为结对 ( pairing )。如果任一方丢失了连接密钥,必须重新进行结对。
2.4 BluetoothSocket
代表一个蓝牙套接字的接口,是应用程序通过输入输出流与其他蓝牙设备通信的连接点。
3. 蓝牙应用开发示例
3.1 获取 BluetoothAdapter 对象
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
// Device does not support Bluetooth
}
3.2 启用蓝牙
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
系统会弹窗提升用户是否要允许开启蓝牙。如果开启成功,onActivityResult()
回调中收到 RESULT_OK
3.3 查询已配对设备集
已绑定/匹配的蓝牙设备,不同于已连接状态(设备间存在一个 RFCOMM 通道并能够互相传递数据)。建立连接首先要求建立配对,如果是第一次连接的话,配对请求会显示给用户;配对成功后会保存设备名称及 MAC 地址。
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0 ) {
// Loop through paired devices
for (BluetoothDevice device : pairedDevices) {
// Add the name and address to an array adapter to show in a ListView
mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
}
3.4 发现设备
调用 startDiscovery()
方法,注意这是一个异步方法,整个扫描过程大概12s,应用程序需要注册一个 BroadcastReceiver 来接收扫描到的信息,对于每一个设备,系统都会广播 ACTION_FOUND
动作。
// Create a BroadcastReceiver for ACTION_FOUND
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// When discovery finds a device
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// Get the BluetoothDevice object from the Intent
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// Add the name and address to an array adapter to show in a ListView
mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
}
}
// Register the BroadcastReceiver
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter); // Don't forget to unregister during onDestory
注意,扫描是一个很耗费资源的过程,一旦找到需要的设备后,在发起请求之前,确保你的程序调用 cancelDiscovery()
停止扫描。
3.5 开启可见
将 ACTION_REQUEST_DISCOVERABLE
动作封装在 Intent 中并调用 startActivityForResult(Intent, int)
方法就可以了,可以通过 EXTRA_DISCOVERABLE_DURATION
字段改变可见时长(缺省120s)
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
3.6 作为服务端连接
持有一个打开的 BluetoothServerSocket
以监听外来连接请求,当监听到以后提供一个连接上的 BluetoothSocket
给客户端、并可以销毁 BluetoothServerSocket
(除非还想监听更多的连接请求,一般都是单点传输)
详细步骤:
- 通过
listenUsingRfcommWithServiceRecord(String, UUID)
方法来获取BluetoothServerSocket
对象,系统会在设备上建立 SDP 数据库;UUID 必须双方匹配才能建立连接 - 调用
accept()
方法来监听可能到来的连接请求,当监听到以后、返回一个连接上的BluetoothSocket
- 在监听到一个连接后,调用
close()
方法关闭监听程序。(因accept
是一种阻塞调用,单独开线程管理)
蓝牙服务发现协议(Service Discovery Protocol, SDP):让客户端应用发现存在的服务端应用所提供的服务、以及这些服务的属性。SDP 只提供侦测 service 的机制,不提供使用服务的方法。每个蓝牙设备都需要一个 SDP Service,只做客户端的蓝牙设备除外。
SDP Server 维护的服务条目包含在 Service Record中,由一个32位数唯一区分。
from Android SDK Docs:
If you are connecting to a Bluetooth serial board then try using the well-known SPP UUID 00001101-0000-1000-8000-00805F9B34FB. However if you are connecting to an Android peer then please generate your own unique UUID.
private class AcceptThread extends Thread {
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
// Use a temporary object that is later assigned to mmServerSocket,
// because mmServerSocket is final
BluetoothServerSocket tmp = null;
try {
// MY_UUID is the app's UUID string, also used by the client code
tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) {
// IGNORE
}
mmServerSocket = tmp;
}
public void run() {
BluetoothSocket socket = null;
// Keep listening until exception occurs or a socket is returned
while (true) {
try {
socket = mmServerSocket.accept();
} catch (IOException e) {
break;
}
// If a connection was accepted
if (socket != null) {
// Do work to manage the connection (in a separate thread)
manageConnectedSocket(socket);
mmServerSocket.close();
break;
}
}
}
/** Will cancel the listening socket, and cause the thread to finish */
public void cancel () {
try {
mmServerSocket.close();
} catch (IOException e) {
// IGNORE
}
}
}
3.7 作为客户端连接
为了初始化一个与远端设备的连接,需要先获取代表该设备的一个 BluetoothDevice
对象,再通过 createRfcommSocketToServiceRecord(UUID)
获取 BluetoothSocket
,并调用 connect()
方法初始化连接。如果成功建立连接,将在通信过程中共享 RFCOMM 信道,且 connect()
方法返回。
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
BluetoothSockcet tmp = null;
mmDevice = device;
// Get a BluetoothSocket to connect with the given BluetoothDevice
try {
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
// IGNORE
}
mmSocket = tmp;
}
public void run() {
// Cancel discovery because it will slow down the connection
mBluetoothAdapter.cancelDiscovery();
try {
// Connect the device through the socket. This will block
// until it succeeds or throws an exception
mmSocket.Connect();
} catch (IOException connectException) {
// Unable to connect; close the socket and get out
try {
mmSocket.close();
} catch (IOException closeException) {
// IGNORE
}
return;
}
// Do work to manage the connection (in a separate thread)
manageConnectedSocket(mmSocket);
}
/** Will cancel an in-progress connection, and close the socket */
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
// IGNORE
}
}
}
3.8 管理连接(主要涉及数据的传输)
当成功建立连接后,两端都持有BluetoothSocket
,以流的方式通信。由于读写操作都是阻塞调用,需要开一个线程来管理。
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket) {
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// Get the input and output streams, using temp objects because
// member streams are final
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
// IGNORE
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
byte[] buffer = new byte[1024]; // buffer store for the stream
int bytes; // bytes returned from read()
// Keep listening to the InputStream until an exception occurs
while (true) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
// Send the obtained bytes to the UI activity
mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer).sendToTarget();
} catch (IOException e) {
// IGNORE
break;
}
}
}
/** Call this from the main activity to send data to the remote device */
public void write(byte[] bytes) {
try {
mmOutStream.write(bytes);
} catch (IOException e) {
// IGNORE
}
}
/** Call this from the main activity to shutdown the connection */
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
// IGNORE
}
}
}