文章目录
前言
几篇博客写下来,发现写博客还是挺花时间的,但是收获也不少,可以对以前做过的东西做一个比较全面的总结,加深印象,而不是在本地笔记上随便做点简单的记录敷衍了事,个人觉得在时间允许的情况还是要抽时间多做总结。
简介
在之前写的文章 基于蓝牙Ble的Android应用开发 中提到过,除了从Android4.3之后引入的Ble低功耗蓝牙之外,还有一种很早之前存在的比较常见的就是基于Spp协议的经典蓝牙,也就是我们经常说的传统蓝牙。两种蓝牙的主要区别在上一篇中也已经做了介绍,就不再赘述。
那么传统蓝牙有哪些应用场景呢?举几个比较常见的列子,比如说我们常见的蓝牙耳机、蓝牙音箱等,基本上都是基于传统蓝牙开发的。那么传统蓝牙开发和Ble开发主要有哪些异同点呢?让笔者在开发步骤中和Ble开发步骤对比,一 一简要说明。
开发步骤
第一、第二步和Ble开发的步骤一样,第三步注册的广播不一样
准备
准备需要用到的UUID:
public static final UUID SPP_UUID =UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
和Ble开发类似,先准备好需要用到的UUID,但是这个UUID有点特别,它不再是从硬件或者嵌入式工程师手中拿过来,而是协议栈中默认的UUID(需要用到这个UUID的系统方法有说明,后面会说)。另外需要注意的是,根据Google开发文档中的介绍,当前如果是连接的对象是另一台Android手机,则需要只用自定义的UUID。
第一步 配置清单文件
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Android6.0及以上必须获取位置权限,否则无法扫描到周边的蓝牙设备 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
第二步 检查设备,获取BluetoothAdapter
/**
* 判断该设备是否支持Ble并获取BluetoothAdapter
*/
public Boolean ensureBLEExists() {
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
return false;
}
//获取BluetoothAdapter
BluetoothManager bm = (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
if (bm!=null) mBluetoothAdapter = bm.getAdapter();
return true;
}
第三步 注册广播,开启蓝牙
/**
* 注册蓝牙监听广播
*/
public void registerReceiver(){
if (mBluetoothReceiver==null) mBluetoothReceiver=new BluetoothReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);//蓝牙开关状态
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);//蓝牙开始扫描
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//蓝牙扫描结束
filter.addAction(BluetoothDevice.ACTION_FOUND);//发现设备
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);//设备绑定状态变化
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);//连接上设备
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);//连接断开
filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST);//收到配对请求
mContext.registerReceiver(mBluetoothReceiver,filter);
}
private class BluetoothReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// if (D) LogUtil.i("收到广播:"+action);
switch (action) {
case BluetoothAdapter.ACTION_STATE_CHANGED:
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
switch (state) {
case BluetoothAdapter.STATE_ON:
//蓝牙开启
break;
case BluetoothAdapter.STATE_OFF:
//蓝牙关闭
break;
}
break;
case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
//开始扫描
break;
case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
//扫描结束
break;
case BluetoothDevice.ACTION_FOUND: //发现设备
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
short rssi=intent.getExtras().getShort(BluetoothDevice.EXTRA_RSSI);//信号强度
// 在此可以通过设备名字对设备进行过滤,然后回调通知
break;
case BluetoothDevice.ACTION_BOND_STATE_CHANGED: //配对状态变化
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE);
device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
switch (bondState) {
case BluetoothDevice.BOND_BONDING:
LogUtil.i("Device:" + device.getName() + " 配对中...");
break;
case BluetoothDevice.BOND_BONDED:
LogUtil.i("Device:" + device.getName() + " 配对成功");
//配对成功,连接设备
connect(device);
break;
case BluetoothDevice.BOND_NONE:
LogUtil.i("Device:" + device.getName() + " 未配对");
break;
}
break;
case BluetoothDevice.ACTION_PAIRING_REQUEST:
//这里接收到配对请求会弹出一个配对框要求输入配对码
LogUtil.i("接收到配对请求");
device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
//如果想免去手动输入配对码,可以使用下面的反射方法自动设置配对码进行配对
//str.getBytes()中的str为配对码字符串,替换即可
try {
Method removeBondMethod = device.getClass().getDeclaredMethod("setPin", new Class[]{byte[].class});
Boolean returnValue = (Boolean) removeBondMethod.invoke(device ,new Object[]{str.getBytes()});
Log.e("returnValue", "" + returnValue);
} catch (SecurityException e) {
// throw new RuntimeException(e.getMessage());
e.printStackTrace();
} catch (IllegalArgumentException e) {
// throw new RuntimeException(e.getMessage());
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
break;
case BluetoothDevice.ACTION_ACL_CONNECTED:
// mHandler.sendEmptyMessage(STATE_CONNECTED);
break;
case BluetoothDevice.ACTION_ACL_DISCONNECTED:
mHandler.sendEmptyMessage(STATE_DISCONNECTED);
break;
default:
break;
}
}
}
/**
* 开启蓝牙
*/
public void enableBluetooth() {
if (mBluetoothAdapter!=null){
if (!mBluetoothAdapter.isEnabled()) { //蓝牙未开启,通过隐式意图请求开启蓝牙
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, 0);
}
}
}
第四步 扫描设备
private void startDeviceScan() {
//如果当前正在扫描则先停止扫描
if (mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
}
//开始扫描
mBluetoothAdapter.startDiscovery();
}
第五步 连接设备
public void connect(BluetoothDevice device){
//扫描是比较耗费资源的,在进行连接的时候要先将扫描停掉
if (mBluetoothAdapter.isDiscovering()) {
mBluetoothAdapter.cancelDiscovery();
}
//如果设备还没有绑定过,要先绑定设备,绑定成功后会收到绑定成功的通知,然后我们再次调用这个连接方法
if (device.getBondState()==BluetoothDevice.BOND_NONE){
device.createBond();
return;
}
if(device==null||mState==BluetoothState.STATE_CONNECTING||mState==BluetoothState.STATE_CONNECTED) return;
if (mConnectedThread!=null){
mConnectedThread.cancel();
mConnectedThread=null;
}
if (mConnectThread!=null){
mConnectThread.cancel();
mConnectThread=null;
}
mConnectThread =new ConnectThread(device);
mConnectThread.start();
}
//---------------------------------------------------------------------------------------------
/**
* 执行连接操作的线程
* 注:连接会阻塞,需要在子线程中执行
*/
private class ConnectThread extends Thread{
private BluetoothSocket mSocket;
public ConnectThread(BluetoothDevice device) {
BluetoothSocket temp=null;
//获取建立连接需要的套接字然后进行连接
try {
//加密传输,Android强制执行配对,弹窗显示输入配对码,推荐使用这种
temp = device.createRfcommSocketToServiceRecord(SPP_UUID);
/*
temp = device.createInsecureRfcommSocketToServiceRecord(SPP_UUID);
明文传输(不安全),无需配对,通常使用于蓝牙2.1设备,因为对于蓝牙2.1设备
如果有任何设备没有具有输入和输出能力或显示数字键,无法进行安全套接字连接
*/
} catch (IOException e) {
e.printStackTrace();
}
mSocket=temp;
}
@Override
public void run() {
try {
mSocket.connect();//建立socket连接
} catch (Exception e) {
// LogUtil.e("连接异常:"+e.getMessage());
//针对连接异常的处理
try {
mSocket = (BluetoothSocket) mCurrentConnectDevice.getClass().getMethod("createRfcommSocket", new Class[] {int.class}).invoke(mCurrentConnectDevice,1);
mSocket.connect();
} catch (Exception e2) {
try {
mSocket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
return;
}
}
connected(mSocket);
}
public void cancel(){
try {
mSocket.close();
} catch (IOException e) { }
}
}
//---------------------------------------------------------------------------------------------
/**
* 进行 发送/接收 数据的线程
* 注:连接会阻塞,需要在子线程中执行
*/
private class ConnectedThread extends Thread {
private boolean isSending=false;//是否正在发送数据
private final BluetoothSocket mmSocket;
private final InputStream mInStream;
private final OutputStream mOutStream;
public ConnectedThread(BluetoothSocket socket) {
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// 获取输入输出流
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) { }
mInStream = tmpIn;
mOutStream = tmpOut;
//到这里可以认为已经成功的建立了连接
mHandler.sendEmptyMessage(STATE_CONNECTED);
}
public void run() {
int num;//从流中读取到的有效字节数
//用一个1024字节的数据接收数据,可根据需要调整大小
byte[] buffer = new byte[1024];
byte[] buffer_new = null;//实际收到的数据
//对输入流保持监听直至一个异常发生
while (true) {
try {
num= mInStream.read(buffer);
buffer_new=new byte[num];
//截取有效数据
System.arraycopy(buffer,0,buffer_new,0,num);
//可用传统的Handle或者回调方式将接收到的数据传出去
mHandler.obtainMessage(RECEIVE_DATA, bytes, -1, buffer_new)
.sendToTarget();
} catch (IOException e) {
break;
}
}
}
/**
* 向蓝牙模块发射数据
* @param bytes
*/
public void write(byte[] bytes) {
if (D)LogUtil.i("write()发送内容:"+ StringByteUtils.byte2HexStr(bytes));
if (isSending||bytes==null||bytes.length==0) return;
isSending=true;
try {
mOutStream.write(bytes);
} catch (IOException e) {
e.printStackTrace();
}
isSending=false;
}
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}
}
spp协议的蓝牙的设备连接与Ble连接方式有很大的区别,它首先要先进行一个绑定操作,然后才能进行连接,连接是通过socket套接字的方式进行连接,获取套接字系统提供了两个方法:
device.createRfcommSocketToServiceRecord(SPP_UUID);
device.createInsecureRfcommSocketToServiceRecord(SPP_UUID);
RFCOMM是一个简单传输协议,其目的为了解决如何在两个不同设备上的应用程序之间保证一条完整的通信路径,并在它们之间保持一段通信的问题。这两个方法的区别在代码中已经做了详细的注释,就不再说明。说一下方法的参数,如果点进这其中一个方法中查看方法声明你会在方法声明的后半部分看到这段:
* <p>Hint: 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.
“SPP UUID 00001101-0000-1000-8000-00805F9B34FB”,这就是“准备”那一步中需要准备的UUID。
/**
* 发送数据
* @param data
*/
protected void sendData(byte[] data) {
if (mIsConnected){
if (mConnectedThread !=null) mConnectedThread.write(data);
}
}
第七步 退出释放资源
public void disconnect() {
if (mConnectThread!=null){
mConnectThread.cancel();
mConnectThread=null;
}
if (mConnectedThread!=null){
mConnectedThread.cancel();
mConnectedThread=null;
}
if (mBluetoothAdapter != null&&mBluetoothAdapter.isDiscovering()) {//停止扫描,关闭蓝牙
mBluetoothAdapter.cancelDiscovery();
}
mContext.unregisterReceiver(mBluetoothReceiver);
}
容易碰到的错误
java.io.IOException: read failed, socket might closed or timeout, read ret: -1
发生这个错误的主要原因是在调用createRfcommSocketToServiceRecord(UUID uuid)的时候,其内部默认指定端口为-1,至于为什么出现这个错误的原因网上有几个比较专业的讨论 “https://www.e-learn.cn/content/wangluowenzhang/12377”,由于笔者手上没有比较大量的设备进行一一测试,所有野不敢妄下定论真正的原因,但在调用createRfcommSocketToServiceRecord(UUID uuid)发生异常的时候,通过以下反射的方式将连接端口设置为1基本可以解决,这个方法以前是public的,后面不知道为什么Google将其Hide了,或许是出于安全的考虑。
try {
mSocket = (BluetoothSocket) mCurrentConnectDevice.getClass().getMethod("createRfcommSocket", new Class[] {int.class}).invoke(mCurrentConnectDevice,1);
mSocket.connect();
} catch (Exception e2) {
LogUtil.e("连接异常:"+e2.getMessage().toString());
setStateChange(BluetoothState.STATE_FAILED);
if (!mIsConnected){
try {
mSocket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
return;
}