前言
这是我大二做的一个智能小车配套使用的APP,用Android的蓝牙接口实现,当时有些os相关的内容Thread
之类还有一些Android接口、java语法,我其实不是很理解。学了操作系统,再来回顾一下,并整理项目代码,项目具有很高的复用性,特别是蓝牙部分。
reference
项目参考了稚晖君的开源项目 https://github.com/peng-zhihui/BluetoothTouch
Android蓝牙开发官方文档 https://developer.android.google.cn/guide/topics/connectivity/bluetooth
开启蓝牙权限
为APP开启蓝牙权限,将以下config
添加到项目配置文件AndroidManifest.xml
中:
<manifest>
<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- Needed only if your app looks for Bluetooth devices.
If your app doesn't use Bluetooth scan results to derive physical
location information, you can strongly assert that your app
doesn't derive physical location. -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<!-- Needed only if your app makes the device discoverable to Bluetooth
devices. -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<!-- Needed only if your app communicates with already-paired Bluetooth
devices. -->
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Needed only if your app uses Bluetooth scan results to derive physical location. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...
</manifest>
设置蓝牙
这个在MainActivity
中设置,分为两步:
- 获取
BluetoothAdapter
,这是所有蓝牙活动的基础;
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// Get local Bluetooth adapter
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
mBluetoothAdapter = ((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter();
else
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
// If the adapter is null, then Bluetooth is not supported
if (mBluetoothAdapter == null) {
showToast("Bluetooth is not available", Toast.LENGTH_LONG);
finish();
return;
}
}
- 使能蓝牙,首先确定蓝牙是否启用,若未启用,调用
startActivityForResult
,传递ACTION_REQUEST_ENABLE
目标动作,这些 大写变量 实际是Android定义好的字符串;若以启用,传入BluetoothHandler
和BluetoothAdapter
,实例化我们自定义的蓝牙服务类BluetoothChatService
(后面慢慢介绍,实际就是蓝牙接口封装起来的一个类);
public void onStart()
{
super.onStart();
// If BT is not on, request that it be enabled.
// setupChat() will then be called during onActivityResult
if (!mBluetoothAdapter.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
} else
setupBTService(); // Otherwise, setup the chat session
}
private void setupBTService()
{
if (mChatService != null)
return;
if (mBluetoothHandler == null)
mBluetoothHandler = new BluetoothHandler(this);
mChatService = new BluetoothChatService(mBluetoothHandler, mBluetoothAdapter); // Initialize the BluetoothChatService to perform Bluetooth connections
}
连接设备
通过retry
判断是否断线重连,否则创建新连接,像http
协议有 ipv4 和 ipv6 地址,以太网协议有 MAC 地址,蓝牙也有地址,在蓝牙构建的网络中标识设备或者蓝牙对象。
private void connectDevice(Intent data, boolean retry)
{
if (retry) {
if (btDevice != null && !stopRetrying) {
mChatService.start(); // This will stop all the running threads
mChatService.connect(btDevice, btSecure); // Attempt to connect to the device
}
} else { // It's a new connection
stopRetrying = false;
mChatService.newConnection = true;
mChatService.start(); // This will stop all the running threads
if (data.getExtras() == null)
return;
// 获取设备蓝牙地址并连接设备
String address = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS); // Get the device Bluetooth address
btSecure = data.getExtras().getBoolean(DeviceListActivity.EXTRA_NEW_DEVICE); // If it's a new device we will pair with the device
btDevice = mBluetoothAdapter.getRemoteDevice(address); // Get the BluetoothDevice object
mChatService.nRetries = 0; // Reset retry counter
mChatService.connect(btDevice, btSecure); // Attempt to connect to the device
showToast(getString(R.string.connecting), Toast.LENGTH_SHORT);
}
}
BluetoothChatService.connect
connect
方法的定义如下,这里有mConnectThread
和mConnectedThread
两个用于连接和保持连接的线程,先cancel
之前的线程,再重新连接start
,这里synchronized
关键字是声明了线程的同步锁(坑,详细解释)。
public synchronized void connect(BluetoothDevice device, boolean secure)
{
stopReading = true;
// Cancel any thread attempting to make a connection
if (mConnectThread != null) {
mConnectThread.cancel();
mConnectThread = null;
}
// Cancel any thread currently running a connection
if (mConnectedThread != null) {
mConnectedThread.cancel();
mConnectedThread = null;
}
// Start the thread to connect with the given device
mConnectThread = new ConnectThread(device, secure);
mConnectThread.start();
setState(STATE_CONNECTING);
}
BluetoothChatService.ConnectThread
这里final
关键字修饰变量在初始化后不可更改。这里BluetoothSocket
是用于收发数据的工具,在网络通信的编程开发中很常见。这里仅实现了作为客户端进行连接(坑,补充作为服务端等待连接的线程实现)。基本连接流程如下:
- 使用
BluetoothDevice
调用createRfcommSocketToServiceRecord(UUID)
获取蓝牙套接字BluetoothSocket
,这个UUID
是由服务端和客户端共同协商的,RFCOMM就是蓝牙通信所使用的的信道; - 阻塞调用
socket
的connect()
方法,系统执行SDP查找具有指定UUID
的设备,若查找成功并且对方设备接受连接,开始蓝牙通信,共享RFCOMM信道;若超时则抛出一个IOException
; - 蓝牙连接完毕,之后调用
BluetoothChatService.connected
(类似BluetoothChatService.connect
),用于启动保持连接线程(或者说通信线程)BluetoothChatService.ConnectedThread
.
private class ConnectThread extends Thread
{
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
private String mSocketType;
public ConnectThread(BluetoothDevice device, boolean secure)
{
mmDevice = device;
BluetoothSocket tmp = null;
mSocketType = secure ? "Secure" : "Insecure";
// Get a BluetoothSocket for a connection with the
// given BluetoothDevice
try {
if (secure)
tmp = mmDevice.createRfcommSocketToServiceRecord(UUID_RFCOMM_GENERIC);
else
tmp = mmDevice.createInsecureRfcommSocketToServiceRecord(UUID_RFCOMM_GENERIC);
} catch (IOException e) {
if (D)
Log.e(TAG, "Socket Type: " + mSocketType + "create() failed", e);
}
mmSocket = tmp;
}
public void run()
{
// Always cancel discovery because it will slow down a connection
mAdapter.cancelDiscovery();
newConnection = false;
// Make a connection to the BluetoothSocket
try {
// This is a blocking call and will only return on a
// successful connection or an exception
mmSocket.connect();
} catch (IOException e) {
// Close the socket
try {
mmSocket.close();
} catch (IOException e2) {
if (D)
Log.e(TAG, "unable to close() " + mSocketType
+ " socket during connection failure", e2);
}
if (!newConnection)
connectionFailed();
return;
}
// Reset the ConnectThread because we're done
synchronized (BluetoothChatService.this) {
mConnectThread = null;
}
// Start the connected thread
connected(mmSocket, mmDevice, mSocketType);
}
public void cancel()
{
try {
mmSocket.close();
} catch (IOException e) {
if (D)
Log.e(TAG, "close() of connect " + mSocketType
+ " socket failed", e);
}
}
}
传递数据
BluetoothChatService.ConnectedThread
成功连接蓝牙设备后,双方均有一个BluetoothSocket
,这时可以分享信息,数据传递流程如下:
- 通过
BluetoothSocket
获取I/O对象InputStream
和OutputStream
; - 通过
read(byte[])
和write(byte[])
进行以字节为单位数据读写,实际上读要不断进行,因此在线程的run()
方法中实现,这里读到后直接解析了,更好地做法应该定义一个parser
类负责数据的解析(坑)。
private class ConnectedThread extends Thread
{
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
public ConnectedThread(BluetoothSocket socket, String socketType)
{
if (D)
Log.d(TAG, "create ConnectedThread: " + socketType);
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// Get the BluetoothSocket input and output streams
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) {
if (D)
Log.e(TAG, "temp sockets not created", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
stopReading = false;
}
public void run()
{
if (D)
Log.i(TAG, "BEGIN mConnectedThread");
byte[] buffer = new byte[1024];
int bytes;
// Keep listening to the InputStream while connected
while (!stopReading) {
try {
if (mmInStream.available() > 0) { // Check if new data is available
bytes = mmInStream.read(buffer); // Read from the InputStream
/*************** 解释器 **************/
String readMessage = new String(buffer, 0, bytes);
String[] splitMessage = readMessage.split(",");
if (D) {
Log.i(TAG, "Received string: " + readMessage);
for (int i = 0; i < splitMessage.length; i++)
Log.i(TAG, "splitMessage[" + i + "]: " + splitMessage[i]);
}
// 命令解析...
}
} catch (IOException e) {
if (D)
Log.e(TAG, "disconnected", e);
if (!stopReading) {
cancel();
connectionLost();
}
return;
}
}
}
/**
* Write to the connected OutStream.
*
* @param buffer The bytes to write
*/
public void write(byte[] buffer)
{
try {
mmOutStream.write(buffer);
} catch (IOException e) {
if (D)
Log.e(TAG, "Exception during write", e);
}
}
public void cancel()
{
stopReading = true;
if (mmInStream != null) {
try {
mmInStream.close();
} catch (Exception ignored) {
}
}
if (mmOutStream != null) {
try {
mmOutStream.close();
} catch (Exception ignored) {
}
}
if (mmSocket != null) {
try {
mmSocket.close();
} catch (Exception ignored) {
}
}
}
}
查找设备
最后补充一下,设备之间是如何相互发现的,这个就是我们使用蓝牙耳机,通过功能键打开耳机的可发现模式,通过系统设置去匹配、连接耳机的过程。如果设备已经匹配可以在“我的设备”(苹果手机)中看见设备信息,但显示“未连接”意味着没有建立RFCOMM共享信道,不可通信,而“匹配”意味着已经交换了建立连接所必要的信息。
通过BluetoothAdapter
可以搜索蓝牙设备或者查询已配对设备列表,已匹配,就是双方协商好了连接使用的UUID
,蓝牙MAC地址等等信息。
通过getBondedDevices()
查询已匹配设备。
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0) {
// There are paired devices. Get the name and address of each paired device.
for (BluetoothDevice device : pairedDevices) {
String deviceName = device.getName();
String deviceHardwareAddress = device.getAddress(); // MAC address
}
}
通过startDiscovery()
开始搜索蓝牙设备,为了接收对方设备的反馈,必须基于ACTION_FOUND
的intent
注册一个BroadcastReceiver
,
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Register for broadcasts when a device is discovered.
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(receiver, filter);
}
// Create a BroadcastReceiver for ACTION_FOUND.
private final BroadcastReceiver receiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// Discovery has found a device. Get the BluetoothDevice
// object and its info from the Intent.
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
String deviceName = device.getName();
String deviceHardwareAddress = device.getAddress(); // MAC address
}
}
};
@Override
protected void onDestroy() {
super.onDestroy();
...
// Don't forget to unregister the ACTION_FOUND receiver.
unregisterReceiver(receiver);
}
调用startActivityForResult(Intent,int)
使设备处于可发现状态,持续两分钟,用于服务端,这个隐含了搜索要求服务客户双方你情我愿的。
int requestCode = 1;
Intent discoverableIntent =
new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivityForResult(discoverableIntent, requestCode);