STM32循迹小车/Android蓝牙控制小车(完结篇)
这是这个系列博文的最后一篇,这篇只讲Android经典蓝牙的应用。
在这个系列开篇之前,我并没有接触过Andorid蓝牙开发以及蓝牙协议,在查找资料的时候发现网上关于蓝牙的资料虽然很多,却大多不够完整或者详细,缺少一篇对新手友好的傻瓜式教程,所以在项目过程中我就一直想写一遍详细的傻瓜式教程,让新接触蓝牙开发的朋友少走弯路!
Android的蓝牙开发分为两个大类:经典蓝牙&低功耗蓝牙,关于两者的区别可以去网上查找资料,本篇只针对经典蓝牙开发。
首先,需要理清一下蓝牙开发的流程。
1、权限
权限申请,除了蓝牙相关的权限之外还需要申请一个定位权限,扫描附近蓝牙的需要定位。
<uses-permission android:name="android.permission.BLUETOOTH" /> <!-- 扫描蓝牙设备或者操作蓝牙设置 -->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <!-- 模糊定位权限,仅作用于6.0+ -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- 精准定位权限,仅作用于6.0+ -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
2、设备是否支持蓝牙
BluetoothAdapter.getDefaultAdapter() 返回值不为null,既表示设备支持蓝牙。
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
mBluetoothAdapter != null;
3、开启蓝牙
if(isSupportBlue()) {//设备是否支持蓝牙,支持就开启蓝牙。
if (!isBlueEnable())
openBlueSync(Main2Activity.this, REQUEST_ENABLE_BT);//如果当前蓝牙没有打开,则打开蓝牙
}else Toast.makeText(Main2Activity.this,"该设备不支持蓝牙!",Toast.LENGTH_SHORT).show();
4、开启GPS服务
**a、**判断并开启GPS权限
/**
* 运行时检查权限,有权限则开启GPS服务,没有权限则向用户申请权限
*/
private void checkPermissions() {
String[] permissions = {Manifest.permission.ACCESS_FINE_LOCATION};
List<String> permissionDeniedList = new ArrayList<>();
for (String permission : permissions) {
int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
onPermissionGranted(permission); //有开启GPS权限,则打开GPS
} else {
permissionDeniedList.add(permission);
}
}
if (!permissionDeniedList.isEmpty()) { //没有权限,则调用ActivityCompat.requestPermissions申请权限
String[] deniedPermissions = permissionDeniedList.toArray(new String[permissionDeniedList.size()]);
ActivityCompat.requestPermissions(this, deniedPermissions, REQUEST_CODE_PERMISSION_LOCATION);
}
}
**b、**开启GPS之前需要先判断GPS是否打开。
/**
* 检查GPS是否打开
* @return
*/
private boolean checkGPSIsOpen() {
LocationManager locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
if (locationManager == null)
return false;
return locationManager.isProviderEnabled(android.location.LocationManager.GPS_PROVIDER);
}
**c、**GPS权限需要危险类权限申请完成之后,需要在onRequestPermissionsResult回调函数中判断是否取得GPS权限。
/**
* 权限回调
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public final void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case REQUEST_CODE_PERMISSION_LOCATION:
if (grantResults.length > 0) {
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
onPermissionGranted(permissions[i]); //开启GPS
}
}
}
break;
}
}
5、读取已配对蓝牙设备
/*
* 读取已经配对的蓝牙设备信息
* */
private void getPairedDevices(){
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
pairedDevicesdata.clear();
if (pairedDevices.size() > 0) {
for (BluetoothDevice device : pairedDevices) {
pairedDevicesdata.add(new LanYa_Item(device));
}
pairedDevicesYanAdapter.notifyDataSetChanged(); //读取到配对蓝牙信息,更新ListView
}
}
LanYa_Item为蓝牙类,用来存储蓝牙相关信息。
/*
* 蓝牙类,用来存储扫描到的蓝牙信息
* */
public class LanYa_Item {
private String tv_name_Str; //蓝牙名字
private String tv_address_Str; //蓝牙地址
private BluetoothDevice device; //蓝牙设备
public LanYa_Item(BluetoothDevice device) {
this.tv_name_Str = device.getName();
if(TextUtils.isEmpty(tv_name_Str))tv_name_Str = "未知设备";
this.tv_address_Str = device.getAddress();
this.device = device;
}
public String getTv_name_Str() {
return tv_name_Str;
}
public String getTv_address_Str() {
return tv_address_Str;
}
public BluetoothDevice getBluetoothDevice() {
return device;
}
}
读取到的已配对蓝牙信息需要一个列表来显示,这里提供一个ListView显示蓝牙信息,包括蓝牙名字和MAC地址。PairedDevicesYanAdapter为对应ListView的适配器。
//配对蓝牙列表
pairedDevicesdata = new ArrayList<>();
pairedDevicesYanAdapter = new PairedDevicesYanAdapter(pairedDevicesdata,Main2Activity.this);
pairedDevicesListView.setAdapter(pairedDevicesYanAdapter);
pairedDevicesListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
tv_name.setText(pairedDevicesdata.get(position).getTv_name_Str());
tv_address.setText(pairedDevicesdata.get(position).getTv_address_Str());
currentBluetoothDevice = pairedDevicesdata.get(position).getBluetoothDevice();
}
});
PairedDevicesYanAdapter类用来配置ListView.
public class PairedDevicesYanAdapter extends BaseAdapter {
//ListView自定义适配器
private List<LanYa_Item> list;
Context context;
public PairedDevicesYanAdapter(List<LanYa_Item> list, Context context) {
// TODO Auto-generated constructor stub
this.list = list;
this.context = context;
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return list.size();
}
@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return list.get(position);
}
@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
View view = null;
if (convertView == null) {
view = LayoutInflater.from(context).inflate(
R.layout.list_view_layout, null);
} else {
view = convertView;
}
TextView textView1 = (TextView) view
.findViewById(R.id.listViewTextView);
textView1.setText("蓝牙:" + list.get(position).getTv_name_Str() + " MAC:" + list.get(position).getTv_address_Str());
return view;
}
}
6、扫描附近蓝牙信息
在扫描附近蓝牙之前,我们需要注册几个广播接收器,以为系统会将扫描到的蓝牙信息通过发送广播的形式来广播蓝牙信息。
//通过广播的方式接收扫描结果
IntentFilter filter1 = new IntentFilter(android.bluetooth.BluetoothAdapter.ACTION_DISCOVERY_STARTED);
IntentFilter filter2 = new IntentFilter(android.bluetooth.BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
IntentFilter filter3 = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(scanBlueReceiver,filter1);
registerReceiver(scanBlueReceiver,filter2);
registerReceiver(scanBlueReceiver,filter3);
创建了一个广播接收器scanBlueReceiver,并且添加了三个适配器,分别对应三条系统广播:
开始扫描:ACTION_DISCOVERY_STARTED
结束扫描:ACTION_DISCOVERY_FINISHED
发现设备:ACTION_FOUND
广播接收器:
/**
*扫描广播接收类
* Created by zqf on 2018/7/6.
*/
public class ScanBlueReceiver extends BroadcastReceiver {
private static final String TAG = ScanBlueReceiver.class.getName();
private ScanBlueCallBack callBack;
public ScanBlueReceiver(ScanBlueCallBack callBack){
this.callBack = callBack;
}
//广播接收器,当远程蓝牙设备被发现时,回调函数onReceiver()会被执行
@Override
public void onReceive(Context context, Intent intent) {
// String action = intent.getAction();
// Log.d(TAG, "action:" + action);
// BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
String action = intent.getAction();
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
switch (action){
case BluetoothAdapter.ACTION_DISCOVERY_STARTED:
Log.d(TAG, "开始扫描...");
callBack.onScanStarted();
break;
case BluetoothAdapter.ACTION_DISCOVERY_FINISHED:
Log.d(TAG, "结束扫描...");
callBack.onScanFinished();
break;
case BluetoothDevice.ACTION_FOUND:
Log.d(TAG, "发现设备...");
callBack.onScanning(TAG);
// Toast.makeText(context,"广播接收器:" + device.getName() + device.getAddress(),Toast.LENGTH_SHORT).show();
break;
}
}
因为扫描广播是个耗时操作会阻塞线程,所以扫描过程是在异步线程中进行的。为了接收非UI线程反馈回来的蓝牙信息,我们创建了一个接口ScanBlueCallBack 用来接收异步线程的数据。
public interface ScanBlueCallBack {
//开始扫描
void onScanStarted();
//扫描结束
void onScanFinished();
//扫描中
void onScanning(BluetoothDevice device);
//配对请求
void onBondRequest();
//配对成功
void onBondSuccess(BluetoothDevice device);
//正在配对
void onBonding(BluetoothDevice device);
//配对失败
void onBondFail(BluetoothDevice device);
//连接成功
void onConnectSuccess(BluetoothDevice bluetoothDevice, BluetoothSocket bluetoothSocket);
//连接失败
void onConnectFail(BluetoothDevice bluetoothSocket);
//连接关闭
void onConnectClose();
//开始连接
void onStartConnect();
//开始读取数据
void onStarted();
//读取完成
void onFinished(boolean flag,String s);
}
为了接收到扫描线程反馈的扫描信息,我们需要在UI线程实现ScanBlueCallBack 接口,并且通过重写ScanBlueCallBack 的方法来获得信息。
public class Main2Activity extends AppCompatActivity implements View.OnClickListener,ScanBlueCallBack
重写接口ScanBlueCallBack 方法并且取得相关数据。
/*
* 开始扫描时回调该方法
* */
@Override
public void onScanStarted() {
}
/*
* 扫描结束回调该方法
* */
@Override
public void onScanFinished() {
}
/*
* 扫描发现蓝牙设备时,会回调该方法
* */
@Override
public void onScanning(BluetoothDevice device) {
data.add(new LanYa_Item(device));
lv_devicesListAdapter.notifyDataSetChanged(); //通知蓝牙列表LsitView更新数据
}
为了显示附近蓝牙的信息,我们再次创建了一个ListView,并且配置了一个适配器。
//扫描蓝牙列表
data = new ArrayList<>();
lv_devicesListAdapter = new ListViewLanYanAdapter(data,Main2Activity.this);
lv_devices.setAdapter(lv_devicesListAdapter);
lv_devices.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//每当用户选中某项的时候,将该栏蓝牙名字和地址显示到tv_name、tv_address
tv_name.setText(data.get(position).getTv_name_Str());
tv_address.setText(data.get(position).getTv_address_Str());
currentBluetoothDevice = data.get(position).getBluetoothDevice();
}
});
适配器类:
public class ListViewLanYanAdapter extends BaseAdapter {
//ListView自定义适配器
private List<LanYa_Item> list;
Context context;
public ListViewLanYanAdapter(List<LanYa_Item> list, Context context) {
// TODO Auto-generated constructor stub
this.list = list;
this.context = context;
}
@Override
public int getCount() {
// TODO Auto-generated method stub
return list.size();
}
@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return list.get(position);
}
@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
View view = null;
if (convertView == null) {
view = LayoutInflater.from(context).inflate(
R.layout.list_view_layout, null);
} else {
view = convertView;
}
TextView textView1 = (TextView) view
.findViewById(R.id.listViewTextView);
textView1.setText("蓝牙:" + list.get(position).getTv_name_Str() + " MAC:" + list.get(position).getTv_address_Str());
return view;
}
}
选中ListView的信息,对用蓝牙类的信息就会被读取并且存储,用来进行下一步的操作。
7、蓝牙配对与解除配对
在对指定蓝牙进行配对或者解除配对操作之前,我们需要再创建一个广播接收器,用来接收配对或者解除是否成功等系统广播信息。
//通过广播的方式接收配对结果
IntentFilter filter4 = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
IntentFilter filter5 = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
registerReceiver(pinBlueReceiver,filter4);
registerReceiver(pinBlueReceiver,filter5);
public class PinBlueReceiver extends BroadcastReceiver {
private String pin = "0000"; //此处为你要连接的蓝牙设备的初始密钥,一般为1234或0000
private static final String TAG = PinBlueReceiver.class.getName();
private ScanBlueCallBack callBack;
public PinBlueReceiver(ScanBlueCallBack callBack){
this.callBack = callBack;
}
//广播接收器
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.d(TAG, "action:" + action);
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if (BluetoothDevice.ACTION_PAIRING_REQUEST.equals(action)){
try {
callBack.onBondRequest();
//1.确认配对
Method setPairingConfirmation = device.getClass().getDeclaredMethod("setPairingConfirmation",boolean.class);
setPairingConfirmation.invoke(device,true);
//2.终止有序广播
Log.d("order...", "isOrderedBroadcast:"+isOrderedBroadcast()+",isInitialStickyBroadcast:"+isInitialStickyBroadcast());
abortBroadcast();
//3.调用setPin方法进行配对...
Method removeBondMethod = device.getClass().getDeclaredMethod("setPin", new Class[]{byte[].class});
Boolean returnValue = (Boolean) removeBondMethod.invoke(device, new Object[]{pin.getBytes()});
} catch (Exception e) {
e.printStackTrace();
}
}else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)){
switch (device.getBondState()) {
case BluetoothDevice.BOND_NONE:
Log.d(TAG, "取消配对");
callBack.onBondFail(device);
break;
case BluetoothDevice.BOND_BONDING:
Log.d(TAG, "配对中");
callBack.onBonding(device);
break;
case BluetoothDevice.BOND_BONDED:
Log.d(TAG, "配对成功");
callBack.onBondSuccess(device);
break;
}
}
}
}
通过按键来触发配对设备或者解除配对设备。
case R.id.bt_bound:{ //向指定蓝牙设备发出配对请求
if((currentBluetoothDevice != null) && isBlueEnable())pin(currentBluetoothDevice);
}break;
case R.id.bt_disBound:{ //取消指定设备配对
if((currentBluetoothDevice != null) && isBlueEnable())cancelPinBule(currentBluetoothDevice);
}break;
8、连接蓝牙与断开连接
连接蓝牙:
调用connect方法启动蓝牙连接线程。
if((currentBluetoothDevice != null) && isBlueEnable()){
dialog = ProgressDialog.show(Main2Activity.this,"蓝牙"+currentBluetoothDevice.getName(),"连接中,请稍后…",true,true);
connect(currentBluetoothDevice,Main2Activity.this);
}
连接蓝牙是一个耗时操作,在子线程中进行,为了更好的用户体验在连接的过程中弹出一个等待对话框,并且在蓝牙连接成功之后系统广播回调接口中关闭这个对话框,这时候表示蓝牙已经连接成功。
connect方法:
/**
* 连接 (在配对之后调用)
* @param device
*/
public void connect(BluetoothDevice device, ScanBlueCallBack callBack){
if (device == null){
Log.d(TAG, "bond device null");
return;
}
if (!isBlueEnable()){
Log.e(TAG, "Bluetooth not enable!");
return;
}
//连接之前把扫描关闭
if (mBluetoothAdapter.isDiscovering()){
mBluetoothAdapter.cancelDiscovery();
}
new ConnectBlueTask(callBack).execute(device);
}
connect方法中启动了一个线程ConnectBlueTask,在该线程中对蓝牙进行连接:
public class ConnectBlueTask extends AsyncTask<BluetoothDevice, Integer, BluetoothSocket> {
private static final String TAG = ConnectBlueTask.class.getName();
//00001101-0000-1000-8000-00805F9B34FB
public static final String MY_BLUETOOTH_UUID = "00001101-0000-1000-8000-00805F9B34FB"; //蓝牙通讯
private BluetoothDevice bluetoothDevice;
private ScanBlueCallBack callBack;
// private Object ClassicsBluetooth;
public ConnectBlueTask(ScanBlueCallBack callBack){
this.callBack = callBack;
}
@Override
protected BluetoothSocket doInBackground(BluetoothDevice... bluetoothDevices) {
bluetoothDevice = bluetoothDevices[1];
BluetoothSocket socket = null;
try{
Log.d(TAG,"开始连接socket,uuid:" + MY_BLUETOOTH_UUID);
socket = bluetoothDevice.createRfcommSocketToServiceRecord(UUID.fromString(MY_BLUETOOTH_UUID));
if (socket != null && !socket.isConnected()){
socket.connect();
}
}catch (IOException e){
Log.e(TAG,"socket连接失败");
try {
socket.close();
} catch (IOException e1) {
e1.printStackTrace();
Log.e(TAG,"socket关闭失败");
}
}
return socket;
}
@Override
protected void onPreExecute() {
Log.d(TAG,"开始连接");
if (callBack != null) callBack.onStartConnect();
}
@Override
protected void onPostExecute(BluetoothSocket bluetoothSocket) {
if (bluetoothSocket != null && bluetoothSocket.isConnected()){
Log.d(TAG,"连接成功");
if (callBack != null) callBack.onConnectSuccess(bluetoothDevice, bluetoothSocket);
}else {
Log.d(TAG,"连接失败");
if (callBack != null) callBack.onConnectFail(bluetoothDevice);
}
}
连接的结果还是通过接口回调的方法反馈给主线程。
private BluetoothDevice currentBluetoothDevice = null; //当前用户选中的蓝牙设备,接下来可能会就行其它操作
//蓝牙连接成功
@Override
public void onConnectSuccess(BluetoothDevice bluetoothDevice, BluetoothSocket bluetoothSocket) {
dialog.dismiss(); //连接成功,关闭对话框
connectBluetooth = bluetoothDevice; //取得已经连接蓝牙
mBluetoothSocket = bluetoothSocket;
}
连接蓝牙成功后,我们会得到一个BluetoothSocket bluetoothSocket,通过BluetoothSocket 我们能够对蓝牙进行数据接收和发送操作。
9、通过蓝牙接收发送数据
由于发送接收数据是线程阻塞操作,所以在发送或者接收数据的时候我们都需要在子线程中进行。
接收数据异步线程:
public class ReadTask extends AsyncTask<String, Integer, String> {
private static final String TAG = ReadTask.class.getName();
private ScanBlueCallBack callBack;
private BluetoothSocket socket;
public ReadTask(ScanBlueCallBack callBack, BluetoothSocket socket){
this.callBack = callBack;
this.socket = socket;
}
@Override
protected String doInBackground(String... strings) {
BufferedInputStream in = null;
try {
StringBuffer sb = new StringBuffer();
in = new BufferedInputStream(socket.getInputStream());
int length = 0;
byte[] buf = new byte[1024];
while ((length = in.read()) != -1) {
sb.append(new String(buf,0,length));
}
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
// finally {
// try {
// in.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }
return "读取失败";
}
@Override
protected void onPreExecute() {
Log.d(TAG,"开始读取数据");
if (callBack != null) callBack.onStarted();
}
@Override
protected void onPostExecute(String s) {
Log.d(TAG,"完成读取数据");
if (callBack != null){
if ("读取失败".equals(s)){
callBack.onFinished(false, s);
}else {
callBack.onFinished(true, s);
}
}
}
}
发送数据异步线程:
public class WriteTask extends AsyncTask<String, Integer, String>{
private static final String TAG = WriteTask.class.getName();
private BluetoothSocket socket;
public WriteTask( BluetoothSocket socket){
this.socket = socket;
}
@Override
protected String doInBackground(String... strings) {
String string = strings[0];
OutputStream outputStream = null;
try{
outputStream = socket.getOutputStream();
outputStream.write(string.getBytes());
} catch (IOException e) {
Log.e("error", "ON RESUME: Exception during write.", e);
return "发送失败";
}
return "发送成功";
}
public void cancel() {
try {
socket.close();
} catch (IOException e) {
Log.e(TAG, "Could not close the connect socket", e);
}
}
}
发送数据
writeTask = new WriteTask(mBluetoothSocket);
writeTask.doInBackground(""+1);
接收数据
readTask= new WriteTask(mBluetoothSocket);
readTask.doInBackground("");
数据接收发送通过接口回调来反馈。
STM32循迹小车/Android蓝牙控制小车(一)
STM32循迹小车/Android蓝牙控制小车(二)
STM32循迹小车/Android蓝牙控制小车(三)
STM32循迹小车/Android蓝牙控制小车(四)完结篇——Android经典蓝牙开发