Android 串列埠藍芽通訊開發Java版本
Android串列埠BLE藍芽通訊Java版
0. 導語
我們都知道,在物聯網中,BLE藍芽是通訊裝置的關鍵裝置。在傳統的物聯網應用中,無線WIFI、藍芽和Zigbee,還有一些其他的2.4GHz的無線網絡卡等。對於一個面向物聯網的嵌入式工程師來講,不單單是要學會底層硬體嵌入式或者是嵌入式Linux驅動的開發,還要掌握上層應用,以理解開發流程,方便未來和軟體應用工程師協作開發。“也只有穿別人的鞋子,才知道別人的感覺”似乎就是這個道理吧。
16年的時候,我嘗試做過Qt版本的Android,使用的是C++語言,搭建完環境之後開發出了基於Qt的藍芽通訊demo。雖然Qt在不斷的完善對於Android的支援,但終究C++向Java轉變的時候,效率是一方面的問題,庫同樣是一方面的問題。Qt的定位也只能是臨時應付,或者說應用內的C++演算法繁瑣的時候,選擇C++開發Android才是一個上上策。
對於面向物聯網的嵌入式工程師,對於Android不需要掌握太深刻,重頭戲不在這裡,但是必須要有了解,也要會開發,我猜Android工程師的精髓在於業務邏輯處理上,而對於我們嵌入式工程師來講,我們只在乎功能的實現,也只是Android工程師掌握的一個邊角而已。後繼,我們還會開發基於Java Android的Socket程式設計和伺服器通訊,還有Wifi程式設計。
本文的功能和Qt on Android 藍芽通訊開發一樣,只是使用Java進行重寫。也不得不說,Java的UI比Qt好多了,(在沒有專業的UI製作下),在物聯網通訊中BLE藍芽適合傳輸一些控制命令,或者一些家居資料包括溫度溼度等等。
1. 藍芽通訊組成
我們將使用藍芽功能按順序分成三個部分。第一個部分,藍芽的控制,對於藍芽的開啟和關閉;第二部分,對於藍芽的搜尋,並且可以增加到UI列表裡面;第三部分,藍芽資料傳輸和通訊,可以傳送資料,可以接收資料。如圖所示,為整個藍芽demo區域,實現了最基本的功能,同Qt on Android藍芽通訊開發一樣,有藍芽列表顯示+接收和傳送和藍芽控制的基本功能。下一步就進行藍芽通訊的開發介紹。
1.1 藍芽開發架構
如圖所示,為藍芽Activity的介面,裡面包含了,TextView,ListView,LineEdit,和一些按鈕,使用的是相對佈局,為該藍芽開發提供最基本的功能控制。
XML佈局程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/tv_bluelist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:text="Bluetooth List:"
android:textColor="#000"
android:textSize="15dp"
/>
<ListView
android:id="@+id/lv_bluelist"
android:layout_width="match_parent"
android:layout_height="160dp"
android:paddingTop="1dp"
android:layout_marginTop="5dp">
</ListView>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Recv display:"
android:textColor="#000"
android:textSize="15dp"
android:layout_marginTop="5dp"
/>
<TextView
android:id="@+id/tv_recv"
android:layout_width="match_parent"
android:layout_height="160dp"
android:layout_marginTop="5dp"
android:textColor="#000"
/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Send:"
android:textColor="#000"
android:textSize="15dp"
android:layout_marginTop="5dp"
/>
<EditText
android:id="@+id/et_send"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="5dp"
android:textColor="#000"
android:textSize="12dp"
/>
<TableLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
>
<TableRow
android:layout_width="match_parent"
android:layout_marginHorizontal="10dp"
>
<Button
android:id="@+id/btn_send"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:text="Send" />
<Button
android:id="@+id/btn_disconnect"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:text="Disconnect" />
<Button
android:id="@+id/btn_scan"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:text="Scan Device" />
</TableRow>
<TableRow
android:layout_width="match_parent"
android:layout_marginHorizontal="10dp"
>
<Button
android:id="@+id/btn_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Clear" />
<Button
android:id="@+id/btn_open"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Open" />
<Button
android:id="@+id/btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Close" />
</TableRow>
</TableLayout>
</LinearLayout>
</RelativeLayout>
值得注意的是,按鈕使用的是TableLayout進行佈局,裡面請注意觀察TableRow對按鈕進行劃分。
1.2 許可權申請
在Android 6.0 以前,申請藍芽的控制權限只需在mainfest.xml中加入permission專案即可,但是Android 6.0系統需要動態申請許可權,而且使用藍芽搜尋需要申請位置許可權,否則無法申請藍芽的許可權。
- AndroidManifest.xml檔案中
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
分別是藍芽許可權和定位許可權。
- 在藍芽的activity oncreate中動態申請定位許可權
/*
* GPS COARSE LOCATION permission checked.
*
* */
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
MY_PERMISSIONS_REQUEST_ACCESS_COARSE_LOCATION);
if(ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.READ_CONTACTS)) {
Toast.makeText(this, "shouldShowRequestPermissionRationale", Toast.LENGTH_SHORT).show();
}
}
在執行藍芽的activity中會執行許可權掃描,確認使用者是否具備定位申請的許可權。
2 藍芽操作
2.1 藍芽的開啟和關閉
在demo中提供了藍芽開啟和關閉的功能,通過控制類來完成對於藍芽的開啟關閉。
import android.bluetooth.BluetoothAdapter;
BluetoothAdapter類就是對藍芽實行開啟和控制的類,包括開啟藍芽,關閉藍芽,檢視藍芽開啟關閉狀態。如下程式碼為開機對於藍芽裝置的開啟和關閉檢測。
/*
* Check bluetooth state.
* */
BluetoothAdapter mBluetoothAdapter = new BluetoothAdapter();
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if( mBluetoothAdapter == null ) {
Toast.makeText(this, "Bluetooth is not available.", Toast.LENGTH_LONG).show();
finish();
return;
}
if( !mBluetoothAdapter.isEnabled() ) {
//Toast.makeText(this, "Please enable your Bluetooth and re-run this program.", Toast.LENGTH_LONG).show();
mBtnOpen.setEnabled(true);
mBtnClose.setEnabled(false);
System.out.println("is enable");
}else{
mBtnOpen.setEnabled(false);
mBtnClose.setEnabled(true);
System.out.println("is not enable");
}
藍芽的開啟:mBluetoothAdapter.enable();
藍芽的關閉:mBluetoothAdapter.disable();
把他們相應的放在按鈕的點選事件即可。
2.2 藍芽的搜尋
藍芽搜尋的功能需要使用安卓的廣播功能在UI上還要建立一個ListView將搜尋到的周邊藍芽裝置加入到裝置列表,在demo中就是這樣實現的,還能通過ListView的點選事件和該藍芽裝置進行配對連線。
2.2.1 建立Listview列表
新建一個Activity類:
public class BluetoothDeviceAdapter extends BaseAdapter
專門來處理搜尋到的裝置然後加入到Listview列表裡面。
package com.mltbns.root.delvisapp;
import android.app.Activity;
import android.bluetooth.BluetoothDevice;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import java.util.ArrayList;
import android.app.Activity;
import android.bluetooth.BluetoothDevice;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
public class BluetoothDeviceAdapter extends BaseAdapter {
private ArrayList<BluetoothDevice> mLeDevices;
//LayoutInflater是用來找res/layout/下的xml佈局檔案,並且例項化
//它的作用類似於findViewById()
private LayoutInflater mInflator;
private Activity mContext;//獲得 LayoutInflater 例項的一種方法就是使用Activity;
public BluetoothDeviceAdapter(Activity c) {
super();
mContext = c;
mLeDevices = new ArrayList<BluetoothDevice>();
mInflator = mContext.getLayoutInflater();
}
public void addDevice(BluetoothDevice device) {
if (!mLeDevices.contains(device)) {
mLeDevices.add(device);
System.out.println(device.getName() + " " + device.getAddress());
}
}
// 獲取子項中對應的裝置
public BluetoothDevice getDevice(int position) {
return mLeDevices.get(position);
}
// 清空列表的資料
public void clear() {
mLeDevices.clear();
}
@Override
public int getCount() {
return mLeDevices.size();
}
@Override
public Object getItem(int position) {
return mLeDevices.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View view, ViewGroup parent) {
ViewHolder viewHolder;
// General ListView optimization code.
if (view == null) {
view = mInflator.inflate(R.layout.activity_bluetooth_device_adapter, null);//例項化這個控制元件
viewHolder = new ViewHolder();
viewHolder.deviceAddress = (TextView) view.findViewById(R.id.Address);
viewHolder.deviceName = (TextView) view.findViewById(R.id.Name);
view.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) view.getTag();
//the Object stored in this view as a tag
}
// 對應的裝置進行處理
BluetoothDevice device = mLeDevices.get(position);
final String deviceName = device.getName();
if (deviceName != null && deviceName.length() > 0) {
viewHolder.deviceName.setText(deviceName);
} else {
viewHolder.deviceName.setText("未知裝置");
}
if( device.getBondState() == BluetoothDevice.BOND_BONDED ) {
viewHolder.deviceName.setTextColor(Color.rgb(75,0,130));
}else{
viewHolder.deviceName.setTextColor(Color.rgb(47,79,79));
}
viewHolder.deviceAddress.setText(device.getAddress());
return view;
}
final class ViewHolder {
TextView deviceName;
TextView deviceAddress;
}
}
2.2.2 建立BluetoothReceiver
public class BluetoothReceiver extends BroadcastReceiver {}
該類整合BroadcastReceiver類,裡面有個onReceive的事件,當搜尋到藍芽的時候,就會將該藍芽的裝置資訊傳送進來。
public class BluetoothReceiver extends BroadcastReceiver {
private String pair_info;
private String unpair_info;
private String state_info;
@Override
public void onReceive(Context context, Intent intent ) {
String action = intent.getAction();
System.out.println ( "SYSTEM: action triggered: " + action );
if(BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra( BluetoothDevice.EXTRA_DEVICE );
mLvDeviceList.setAdapter(mBluetoothDeviceAdapter);
System.out.println ( "SYSTEM: Find a device : " + device.getName() + " : " + device.getAddress() );
// Scanned a device add to List
mBluetoothDeviceAdapter.addDevice(device);
// 資料改變並更新列表
mBluetoothDeviceAdapter.notifyDataSetChanged();
if( device.getBondState() == BluetoothDevice.BOND_BONDED ) {
pair_info = device.getAddress();
}else {
unpair_info = device.getAddress();
}
}else if(BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)){
System.out.println ( "SYSTEM: Discovery finished..." );
}
}
public void set_pairInfo( String val ) {
pair_info = val;
}
public String get_pairInfo() {
return pair_info;
}
public void set_unpairInfo( String val ) {
unpair_info = val;
}
public String get_unpairInfo() {
return unpair_info;
}
public void set_stateInfo( String val ) {
state_info = val;
}
public String get_stateInfo() {
return state_info;
}
}
當搜尋到了資訊之後就將該裝置傳入BluetoothDeviceAdapter中,然後該裝置的資訊都顯示。搜尋到的資訊調入這個方法mBluetoothDeviceAdapter.addDevice(device);
就完成了裝置的新增,然後重新整理顯示:mBluetoothDeviceAdapter.notifyDataSetChanged();
這樣就完成了最終的顯示。
使用mBluetoothAdapter.startDiscovery();
開始對藍芽進行查詢。
如圖所示,上面顯示的是藍芽的名字,下面顯示的MAC地址,對於已經配對的裝置可以用if( device.getBondState() == BluetoothDevice.BOND_BONDED )
來進行判斷,然後設定已配對藍芽的名字為更鮮豔的顏色區分未配對裝置。
2.3 連線裝置
本demo在listview增加點選事件,當點選裝置的時候激發對於該device的連線。這個連線的過程我們需要新建立一個執行緒進行連線,連線成功之後使用回撥函式的方法通知主執行緒已經連線了,可以進行操作了。在主執行緒(我們的藍芽主activity中新建handler函式專門處理其他執行緒回撥資訊的)
2.3.1 constant常量類
相當於C++的#include 裡面定義一些通訊協議的常量,我們在主執行緒和一會兒建立的搜尋執行緒中會使用該資訊。
public class Constant {
public static final String CONNECTTION_UUID = "00001101-0000-1000-8000-00805F9B34FB";
/**
* 開始監聽
*/
public static final int MSG_START_LISTENING = 1;
/**
* 結束監聽
*/
public static final int MSG_FINISH_LISTENING = 2;
/**
* 有客戶端連線
*/
public static final int MSG_GOT_A_CLINET = 3;
/**
* 連線到伺服器
*/
public static final int MSG_CONNECTED_TO_SERVER = 4;
/**
* 獲取到資料
*/
public static final int MSG_GOT_DATA = 5;
/**
* 出錯
*/
public static final int MSG_ERROR = -1;
}
2.3.2 connect執行緒
package com.mltbns.root.delvisapp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.os.Handler;
import java.io.IOException;
import java.util.UUID;
/**
* 客戶端連線執行緒
*/
public class ConnectThread extends Thread {
private static final UUID MY_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
private BluetoothAdapter mBluetoothAdapter;
private final Handler mHandler;
private ConnectedThread mConnectedThread;
public ConnectThread(BluetoothDevice device, BluetoothAdapter adapter, Handler handler) {
BluetoothSocket tmp = null;
mmDevice = device;
mBluetoothAdapter = adapter;
mHandler = handler;
//根據給定的裝置獲取一個BluetoothSocket物件
try {
// 和伺服器端使用相同的UUID
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) { }
mmSocket = tmp;
}
public void run() {
// 取消搜尋裝置因為會關閉連線
System.out.println(" Connect +: "+ mmDevice.getName());
mBluetoothAdapter.cancelDiscovery();
try {
// Connect the device through the socket. This will block
// until it succeeds or throws an exception
mmSocket.connect();
System.out.println("Connecting.... ");
} catch (Exception connectException) {
mHandler.sendMessage(mHandler.obtainMessage(Constant.MSG_ERROR, connectException));
// Unable to connect; close the socket and get out
try {
mmSocket.close();
} catch (IOException closeException) { }
return;
}
// Do work to manage the connection (in a separate thread)
manageConnectedSocket(mmSocket);
}
private void manageConnectedSocket(BluetoothSocket mmSocket) {
mHandler.sendEmptyMessage(Constant.MSG_CONNECTED_TO_SERVER);
mConnectedThread = new ConnectedThread(mmSocket, mHandler);
mConnectedThread.start();
System.out.println("Connected thread start... ");
}
/** Will cancel an in-progress connection, and close the socket */
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
public void sendData(byte[] data) {
if( mConnectedThread!=null){
mConnectedThread.write(data);
}
}
}
我們在對listview裡面有點選事件的函式,點選的專案索引編號和device的資訊進行了繫結,然後進行了連線。通過該執行緒就完成了對於藍芽裝置的連線。
2.4 藍芽資料傳輸
2.4.1 connected執行緒
連線完成之後,進入的執行緒,主要是進行收發訊息的。
package com.mltbns.root.delvisapp;
import android.bluetooth.BluetoothSocket;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Created by Rex on 2015/5/30.
*/
public class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
private final Handler mHandler;
public ConnectedThread(BluetoothSocket socket, Handler handler) {
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
mHandler = handler;
// 獲取輸入輸出流
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) { }
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
byte[] buffer = new byte[1024]; // buffer store for the stream
int bytes;
// Keep listening to the InputStream until an exception occurs
while (true) {
try {
// 從輸入流讀取資料
bytes = mmInStream.read(buffer);
// Send the obtained bytes to the UI activity
if( bytes >0) {
Message message = mHandler.obtainMessage(Constant.MSG_GOT_DATA, new String(buffer, 0, bytes, "utf-8"));
mHandler.sendMessage(message);
}
Log.d("GOTMSG", "message size" + bytes);
} catch (IOException e) {
mHandler.sendMessage(mHandler.obtainMessage(Constant.MSG_ERROR, e));
break;
}
}
}
/* 傳送資料島遠端裝置*/
public void write(byte[] bytes) {
try {
mmOutStream.write(bytes);
} catch (IOException e) { }
}
/* 關閉連線 */
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}
2.4.2 主執行緒的handler
private void showToast(String text) {
if( mToast == null) {
mToast = Toast.makeText(this, text, Toast.LENGTH_LONG);
}
else {
mToast.setText(text);
}
mToast.show();
}
/**
* 處理訊息
*/
private class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case Constant.MSG_START_LISTENING:
setProgressBarIndeterminateVisibility(true);
System.out.println("Start to listener...");
break;
case Constant.MSG_FINISH_LISTENING:
setProgressBarIndeterminateVisibility(false);
System.out.println("stop listenner");
break;
case Constant.MSG_GOT_DATA:
mTextView.append(String.valueOf(msg.obj));
System.out.println("data: "+String.valueOf(msg.obj));
break;
case Constant.MSG_ERROR:
System.out.println("error: "+String.valueOf(msg.obj));
break;
case Constant.MSG_CONNECTED_TO_SERVER:
System.out.println("Connected to Server");
mLvDeviceList.setEnabled(false);
mLvDeviceList.setBackgroundColor(Color.rgb(119,136,153));
showToast("Bluetooth connection has been set up!");
break;
case Constant.MSG_GOT_A_CLINET:
System.out.println("Got a Client");
break;
}
}
}
在開始搜尋裝置執行緒的時候我們將handler也傳遞給了執行緒,執行緒在執行完搜尋完成之後就會進入handler的函式按照回撥資訊進入case裡面,輸出相關資訊。
這裡有收到資訊,資訊錯誤,連線成功的事件,我們在相應的事件裡面進行進行執行函式就好了。
2.4.3 函式的傳送
String text = mEditText.getText().toString();
connectThread.sendData( text.getBytes() );
我們獲取editText元件的字串,然後用connectThread的send函式就能將資料傳送出去。
2.4.4 函式的接收
mTextView.append(String.valueOf(msg.obj));
System.out.println("data: "+String.valueOf(msg.obj));
該函式在handler函式體內,是case Constant.MSG_GOT_DATA:
裡面的。
3 原始碼下載
4 參考文獻
[2] qq_22252423, Android串列埠藍芽開發實戰
, csdn, 2017年07月18日
[5] Small_Lee, 藍芽實戰(三)
, csdn, 2016年3月15日