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經典藍牙開發