从Android 6.0之后,想要扫描低功率蓝牙设备应用除了蓝牙权限还需要拥有访问设备位置的ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION权限,另外,使用6.0的API可以不注册广播!
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
BluetoothAdapter表示本地蓝牙适配器,通过它来调起手机里面的蓝牙开关,扫描、停止扫描里都需要对它做判断,判断手机里的蓝牙开关处于打开状态才能开始、停止扫描,不然异常直接导致闪退。
BluetoothDevice表示扫描到的远程蓝牙设备,该类不可继承。提它是因为它里面包含了设备名称、地址、连接状态、设备类型等,但偏偏没有rssi(信号强度)这个字段,如果你刚好有信号强度相关的需求就有点尴尬了。
BluetoothLeScanner是6.0API中的扫描器,它提供了对Bluetooth BLE设备扫描相关的操作,通过BluetoothAdapter的getBluetoothLeScanner()得到实例。使用它的时候记得判断适配器是否打开,原因上面说了。
ScanCallback是扫描BLE设备的回调,在获取设备的时候记得去重,不然设备列表里一堆重复的设备。
ScanFilter为BLE设备扫描结果的过滤条件,目前支持:UUID、设备名、设备的mac地址、服务数据和制造商数据。
ScanResult即为扫描器扫描出来的结果,BluetoothDevice就是从这里得到的。同时还有信号强度、设备类型、发送功率等属性。
ScanSettings是扫描时的参数设置,比如设置速度优先或者省电优先又或者是特殊的扫描模式。
关于ScanResult和ScanSettings我不推荐使用,而是自己手写实现的过滤!原因:API提供的过滤太“死板”,比如只扫出名字叫“A1B2C3”的设备那么就必须写上全限定名,错/漏一个字都不行!自己实现的话就可以稍微灵活点,但是效率赶人家的API还是差点儿,大家根据业务自行取舍吧。
/**
* 开启本地蓝牙
*/
private void OpenBluetoothAdapter()
{
m_bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if(m_bluetoothAdapter != null)
{
//未打开蓝牙,才需要打开蓝牙
//会以Dialog样式显示一个Activity,我们可以在onActivityResult()方法去处理返回值
if(!m_bluetoothAdapter.isEnabled())
{
Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent, 1);
}
else
{
switch(m_bluetoothAdapter.getState())
{
//蓝牙已开启则直接开始扫描设备
case BluetoothAdapter.STATE_ON:
case BluetoothAdapter.STATE_TURNING_ON:
m_btn1.setText("STOP");
m_bluetoothLeScanner = m_bluetoothAdapter.getBluetoothLeScanner();
break;
//蓝牙未开启
case BluetoothAdapter.STATE_OFF:
case BluetoothAdapter.STATE_TURNING_OFF:
default:
m_btn1.setText("SCAN");
break;
}
}
}
else
{
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setPositiveButton("知道了", new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface dialog, int which)
{
System.exit(0);
}
});
builder.setMessage("设备不支持蓝牙");
builder.show();
}
}
很简单,使用扫描器的startScan()和stopScan()就可以了,开始前先判断是否已经有扫描,我这里没有判断直接先停止再开始。
/**
* 开始搜索蓝牙
* 开始扫描时候要确保蓝牙适配器处于开启状态
*/
private void BeginDiscovery()
{
if(IsBluetoothAvailable())
{
m_btn1.setText("STOP");
m_bluetoothLeScanner.stopScan(scanCallback);
m_bluetoothLeScanner.startScan(scanCallback);
}
else
{
Toast.makeText(m_context, "请确认系统蓝牙是否开启", Toast.LENGTH_SHORT);
}
}
/**
* 取消搜索蓝牙
* 取消扫描的时候要确保蓝牙适配器处于开启状态
*/
private void CancelDiscovery()
{
if(IsBluetoothAvailable())
{
m_btn1.setText("SCAN");
m_bluetoothLeScanner.stopScan(scanCallback);
}
}
/**
* 检查蓝牙适配器是否打开
* 在开始扫描和取消扫描的都时候都需要判断适配器的状态以及是否获取到扫描器,否则将抛出异常IllegalStateException: BT Adapter is not turned
* ON.
*
* @return
*/
private boolean IsBluetoothAvailable()
{
return (m_bluetoothLeScanner != null && m_bluetoothAdapter != null && m_bluetoothAdapter.isEnabled() && m_bluetoothAdapter.getState() == BluetoothAdapter.STATE_ON);
}
里面的MyBluetoothDevice为自定义的,为了方便按信号强度排序以及手动实现按信号强度过滤,就将BluetoothDevice和rssi封装进去了。如果你不需要信号强度的话直接用BluetoothDevice就够了。
private final ScanCallback scanCallback = new ScanCallback()
{
@Override
public void onScanResult(int callbackType, ScanResult result)
{
super.onScanResult(callbackType, result);
BluetoothDevice bluetoothDevice = result.getDevice();
String name = bluetoothDevice.getName();
String address = bluetoothDevice.getAddress();
int rssi = result.getRssi();
int state = bluetoothDevice.getBondState();
//设备去重
if(m_deviceMap.containsKey(address))
{
MyBluetoothDevice device = m_deviceMap.get(address);
device.set_name(name);
device.set_rssi(rssi);
device.set_boundState(state);
m_deviceMap.put(address, device);
}
else
{
MyBluetoothDevice device = new MyBluetoothDevice();
device.set_name(name);
device.set_address(address);
device.set_rssi(rssi);
device.set_boundState(state);
device.set_bluetoothDevice(bluetoothDevice);
m_deviceMap.put(address, device);
}
//根据条件筛选设备
Map map = FilterDeviceByCondition(m_deviceMap);
m_deviceList = new ArrayList<>(map.values());
//按照信号强度降序排序
Collections.sort(m_deviceList, new Comparator()
{
@Override
public int compare(MyBluetoothDevice o1, MyBluetoothDevice o2)
{
if(o1.get_rssi() > o2.get_rssi())
return -1;
if(o1.get_rssi() < o2.get_rssi())
return 1;
return 0;
}
});
m_bluetoothListAdapter.setM_deviceList(m_deviceList);
Log.i(TAG, String.format("设备%s的信号强度%d", address, rssi));
//使用notifyDataSetChanged()会保存当前的状态信息,然后更新适配器里的内容
m_bluetoothListAdapter.notifyDataSetChanged();
m_listView.setOnItemClickListener(BluetoothFragment_List.this);
}
@Override
public void onBatchScanResults(List results)
{
super.onBatchScanResults(results);
}
@Override
public void onScanFailed(int errorCode)
{
super.onScanFailed(errorCode);
//扫描失败返回的参数有4个
//errorCode=1:已启动具有相同设置的BLE扫描
//errorCode=2:应用未注册
//errorCode=3:内部错误
//errorCode=4:设备不支持低功耗蓝牙
}
};
之前的API有人说在连接成功后不停止扫描再次扫描会发生阻塞导致扫描不到BLE设备,关于这点6.0里暂时没有这种情况,连接BLE设备成功后停不停止扫描看你业务需要。另外,在处理业务的过程中要保证你的设备和适配器对象不被销毁,之所以说这个是因为有时候因为业务需要要进行页面跳转,跳转过程中要注意生命周期。
import java.util.UUID;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
UUID不管是传统的蓝牙(BT)还是低功耗蓝牙(BLE)在连接的时候都需要这个通用唯一标识符,它用于标识应用程序的蓝牙服务。比如发现、读、写服务都有它们对应的UUID,这些UUID由硬件工程师或者开发文档里提供,要是都没有只能通过第三方工具查看了,这里推荐nRF Connect这款工具。
BluetoothGatt它定义了Service(服务)和Characteristic(特征),是一个ATT服务框架,官方释义:蓝牙GATT配置文件的公共API吧啦吧啦吧啦……反正就是它提供了蓝牙的通信功能,发现BLE设备和读写数据都要通过它获取服务,再通过UUID获取到特征才能实现,并把相应的结果返回到BluetoothGattCallback。
BluetoothGattCallback设备连接的过程、结果都在这个类里。
BluetoothGattCharacteristic简称特征,用于构造BluetoothGattService。
BluetoothGattDescriptor对特征的描述和控制特征的行为,比如订阅读取通知时使用它的ENABLE_NOTIFICATION_VALUE属性来启用设备通知。
BluetoothGattService代表蓝牙GATT的服务,一个服Service包含多BluetoothGattCharacteristic。
发现设备并根据UUID找到对应的特征后才算是真正与BLE设备建立了连接,现在的BLE设备基本都是以notify的方式返回数据,所以需要在找到读取的特征那里订阅读取通知,不然收不到数据。
if(m_bluetoothDevice != null)
{
Log.d(TAG, ">>>开始连接...");
m_bluetoothGatt = m_bluetoothDevice.connectGatt(m_context, false, new BluetoothGattCallback()
{
/**
* 连接状态改变时回调
* @param gatt
* @param status
* @param newState
*/
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
{
if(status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED)
{
Log.d(TAG, ">>>成功建立连接!");
//发现服务
gatt.discoverServices();
}
else
{
Log.d(TAG, ">>>连接已断开!");
m_bluetoothGatt.close();
}
}
/**
* 发现设备(真正建立连接)
* @param gatt
* @param status
*/
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status)
{
//直到这里才是真正建立了可通信的连接
//通过UUID找到服务
m_bluetoothGattService = m_bluetoothGatt.getService(SERVICE_UUID);
if(m_bluetoothGattService != null)
{
//写数据的服务和特征
m_bluetoothGattCharacteristic_write = m_bluetoothGattService.getCharacteristic(WRITE_UUID);
if(m_bluetoothGattCharacteristic_write != null)
{
Log.d(TAG, ">>>已找到写入数据的特征值!");
}
else
{
Log.e(TAG, ">>>该UUID无写入数据的特征值!");
}
//读取数据的服务和特征
m_bluetoothGattCharacteristic_read = m_bluetoothGattService.getCharacteristic(READ_UUID);
if(m_bluetoothGattCharacteristic_read != null)
{
Log.d(TAG, ">>>已找到读取数据的特征值!");
//订阅读取通知
gatt.setCharacteristicNotification(m_bluetoothGattCharacteristic_read, true);
BluetoothGattDescriptor descriptor = m_bluetoothGattCharacteristic_read.getDescriptor(CONFIG_UUID);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
}
else
{
Log.e(TAG, ">>>该UUID无读取数据的特征值!");
}
}
}
/**
* 写入成功后回调
*
* @param gatt
* @param characteristic
* @param status
*/
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)
{
byte[] byteArray = characteristic.getValue();
String result = ConvertUtil.ByteArrayToHexStr(byteArray);
result = ConvertUtil.HexStrAddCharacter(result, " ");
Log.d(TAG, ">>>写入:" + result);
}
/**
* 收到硬件返回的数据时回调,如果是Notify的方式
* @param gatt
* @param characteristic
*/
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)
{
byte[] byteArray = characteristic.getValue();
String result = ConvertUtil.ByteArrayToHexStr(byteArray);
result = ConvertUtil.HexStrAddCharacter(result, " ");
Log.d(TAG, ">>>接收:" + result);
}
});
}
else
{
AlertDialog.Builder builder = new AlertDialog.Builder(m_context);
builder.setTitle("警告");
builder.setMessage("未获取到蓝牙,请重试!");
builder.setPositiveButton("知道了", (dialog, which) ->
{
});
builder.show();
}
BLE传输每包最多20字节,超过需要分包处理。关于读取数据可以稍稍参考一下我的这篇博文:https://blog.csdn.net/baidu_36743221/article/details/102967689
/**
* 收到硬件返回的数据时回调,如果是Notify的方式
* @param gatt
* @param characteristic
*/
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic)
{
byte[] byteArray = characteristic.getValue();
String result = ConvertUtil.ByteArrayToHexStr(byteArray);
result = ConvertUtil.HexStrAddCharacter(result, " ");
Log.d(TAG, ">>>接收:" + result);
}
发送的话也比较简单,如果想知道数据是否发送成功可以在BluetoothGattCallback类的onCharacteristicWrite()里看到。
String hexStr = "680000000000006810000100E116";
Log.d(TAG, ">>>发送:" + hexStr);
byte[] byteArray = ConvertUtil.HexStrToByteArray(hexStr);
m_bluetoothGattCharacteristic_write.setValue(byteArray);
m_bluetoothGatt.writeCharacteristic(m_bluetoothGattCharacteristic_write);
……差不多就先写这么多吧,代码涉及到业务,全部贴上来也没什么用,关注DeviceList和BLEUtil这两个类就可以了。
GitHub地址:https://github.com/QQ652276536/BLEControl.git
如果碰巧对你有帮助还望给个star,谢谢~