安卓低功耗藍芽開發
近幾日做了些安卓低功耗藍芽的專案,主要是用了北歐半導體公司的板子。不過對於安卓上位機來說,是哪家公司的板子,差別並不是很大。
剛開始對藍芽不是很瞭解,找了NordicSemiconductor的Android-nRF-Toolbox和谷歌自己的sample 的程式碼研究了一番。
Nordic的程式碼較為龐大,內容也很豐富,包括了dfu升級服務,體溫服務,串列埠服務等等。而谷歌的相對要簡單些,僅僅讓使用者能夠看出藍芽發上來的資料,對於心率的服務還做了解析。兩者的共同點是將大部分的藍芽操作置於Service之中,而acticivty只需要繫結服務,並且註冊相應的廣播來接收各種資訊即可。
因此,我對藍芽的操作進行了一些小小的總結。只需要基於以下幾個基本功能,就可以完成一個簡單的藍芽操作過程。
一、藍芽的初始化
1.判斷安卓裝置是否支援ble
如果安卓裝置不支援ble的話,基本就沒得玩了。不過現在的安卓手機普遍都支援,按照官方文件的說法,如圖。
只要是安卓4.3以上,即api18以上的裝置基本都支援ble。但是不排除有些安卓裝置是從4.3以下升級上來的,這些裝置可能就不能使用低功耗藍芽了,但是這種情況還是比較少的。儘管如此,我們還是可以用
public boolean isBLESupported() {
boolean flag = true;
if(!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
flag = false ;
}
return flag;
}
來判斷以下。
2.獲取本地藍芽介面卡
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = mBluetoothManager.getAdapter();
只有在獲取本地藍芽介面卡之後才能進行相應的藍芽操作。
3.判斷藍芽是否已經開啟
儘管這不是必須的,但是有時候可能會使用到。
private boolean isBLEEnabled () {
return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled();
}
4.開啟藍芽
開啟藍芽有兩種方式,一種是需要通過使用者同意才打開,另一種是不經過使用者,直接開啟。這兩種方式各有利弊。要根據情況來稍作判斷。
public void enableBle(boolean bShowDialog) {
if (isBLESupported()){
if (bShowDialog){
//這種是需要彈出對話方塊讓使用者選擇是否開啟的
final Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
mContext.startActivity(enableIntent);
}else{
//這種是不經過使用者,直接開啟
mBluetoothAdapter.enable();
}
}else {
Log.v(TAG,"ble is not support");
}
}
5.關閉藍芽
mBluetoothAdapter.disable();
關閉的程式碼較為簡單。
二、掃描裝置
掃描裝置是相當耗能的,通常情況下,在掃描裝置的時候不會進行其他有關藍芽的操作。按照官方文件,我們掃描的時候還應當設定一個掃描時間,免得手機一直再掃描,消耗電量。
掃描裝置又有幾種方式,不同安卓系統之間的函式又有不同,5.0以上(含)的系統和5.0以下的系統,用的是不同的掃描函式,但是本質上差的不是很多,這裡只提供5.0以下的掃描函式,因為,即使是5.0以上的系統也還可以用5.0以下的函式進行掃描。
1.準備一個掃描回撥
為什麼要弄一個這種東西呢?這裡涉及到了java裡的一種回撥機制,簡單的說
比方說:有一天,我去麵包店買麵包,可是店員告訴我麵包賣完了,我留了個電話給他,讓他有面包的時候打電話通知我,我再過來買。這樣做的好處是,我可以先去幹別的事情而不用為了這點麵包苦苦等待。
運用到藍芽中就是,我開啟了掃描,在掃描的這段時間裡我可以先處理一些和藍芽有關或無關的事情。不會讓掃描這件事一直阻塞著我的程序。
private BluetoothAdapter.LeScanCallback mLeScanCallback =
new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
//在這裡可以處理掃描到裝置的時候想要做的事情
//你可以傳送一個掃描到裝置的廣播,或者發個訊息,等等
}
};
2.掃描裝置
掃描呢,就得注意一個東西,就是掃描時間,我們可以弄一個延時函式Handler類的postDelayed並用上Runnable就可以達到延時效果,開啟掃描後延時一定的毫秒數停止掃描。
另外呢,掃描的時候可以指定相應的服務的uuid進行掃描,也可以不管三七二十一,只要是低功耗藍芽裝置都給掃了。只需要呼叫不同的startLeScan就可以。
public void scanLeDevice(UUID[] serviceUuids, long scanPeriod){
// Stops scanning after a pre-defined scan period.
if (isBLEEnabled()){
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mScanning = false;
mBluetoothAdapter.stopLeScan(mLeScanCallback);
}
}, scanPeriod);//scanPeriod是毫秒,也就是1秒 = 1000
//mBluetoothAdapter.stopLeScan(mLeScanCallback);若呼叫這條函式,即可掃描全部ble裝置
mBluetoothAdapter.startLeScan(serviceUuids, mLeScanCallback);
//這裡我傳送了個開始掃描的廣播,可發可不發
broadcastUpdate(BleBroadcastAction.ACTION_DEVICE_DISCOVERING);
}
}
3.停止掃描
停止掃描也比較簡單
mBluetoothAdapter.stopLeScan(mLeScanCallback);
三、連線裝置
- 準備一個BluetoothGattCallback——一個基於gatt服務的回撥
有是一個回撥,java中因為沒有函式指標,所以回撥通常會藉助介面或者抽象類來完成。這個回撥包含了幾個功能:
1. 接收手機與ble裝置連線狀態改變的訊號,對應函式onConnectionStateChange(BluetoothGatt gatt, int status,
int newState)
2. 發現所連線的裝置具有的服務。對應函式onServicesDiscovered(BluetoothGatt gatt, int status)
3. 讀取所連線服務對應的資料,對應函式onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status)
4. 得到資料改變的通知,並獲取改變的結果,對應函式onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic)
注意:這裡的涉及到一個藍芽連線後的順序問題,必須要先連線裝置,才能發現裝置相應的服務。發現服務後,必須要先連線服務,才能獲取服務對應的資料,和傳送給服務對應的資料。
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
String intentAction;
if (newState == BluetoothProfile.STATE_CONNECTED) {
intentAction = BleBroadcastAction.ACTION_GATT_CONNECTED;
mConnectionState = STATE_CONNECTED;
broadcastUpdate(intentAction);
Log.i(TAG, "Connected to GATT server.");
// Attempts to discover services after successful connection.
Log.i(TAG, "Attempting to start service discovery:" +
mBluetoothGatt.discoverServices());
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
intentAction = BleBroadcastAction.ACTION_GATT_DISCONNECTED;
mConnectionState = STATE_DISCONNECTED;
Log.i(TAG, "Disconnected from GATT server.");
broadcastUpdate(intentAction);
close();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(BleBroadcastAction.ACTION_GATT_SERVICES_DISCOVERED);
connectCharacteristic(mCharacteristicUuidArr);
} else {
Log.w(TAG, "onServicesDiscovered received: " + status);
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
broadcastUpdate(characteristic.getUuid().toString(), characteristic);
}
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic) {
broadcastUpdate(characteristic.getUuid().toString(), characteristic);
}
};
這上面是谷歌的程式碼,我只做了略微修改。總的來說,只要呼叫了哪個回撥函式都可以發個廣播或者訊息,讓其他activity或者service去處理。
2.連線
連線ble裝置,第一次連線是比較簡單的,呼叫
final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);//address是mac字串
mBluetoothGatt = device.connectGatt(mContext, false, mGattCallback);
即可開始連線。
但是這裡又涉及到一個比較隱晦坑爹的點,就再次連線同一個裝置的情況。我一開始在做這個部分的時候沒有注意到連線同一個裝置的情況。結果導致我待會斷開連線之後,裝置還是一直保持通訊。原因是安卓手機無法識別他們是同一個裝置,建立了新的連線,把裝置當做新裝置連線來處理。我後來斷開的裝置只是斷掉了安卓認為的新裝置。
這裡呢,可以先判斷是否連線過,如果連線過,可以呼叫connect();方法重新連線。
if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress)
&& mBluetoothGatt != null) {
Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection.");
if (mBluetoothGatt.connect()) {
mConnectionState = STATE_CONNECTING;
return true;
} else {
return false;
}
}
但是,這麼做呢,經過試驗是有些缺陷的,連線時間會大大拉長。可以採取nordic的做法。
if (mConnected)
return;
if (mBluetoothGatt != null) {
Logger.d(mLogSession, "gatt.close()");
mBluetoothGatt.close();
mBluetoothGatt = null;
}
final boolean autoConnect = shouldAutoConnect();
mUserDisconnected = !autoConnect; // We will receive Linkloss events only when the device is connected with autoConnect=true
Logger.v(mLogSession, "Connecting...");
Logger.d(mLogSession, "gatt = device.connectGatt(autoConnect = " + autoConnect + ")");
mBluetoothGatt = device.connectGatt(mContext, autoConnect, getGattCallback());
這樣做,可以加快連線速度。
3.獲取服務
連線完成後,需呼叫BluetoothGatt的discoverServices()函式去查詢當前連線的裝置所具有的服務,當安卓裝置發現所連線的裝置具有服務時,會呼叫我們剛才重寫的回撥函式onServicesDiscovered(BluetoothGatt gatt, int status)。我們便可呼叫mBluetoothGatt.getServices()去獲取裝置擁有的服務。並進行操作,獲取他們的characteristic。
4.斷開連線
有連線,則有斷開連線。
public void disconnect() {
if (mBluetoothAdapter == null || mBluetoothGatt == null) {
Log.w(TAG, "BluetoothAdapter not initialized");
return;
}
mConnectionState = STATE_DISCONNECTING;
broadcastUpdate(BleBroadcastAction.ACTION_GATT_DISCONNECTING);
mBluetoothGatt.disconnect();
}
程式結束後呢,還得釋放資源,怎麼說呢像c++的delete或者c語言free那樣吧。呼叫BluetoothGatt的close函式,關閉一波
public void close() {
if (mBluetoothGatt == null) {
return;
}
mBluetoothGatt.close();
mBluetoothGatt = null;
}
四、資料收發
1.接收資料
收資料呢,安卓也是採取了回撥的方式,一收到資料,就立馬通知使用者進行處理,這樣做的好處呢,我們可以及時處理收到的資料,不會像stm32 串列埠處理資料一樣,即有可能因為定時器來不及處理,資料就被覆蓋。筆者寫微控制器的時候還是被坑了一波爹的。
當然,裝置那麼多個服務,我們也不是要全部接受資料,安卓也沒有那麼勤快,什麼資料都自動幫我們接收,想要接受什麼資料,我們得先通知他一聲。
private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
呼叫setCharacteristicNotification(characteristic,enabled)通知安卓我要接收那個服務的資料,要接收就true,不接收就false。
這裡又涉及到了一個東西,就是characteristic。
characteristic可能更貼近服務的概念吧
Service是characteristic的集合,也就是說Service更像是一個組名,不直接提供資料交換,而Descriptor是用來描述characteristic的,比方說描述某個characteristic是否可讀,是否可寫。(這裡有待考證,我還沒有親自試驗過)。所以使用者使用最多應該還是characteristic。基本上光使用characteristic就可以換成資料互動。
呼叫readCharacteristic可以讀取資料,並進入onCharacteristicRead的回撥中去,解析讀到的資料。
2.傳送資料
發資料呢,這是一個比較奇怪的方式,不像串列埠一樣,懟著哪個通道就把資料給丟出去,方便快捷。按說是需要獲取到你要寫資料的characteristic,然後呼叫setValue函式,把你想要發的資料寫進characteristic,然後再發出去。
private boolean writeCharacteristic(final BluetoothGattCharacteristic characteristic) {
final BluetoothGatt gatt = mBluetoothGatt;
if (gatt == null || characteristic == null)
return false;
// Check characteristic property
final int properties = characteristic.getProperties();
if ((properties & (BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) == 0)
return false;
return gatt.writeCharacteristic(characteristic);
}
public boolean writeBle(byte[] data, UUID uuid) {
BluetoothGattCharacteristic targetCharacteristic = null;
for (BluetoothGattService bluetoothGattService : getSupportedGattServices()) {
for (BluetoothGattCharacteristic bluetoothGattCharacteristic : bluetoothGattService.getCharacteristics()) {
if (bluetoothGattCharacteristic.getUuid().toString().equals(uuid.toString())) {
targetCharacteristic = bluetoothGattCharacteristic;
}
}
}
if (targetCharacteristic == null){
return false;
}else {
targetCharacteristic.setValue(data);
return writeCharacteristic(targetCharacteristic);
}
// return writeCharacteristic()
}