近期的项目涉及到蓝牙通讯,于是就整理了一下蓝牙的通讯机制的知识点。
蓝牙通讯主要是配对和连接两个过程。
配对和连接是两个不同的概念,请不要混为一谈,配对上的设备不代表已经连接。
首先我们需要权限
<manifest ... >
<uses-permission android:name="android.permission.BLUETOOTH" /> ...
</manifest>
BluetoothAdapter
代表本地蓝牙适配器(蓝牙无线电)。BluetoothAdapter
是所有蓝牙交互的入口。使用这个你可以发现其他蓝牙设备,查询已配对的设备列表,使用一个已知的MAC地址来实例化一个BluetoothDevice
。
//通常我们使用该方法获得蓝牙的本地属性。
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
// 代表设备不支持蓝牙
}
BluetoothDevice
代表一个远程蓝牙设备,使用这个来请求一个与远程设备的BluetoothSocket
连接,或者查询关于设备名称、地址、类和连接状态等设备信息。
//通过mac地址来获得远程蓝牙设备,通常我们也使用查找设备的广播来获得远程蓝牙设备,稍后会介绍
mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(macAddress);
//该类的方法与adapter类似
开启蓝牙
(以下的mBluetoothAdapter
表示本地蓝牙设备,mBluetoothDevice
表示远程蓝牙设备)
方法一:
//利用系统设置开启蓝牙
Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 3600);
startActivity(discoverableIntent);
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
方法二
mBluetoothAdapter.enable()
//需要权限
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
蓝牙搜寻广播
Action 静态注册
<intent-filter>
<action android:name="android.bluetooth.device.action.FOUND" />
</intent-filter>
动态注册
// 定义一个广播 for ACTION_FOUND
private final BroadcastReceiver mReceiver = 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);
// 保存设备的mac地址与蓝牙名称
mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
}
}
};
// 动态注册代码
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter);
//不要忘了在onDestroy注销广播哟
扫描代码
mBluetoothAdapter.startDiscovery();//开始扫描
//蓝牙扫描是非常耗费资源与时间的,当我们扫描到需要操作的设备的时候,我们需要停止扫描来获得更好的连接效率。
mBluetoothAdapter.cancelDiscovery();//取消扫描
设置蓝牙可被搜索
Intent discoverableIntent = newIntent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);//后面的参数是可被发现的时间,最大支持3600秒
startActivity(discoverableIntent);
蓝牙配对
如果只是单纯的配对,我们可以利用反射:
(这里将会介绍两张配对方式,反射是第一种,第二种是通过连接来配对,这里的配对都不需要输入pin
码的)
try {
//检查是否处于未配对状态
if (mBluetoothDevice.getBondState() == BluetoothDevice.BOND_NONE) {
Method creMethod = BluetoothDevice.class.getMethod("createBond");
Log.e("TAG", "开始配对");
creMethod.invoke(mBluetoothDevice);
}
} catch (Exception e) {
// TODO: handle exception
//DisplayMessage("无法配对!");
e.printStackTrace();
}
取消配对
static public boolean removeBond(Class btClass, BluetoothDevice btDevice)
throws Exception
{
Method removeBondMethod = btClass.getMethod("removeBond");
Boolean returnValue = (Boolean) removeBondMethod.invoke(btDevice);
return returnValue.booleanValue();
}
连接与配对
是的蓝牙的连接类似
TCP的socket
连接,但是不同的是蓝牙的BluetoothSock
不是new出来的,而是通过一个静态方法根据UUID
生成的,而不是端口号。同一时间一个通道只允许一个socket
连接通讯(多线程或许能解决这个问题),但是大多蓝牙情景都是一对一的连接。
为了在两台设备上创建一个连接,你必须实现服务器端和客户端两头的机制,因为一个设备必须打开一个服务器socket
,而另一个设备初始化创建(使用服务器设备的MAC地址来初始化一个连接)。当他们在相同的RFCOMM
通道上有一个已连接的BluetoothSocket
时,服务器和客户被认为是互相连接了。
**注意:如果两台设备之前没有配对过,那么Android框架将会自动显示一个请求配对的通知或对话框。因此,当尝试连接设备时,你的应用不需要考虑设备是否配对过。你的RFCOMM
连接尝试将会阻塞,知道用户成功配对,或者用户拒绝失败时,或者配对失败,或者超时
**
BluetoothSocket
代表一个蓝牙socket
的接口(和TCP Socket
类似)。这是一个连接点,它允许一个应用与其他蓝牙设备通过InputStream
和OutputStream
交换数据。
BluetoothServerSocket secure = null;
if(sdk>=10){
secure = adapter.listenUsingRfcommWithServiceRecord(app_name, SECURE_UUID);
}else{
secure = adapter.listenUsingRfcommWithServiceRecord(app_name, SECURE_UUID);
}
//UUID可以参考网上内容自己生成
UUID
一个全局唯一的标识符(
UUID
)是一个标准的128-bit格式的string ID
,它被用于唯一标识信息。一个UUID
的关键点是它非常大以至于你可以随机选择而不会发生崩溃。在这种情况下,它被用于唯一地指定你的应用中的蓝牙服务。为了得到一个UUID
以在你的应用中使用,你可以使用网络上的任何一种随机UUID产生器,然后使用fromString(String)
初始化一个UUID
。
BluetoothServerSocket
代表一个开放的服务器socket
,它监听接受的请求(与TCP ServerSocket
类似)。为了连接两台Android设备,一个设备必须使用这个类开启一个服务器socket
。当一个远程蓝牙设备开始一个和该设备的连接请求,BluetoothServerSocket
将会返回一个已连接的BluetoothSocket
,接受该连接。
BluetoothSocket socket;
if (sdk < 10) {
socket = device.createRfcommSocketToServiceRecord(BlueToothControl.SECURE_UUID);
} else {//sdk >= 10
socket = device.createInsecureRfcommSocketToServiceRecord(BlueToothControl.INSECURE_UUID);
}
//UUID可以参考网上内容自己生成
下面我会举一个栗子,并且说明是如何进行通讯的
服务端
private class AcceptThread extends Thread {
private final BluetoothServerSocket mmServerSocket;
public AcceptThread() {
// 使用一个临时对象来标志mmServerSocket,
// 因为mmServerSocket是final类型
BluetoothServerSocket tmp = null;
try {
// MY_UUID 是应用的 UUID string, 同样也在客户端使用
tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) { }
mmServerSocket = tmp;
}
public void run() {
BluetoothSocket socket = null;
// 保持监听直到建立连接或者发生异常
while (true) {
try {
socket = mmServerSocket.accept();
} catch (IOException e) {
break;
}
// 如果接收到连接
if (socket != null) {
// 用一个线程去做一些连接的管理工作
manageConnectedSocket(socket);
mmServerSocket.close();
break;
}
}
}
//该方法用于中断监听并且断开连接
public void cancel() {
try {
mmServerSocket.close();
} catch (IOException e) { }
}
}
- 用
listenUsingRfcommWithServiceRecord(String, UUID)
得到一个BluetoothServerSocket
。这个String是你的服务的标志名称,系统将会把它写入设备中的一个新的服务发现协议(SDP
)数据库条目中(名字是任意的,并且可以只是你应用的名字)。UUID
同样被包含在SDP
条目中,并且将会成为和客户端设备连接协议的基础。也就是说,当客户端尝试连接这个设备时,它将会携带一个UUID
用于唯一指定它想要连接的服务器。这些UUIDs
必须匹配以便该连接可以被接受(在下一步中)。 通过调用accept()
开始监听连接请求。
- 通过调用
accept()
开始监听连接请求。这一个阻塞调用。在一个连接被接受或一个异常出现时,它将会返回。只有当一个远程设备使用一个UUID
发送了一个连接请求,并且该UUID和正在监听的服务器socket
注册的UUID
相匹配时,一个连接才会被接受。成功后,accept()
将会返回一个已连接的BluetoothSocket
。
- 调用
close()
,除非你想要接受更多的连接。这将释放服务器socket
和它所有的资源,但是不会关闭accept()
返回的已连接的BluetoothSocket
。不同于TCP/IP
,RFCOMM
仅仅允许每一个通道上在某一时刻只有一个已连接的客户端,因此在大多数情况下在接受一个已连接的socket
后,在BluetoothServerSocket
上调用close()
是非常必要的。
-
accept()
不应该再主活动UI
线程上执行,因为它是一个阻塞调用,并且将会阻止任何与应用的交互行为。它通常在你的应用管理的一个新的线程中使用一个BluetoothServerSocket
或BluetoothSocket
来完成所有工作。为了中止一个阻塞调用,例如accept()
,从你的其他线程里在BluetoothServerSocket
(或BluetoothSocket
) 上调用close()
,然后阻塞调用就会立即返回。注意在BluetoothServerSocket
或BluetoothSocket
上所有的方法都是线程安全的。
客户端
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
//使用临时变量来标志mmSocket,
// 因为 mmSocket 是 final
BluetoothSocket tmp = null;
mmDevice = device;
// 通过得到的BluetoothDevice 获取 BluetoothSocket来连接
try {
// MY_UUID 是应用的UUID string, 同样也用于服务端
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) { }
mmSocket = tmp;
}
public void run() {
// 你应该在连接前总是这样做,而不需要考虑是否真的有在执行查询任务(但是如果你想要检查,调用 isDiscovering())
//检查会很大程度影响效率
mBluetoothAdapter.cancelDiscovery();
try {
// 通过socket.connect()来连接. 同时也会阻塞线程
// 直到连接成功或者抛出异常
mmSocket.connect();
} catch (IOException connectException) {
// 无法连接,关闭socket并退出
try {
mmSocket.close();
} catch (IOException closeException) { }
return;
}
//做一些管理socket的工作
manageConnectedSocket(mmSocket);
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}
为了和一个远程设备(一个持有服务器
socket
的设备)初始化一个连接,你必须首先得到一个BluetoothDevice
对象来表示这个远程设备。(上面的课程Finding Devices
讲述了如何得到一个BluetoothDevice
)。然后你必须使用BluetoothDevice
来得到一个BluetoothSocket
,然后初始化该连接。
下面是基本的过程:
- 使用
BluetoothDevice
,通过调用createRfcommSocketToServiceRecord(UUID)
来得到一个BluetoothSocket
。T这将初始化一个BluetoothSocket
,它连接到该BluetoothDevice
。这里传递的UUID必须和服务器设备开启它的BluetoothServerSocket
时使用的UUID
相匹配。
- 通过调用
connect()
初始化一个连接。执行这个调用时,系统将会在远程设备上执行一个SDP
查找工作,来匹配UUID
。如果查找成功,并且远程设备接受了连接,它将会在连接过程中分享RFCOMM
通道,而connect()
将会返回。这个方法是阻塞的。如果,处于任何原因,该连接失败了或者connect()
超时了(大约12秒以后),那么它将会抛出一个异常。
因为connect()
是一个阻塞调用,这个连接过程应该总是在一个单独的线程中执行。
注意:你应该总是确保在你调用connect()
时设备没有执行设备查找工作。如果正在查找设备,那么连接尝试将会很大程度的减缓,并且很有可能会失败。
**当你使用完你的 BluetoothSocket
后,总是调用close()
来清除资源。这样做将会立即关闭已连接的socket
,然后清除所有的内部资源
**
读写流
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;
// 获得socket的流信息
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) { }
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
byte[] buffer = new byte[1024]; //缓冲字符数组
// 保持通讯
while (true) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
// 发送包含的信息给UI
mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
break;
}
}
}
/* 调用该方法去发送信息 */
public void write(byte[] bytes) {
try {
mmOutStream.write(bytes);
} catch (IOException e) { }
}
/* 调用该方法关闭连接*/
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}
**当然,实现的细节需要考虑。首先并且最重要的是,你应该为所有输入和输出的数据流使用一个专属的线程。这是十分重要的,因为read(byte[])
和 write(byte[])
方法都是阻塞调用。 read(byte[])
将会发生阻塞知道送数据流中读取到了一些东西。write(byte[])
不经常发生阻塞,但是当远程设备没有足够迅速地调用read(byte[])
而中间缓冲区已经负载时可以阻塞。因此,你的线程中的主要循环应该是专门从InputStream
中读取数据的。一个单独的公共方法可以被用于初始化向OutputStream
中写入数据。
**
如文中有错误,欢迎指出。