本篇接續上篇碼農日常-『Android studio』Android 低功耗藍牙藍芽BLE (上)
要來聊聊下半場囉( ゜ρ゜)ノ
上篇介紹的部分是藍芽的掃描以及顯示裝置所攜帶的相關資料的部分
這次要來聊的是連線後的讀取及寫入的部分囉:D
但是話先說在前,因為藍芽裝置這種東西會寫到這邊的
很高機率是因為公司產品有需求才會寫到這
所以範例中的"寫資料給藍芽"的這個部分並不是非常完整,這部分還請見諒..
不過重點的程式都會特別提到重點在哪邊、怎麼寫;這點請放心
開始這部分之前,我建議大家可以去下載這個APP
nRF Connect for Mobile
Android 版
https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp&hl=zh_TW
IOS 版
https://apps.apple.com/tw/app/nrf-connect/id1054362403
這個是可以查看關於藍芽裝置中,有哪些服務、及哪些特性的工具
首先是掃描,沒什麼好說的,大家看一看吧
再來就是連線後的查看特性及操作了
像是本篇我所取得的這個藍芽裝置所擁有的藍芽特性就如下
總共有4個Service
比對一下我的成品
數量對了,但是名字不一樣?
其實是跟UUID有關,通常UUID00001800的欄位會放設備的名字
其他的UUID也都有各個普遍的職責,我的話礙於程式篇幅我就沒去寫判斷了
再來把延展式列表打開,便可以看見每個內容所代表的功能及特性,像是以下這張圖
這裏是這個裝置的讀&寫特性
再來換點第一個吧!
這部分就是讀取裝置名稱等相關資訊的部份,在參考的APP中那裏是一個向下箭頭,表示該特性可讀不可寫
這個APP很多韌體工程師都會用,也幫助我們這些菜逼八能夠有方向這個藍芽裝置怎麼用、怎麼寫
Btw...nRf是一家專門出廠藍芽相關韌體的廠商,所以有個這樣的APP完全不意外..
好,接著就是今天的內容了
今天要完成的部分就是下圖內容
以及雖然上篇就貼過GitHub了但再貼一次的Github
https://github.com/thumbb13555/BLE_Example
1. 概覽
上面簡單介紹完藍芽裝置的部分後,接著就是今天的實作
這回主要是紅字的DeviceInfoActivity.java
黃字ExpandableListAdapter.java & DescriptorAdapter.java
及綠字實體類ServiceInfo.java
Adapter的部分看起來Hen多Hen可怕
但是不用擔心,一來我會慢慢說,二來這些其實都是列表式元件(ExpandableListView、RecyclerView)的Adapter,其實都是為了顯示藍芽資訊才寫的,不寫也無所謂
那麼首先,還是多少要科普一下BLE藍牙中他們的階層關係,不然後面我講一堆專有名詞會看得跟智障一樣( ・ὢ・ )
首先看一下下面這張圖
礙於版面,我並沒有畫得很詳細
以下敘述: Service = 服務、Characteristic = 特徵、Descriptor = 描述,英文太長了,打到手軟...
首先是服務,服務他裡面包裝著很多特徵;而服務...基本上就是包裝特徵用的
特徵有很多屬性,大致最常看到的就是唯讀、可寫、可讀可寫等等,而實際操作這些東西也都是在特徵中操作
最後是描述符,就我個人的經驗來說,有描符的地方通常管的是裝置的讀寫
好,其實文字描述的地方不是什麼大重點,如果現在看不懂我文字描述沒關係,上面的圖記得就好囉(・ω・)b
2. BluetoothLeService.java
這裡是整個藍芽開發的核心架構
本站中,有曾經提過關於Service的開發(注意,這裡說的Service跟藍芽無關)
在這裡->碼農日常-『Android studio』應用Service完成一個簡單的音樂播放器
那麼,這裏因為程式很長,我們就一個一個步驟來做吧
首先,先宣告所有變數
public class BluetoothLeService extends Service { private final static String TAG = "CommunicationWithBT"; private BluetoothManager mBluetoothManager;//藍芽管理器 private BluetoothAdapter mBluetoothAdapter;//藍芽適配器 private String mBluetoothDeviceAddress;//藍芽設備位址 private BluetoothGatt mBluetoothGatt; private int mConnectionState = STATE_DISCONNECTED; private byte[] sendValue;//儲存要送出的資訊 private static final int STATE_DISCONNECTED = 0;//設備無法連接 private static final int STATE_CONNECTING = 1;//設備正在連接 private static final int STATE_CONNECTED = 2;//設備連接完畢 public final static String ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED";//已連接到GATT服務器 public final static String ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";//未連接GATT服務器 public final static String ACTION_GATT_SERVICES_DISCOVERED = "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";//未發現GATT服務 public final static String ACTION_DATA_AVAILABLE = "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";//接收到來自設備的數據,可通過讀取或操作獲得 public final static String EXTRA_DATA = "com.example.bluetooth.le.EXTRA_DATA"; //其他數據 private boolean lockCharacteristicRead = false;//由於送執會觸發onCharacteristicRead並造成干擾,故做一個互鎖 private final IBinder mBinder = new LocalBinder(); public class LocalBinder extends Binder { public BluetoothLeService getService() { return BluetoothLeService.this; } } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public boolean onUnbind(Intent intent) { return super.onUnbind(intent); } /** * 將byte[] ASCII 轉為字串的方法 */ public static String ascii2String(byte[] in) { final StringBuilder stringBuilder = new StringBuilder(in.length); for (byte byteChar : in) stringBuilder.append(String.format("%02X ", byteChar)); String output = null; try { output = new String(in, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return output; } /** * Byte轉16進字串工具 */ public static String byteArrayToHexStr(byte[] byteArray) { if (byteArray == null) { return null; } StringBuilder hex = new StringBuilder(byteArray.length * 2); for (byte aData : byteArray) { hex.append(String.format("%02X", aData)); } String gethex = hex.toString(); return gethex; } }
再來,來撰寫藍芽功能接收站了
請加入以下內容(兩個粉底白字之間)
這邊開始就出現我上頭講的那些關鍵字囉!!是不是(・ωー)~☆
public class BluetoothLeService extends Service { //(全域變數省略) public class LocalBinder extends Binder { public BluetoothLeService getService() { return BluetoothLeService.this; } } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public boolean onUnbind(Intent intent) { return super.onUnbind(intent); } /**藍芽資訊收發站*/ private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { /**當連接狀態發生改變*/ @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { String intentAction; if (newState == BluetoothProfile.STATE_CONNECTED) {//當設備已連接 intentAction = ACTION_GATT_CONNECTED; mConnectionState = STATE_CONNECTED; Log.i(TAG, "Connected to GATT server."); Log.i(TAG, "Attempting to start service discovery:" + mBluetoothGatt.discoverServices()); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {//當設備無法連接 intentAction = ACTION_GATT_DISCONNECTED; mConnectionState = STATE_DISCONNECTED; Log.i(TAG, "Disconnected from GATT server."); } } /**當發現新的服務器*/ @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { } else { Log.w(TAG, "onServicesDiscovered received: " + status); } } /**Descriptor寫出資訊給藍芽*/ @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { super.onDescriptorWrite(gatt, descriptor, status); Log.d(TAG, "送出資訊: Byte: " + byteArrayToHexStr(sendValue) + ", String: " + ascii2String(sendValue)); BluetoothGattCharacteristic RxChar = descriptor.getCharacteristic(); RxChar.setValue(sendValue); mBluetoothGatt.writeCharacteristic(RxChar); } /**讀取屬性(像是DeviceName、System ID等等)*/ @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { if (!lockCharacteristicRead){ } lockCharacteristicRead = false; Log.d(TAG, "onCharacteristicRead: "+ascii2String(characteristic.getValue())); } } /**如果特性有變更(就是指藍芽有傳值過來)*/ @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { if (mBluetoothAdapter == null || mBluetoothGatt == null) { Log.w(TAG, "BluetoothAdapter not initialized"); return; } lockCharacteristicRead = true; mBluetoothGatt.readCharacteristic(characteristic); String record = characteristic.getStringValue(0); byte[] a = characteristic.getValue(); Log.d(TAG, "readCharacteristic:回傳 " + record); Log.d(TAG, "readCharacteristic: 回傳byte[] " + byteArrayToHexStr(a)); } }; /** * 將byte[] ASCII 轉為字串的方法 */ public static String ascii2String(byte[] in) { final StringBuilder stringBuilder = new StringBuilder(in.length); for (byte byteChar : in) stringBuilder.append(String.format("%02X ", byteChar)); String output = null; try { output = new String(in, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } return output; } /** * Byte轉16進字串工具 */ public static String byteArrayToHexStr(byte[] byteArray) { if (byteArray == null) { return null; } StringBuilder hex = new StringBuilder(byteArray.length * 2); for (byte aData : byteArray) { hex.append(String.format("%02X", aData)); } String gethex = hex.toString(); return gethex; } }
再來,加入BroadCast
BroadCast是Android中的廣播事件,詳細資料我也有寫~
->碼農日常-『Android studio』Broadcast廣播的應用方法
在這裡的廣播用意是要把藍芽接收到的資料傳到DeviceInfoActivity.java用
那麼,來加吧
public class BluetoothLeService extends Service { //(全域變數略) public class LocalBinder extends Binder { public BluetoothLeService getService() { return BluetoothLeService.this; } } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public boolean onUnbind(Intent intent) { return super.onUnbind(intent); } /**藍芽資訊收發站*/ private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { /**當連接狀態發生改變*/ @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { String intentAction; if (newState == BluetoothProfile.STATE_CONNECTED) {//當設備已連接 intentAction = ACTION_GATT_CONNECTED; mConnectionState = STATE_CONNECTED; broadcastUpdate(intentAction); Log.i(TAG, "Connected to GATT server."); Log.i(TAG, "Attempting to start service discovery:" + mBluetoothGatt.discoverServices()); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {//當設備無法連接 intentAction = ACTION_GATT_DISCONNECTED; mConnectionState = STATE_DISCONNECTED; Log.i(TAG, "Disconnected from GATT server."); broadcastUpdate(intentAction); } } /**當發現新的服務器*/ @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED); } else { Log.w(TAG, "onServicesDiscovered received: " + status); } } /**Descriptor寫出資訊給藍芽*/ @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { super.onDescriptorWrite(gatt, descriptor, status); Log.d(TAG, "送出資訊: Byte: " + byteArrayToHexStr(sendValue) + ", String: " + ascii2String(sendValue)); BluetoothGattCharacteristic RxChar = descriptor.getCharacteristic(); RxChar.setValue(sendValue); mBluetoothGatt.writeCharacteristic(RxChar); } /**讀取屬性(像是DeviceName、System ID等等)*/ @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { if (!lockCharacteristicRead){ broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); } lockCharacteristicRead = false; Log.d(TAG, "onCharacteristicRead: "+ascii2String(characteristic.getValue())); } } /**如果特性有變更(就是指藍芽有傳值過來)*/ @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); if (mBluetoothAdapter == null || mBluetoothGatt == null) { Log.w(TAG, "BluetoothAdapter not initialized"); return; } lockCharacteristicRead = true; mBluetoothGatt.readCharacteristic(characteristic); String record = characteristic.getStringValue(0); byte[] a = characteristic.getValue(); Log.d(TAG, "readCharacteristic:回傳 " + record); Log.d(TAG, "readCharacteristic: 回傳byte[] " + byteArrayToHexStr(a)); } }; /**更新廣播*/ private void broadcastUpdate(final String action) { final Intent intent = new Intent(action); sendBroadcast(intent); } /**更新廣播*/ private void broadcastUpdate(final String action, final BluetoothGattCharacteristic characteristic) { final Intent intent = new Intent(action); /**對於所有其他配置文件,以十六進制寫數據*/ final byte[] data = characteristic.getValue(); if (data != null && data.length > 0) { final StringBuilder stringBuilder = new StringBuilder(data.length); for (byte byteChar : data) stringBuilder.append(String.format("%02X ", byteChar)); intent.putExtra(EXTRA_DATA, characteristic.getValue()); } sendBroadcast(intent); } /** * 將byte[] ASCII 轉為字串的方法 */ public static String ascii2String(byte[] in) { //略 } /** * Byte轉16進字串工具 */ public static String byteArrayToHexStr(byte[] byteArray) { //略 } }
最後是實作對外方法
public class BluetoothLeService extends Service { //略 /**取得特性列表(characteristic的特性)*/ public ArrayList<String> getPropertiesTagArray(int properties) { int addPro = properties; ArrayList<String> arrayList = new ArrayList<>(); int[] bluetoothGattCharacteristicCodes = new int[]{ BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS, BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE, BluetoothGattCharacteristic.PROPERTY_INDICATE, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PROPERTY_BROADCAST }; String[] bluetoothGattCharacteristicName = new String[]{ "EXTENDED_PROPS", "SIGNED_WRITE", "INDICATE", "NOTIFY", "WRITE", "WRITE_NO_RESPONSE", "READ", "BROADCAST" }; for (int i = 0; i < bluetoothGattCharacteristicCodes.length; i++) { int code = bluetoothGattCharacteristicCodes[i]; if (addPro >= code) { addPro -= code; arrayList.add(bluetoothGattCharacteristicName[i]); } } return arrayList; } public class LocalBinder extends Binder { public BluetoothLeService getService() { return BluetoothLeService.this; } } @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public boolean onUnbind(Intent intent) { close(); return super.onUnbind(intent); } /**初始化藍芽*/ public boolean initialize() { if (mBluetoothManager == null) { mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); if (mBluetoothManager == null) { return false; } } mBluetoothAdapter = mBluetoothManager.getAdapter(); if (mBluetoothAdapter == null) { return false; } return true; } /**連線*/ public boolean connect(final String address) { if (mBluetoothAdapter == null || address == null) { return false; } if (address.equals(mBluetoothDeviceAddress) && mBluetoothGatt != null) { if (mBluetoothGatt.connect()) { mConnectionState = STATE_CONNECTING; return true; } else { return false; } } final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); if (device == null) { return false; } mBluetoothGatt = device.connectGatt(this, false, mGattCallback); mBluetoothDeviceAddress = address; mConnectionState = STATE_CONNECTING; return true; } /**斷開連線*/ public void disconnect() { if (mBluetoothAdapter == null || mBluetoothGatt == null) { return; } mBluetoothGatt.disconnect(); } public void close() { if (mBluetoothGatt == null) { return; } mBluetoothGatt.close(); mBluetoothGatt = null; } /**送字串模組*/ public boolean sendValue(String value, BluetoothGattCharacteristic characteristic) { try { this.sendValue = value.getBytes(); setCharacteristicNotification(characteristic, true); return true; } catch (Exception e) { return false; } } /**送byte[]模組*/ public boolean sendValue(byte[] value,BluetoothGattCharacteristic characteristic){ try{ this.sendValue = value; setCharacteristicNotification(characteristic, true); return true; }catch (Exception e){ return false; } } /**送出資訊*/ private void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) { if (mBluetoothAdapter == null || mBluetoothGatt == null) { return; } if (characteristic != null) { for (BluetoothGattDescriptor dp : characteristic.getDescriptors()) { if (enabled) { dp.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); } else { dp.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); } /**送出 * @see onDescriptorWrite()*/ mBluetoothGatt.writeDescriptor(dp); } mBluetoothGatt.setCharacteristicNotification(characteristic, true); mBluetoothGatt.readCharacteristic(characteristic); } } /**將搜尋到的服務傳出*/ public List<BluetoothGattService> getSupportedGattServices() { if (mBluetoothGatt == null) return null; return mBluetoothGatt.getServices(); } /**藍芽資訊收發站*/ private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { //略 }; /**更新廣播*/ private void broadcastUpdate(final String action) { //略 } /**更新廣播*/ private void broadcastUpdate(final String action, final BluetoothGattCharacteristic characteristic) { //略 } /** * 將byte[] ASCII 轉為字串的方法 */ public static String ascii2String(byte[] in) { //略 } /** * Byte轉16進字串工具 */ public static String byteArrayToHexStr(byte[] byteArray) { //略 } }
最後記得
凡有Service的部分,都要去AndroidManifest.xml申請
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.jetec.ble_example"> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".Controller.DeviceInfoActivity"></activity> <activity android:name=".Controller.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".Module.Service.BluetoothLeService" android:enabled="true" /> </application>
礙於版面,我就不PO全部的程式碼
若要完整程式碼請點此
3. DeviceInfoActivity.java(前半,藍芽回調部分)
Okay,Service部分完成了
接下來就是Main的部分,也就是DeviceInfoActivity.java
在這邊閒聊一下,我自己在重構專案的時候,除了刪減掉很多程式碼之外,同時也將UI的部分跟藍芽接收的部分徹底分開
所以這裡的前半部分,意味著只要寫到這邊就可以成功地跟連接藍芽以及接收資料了(寫資料要到下半段)
再提一下,這邊會用到Broadcast的技巧
不太會的人可以回去看這篇
碼農日常-『Android studio』Broadcast廣播的應用方法
這部分我先直接PO給你(但是省略掉ExpandableListView的UI設置部分)
public class DeviceInfoActivity extends AppCompatActivity{ public static final String TAG = DeviceInfoActivity.class.getSimpleName()+"My"; public static final String INTENT_KEY = "GET_DEVICE"; private BluetoothLeService mBluetoothLeService; private ScannedData selectedDevice; private TextView tvAddress,tvStatus,tvRespond; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_device_control); selectedDevice = (ScannedData) getIntent().getSerializableExtra(INTENT_KEY); initBLE(); initUI(); } /**初始化藍芽*/ private void initBLE(){ /**綁定Service * @see BluetoothLeService*/ Intent bleService = new Intent(this, BluetoothLeService.class); bindService(bleService,mServiceConnection,BIND_AUTO_CREATE); /**設置廣播*/ IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED);//連接一個GATT服務 intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED);//從GATT服務中斷開連接 intentFilter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED);//查找GATT服務 intentFilter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE);//從服務中接受(收)數據 registerReceiver(mGattUpdateReceiver, intentFilter); if (mBluetoothLeService != null) mBluetoothLeService.connect(selectedDevice.getAddress()); } /**初始化UI*/ private void initUI(){ tvAddress = findViewById(R.id.device_address); tvStatus = findViewById(R.id.connection_state); tvRespond = findViewById(R.id.data_value); tvAddress.setText(selectedDevice.getAddress()); tvStatus.setText("未連線"); tvRespond.setText("---"); } /**藍芽已連接/已斷線資訊回傳*/ private ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder service) { mBluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService(); if (!mBluetoothLeService.initialize()) { finish(); } mBluetoothLeService.connect(selectedDevice.getAddress()); } @Override public void onServiceDisconnected(ComponentName componentName) { mBluetoothLeService.disconnect(); } }; private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); /**如果有連接*/ if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) { Log.d(TAG, "藍芽已連線"); tvStatus.setText("已連線"); } /**如果沒有連接*/ else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) { Log.d(TAG, "藍芽已斷開"); } /**找到GATT服務*/ else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) { Log.d(TAG, "已搜尋到GATT服務"); List<BluetoothGattService> gattList = mBluetoothLeService.getSupportedGattServices(); displayGattAtLogCat(gattList); } /**接收來自藍芽傳回的資料*/ else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) { Log.d(TAG, "接收到藍芽資訊"); byte[] getByteData = intent.getByteArrayExtra(BluetoothLeService.EXTRA_DATA); StringBuilder stringBuilder = new StringBuilder(getByteData.length); for (byte byteChar : getByteData) stringBuilder.append(String.format("%02X ", byteChar)); String stringData = new String(getByteData); Log.d(TAG, "String: "+stringData+"\n" +"byte[]: "+BluetoothLeService.byteArrayToHexStr(getByteData)); tvRespond.setText("String: "+stringData+"\n" +"byte[]: "+BluetoothLeService.byteArrayToHexStr(getByteData)); } } };//onReceive /**將藍芽所有資訊顯示在Logcat*/ private void displayGattAtLogCat(List<BluetoothGattService> gattList){ for (BluetoothGattService service : gattList){ Log.d(TAG, "Service: "+service.getUuid().toString()); for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()){ Log.d(TAG, "\tCharacteristic: "+characteristic.getUuid().toString()+" ,Properties: "+ mBluetoothLeService.getPropertiesTagArray(characteristic.getProperties())); for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()){ Log.d(TAG, "\t\tDescriptor: "+descriptor.getUuid().toString()); } } } } /**關閉藍芽*/ private void closeBluetooth() { if (mBluetoothLeService == null) return; mBluetoothLeService.disconnect(); unbindService(mServiceConnection); unregisterReceiver(mGattUpdateReceiver); } @Override protected void onStop() { super.onStop(); closeBluetooth(); } }
我個人是覺得我寫得還蠻整齊的啦XD
其實到這邊就可以按下執行了
執行起來的話,便可在Logcat看到看到以下訊息
到這邊為止,藍芽本身的功能已經完成99%了
以研究來說,到這邊就夠囉!
再下的篇幅,都是寫跟UI相關的囉(只有到最後面有送資料...)!
4. ServiceInfo.java(藍芽資訊實體類)
首先先新增實體類,以便後續使用
public class ServiceInfo { final private UUID uuid; final private List<CharacteristicInfo> characteristicInfo; public ServiceInfo(BluetoothGattService gattServices) { this.uuid = gattServices.getUuid(); characteristicInfo = new ArrayList<>(); for (BluetoothGattCharacteristic characteristic :gattServices.getCharacteristics()) { characteristicInfo.add(new CharacteristicInfo(characteristic)); } } public String getTitle() { return "Service"; } public UUID getUuid() { return uuid; } public List<CharacteristicInfo> getCharacteristicInfo() { return characteristicInfo; } public static class CharacteristicInfo{ final private UUID uuid; final private ArrayList<String> propertiesTags; final private ArrayList<Integer> propertiesCode; final private List<DescriptorsInfo> descriptorsInfo; final private BluetoothGattCharacteristic characteristic; public CharacteristicInfo(BluetoothGattCharacteristic characteristic) { this.characteristic = characteristic; this.uuid = characteristic.getUuid(); this.propertiesCode = getPropertiesCodeArray(characteristic.getProperties()); this.propertiesTags = getPropertiesTagArray(characteristic.getProperties()); descriptorsInfo = new ArrayList<>(); for (BluetoothGattDescriptor descriptor: characteristic.getDescriptors()) { descriptorsInfo.add(new DescriptorsInfo(descriptor)); } } public BluetoothGattCharacteristic getCharacteristic() { return characteristic; } public String getTitle() { return "Characteristic"; } public UUID getUuid() { return uuid; } public ArrayList<String> getPropertiesTags() { return propertiesTags; } public ArrayList<Integer> getPropertiesCode() { return propertiesCode; } public List<DescriptorsInfo> getDescriptorsInfo() { return descriptorsInfo; } private ArrayList<String> getPropertiesTagArray(int properties){ int addPro = properties; ArrayList<String> arrayList = new ArrayList<>(); int[] bluetoothGattCharacteristicCodes = new int[]{ BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS, BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE, BluetoothGattCharacteristic.PROPERTY_INDICATE, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PROPERTY_BROADCAST }; String[] bluetoothGattCharacteristicName = new String[]{ "EXTENDED_PROPS", "SIGNED_WRITE", "INDICATE", "NOTIFY", "WRITE", "WRITE_NO_RESPONSE", "READ", "BROADCAST" }; for (int i = 0; i < bluetoothGattCharacteristicCodes.length; i++) { int code = bluetoothGattCharacteristicCodes[i]; if (addPro>=code){ addPro -= code; arrayList.add(bluetoothGattCharacteristicName[i]); } } return arrayList; } private ArrayList<Integer> getPropertiesCodeArray(int properties){ int addPro = properties; ArrayList<Integer> arrayList = new ArrayList<>(); int[] bluetoothGattCharacteristicCodes = new int[]{ BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS, BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE, BluetoothGattCharacteristic.PROPERTY_INDICATE, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PROPERTY_BROADCAST }; for (int i = 0; i < bluetoothGattCharacteristicCodes.length; i++) { int code = bluetoothGattCharacteristicCodes[i]; if (addPro>=code){ addPro -= code; arrayList.add(bluetoothGattCharacteristicCodes[i]); } } return arrayList; } public static class DescriptorsInfo{ private UUID uuid; private String title; public DescriptorsInfo(BluetoothGattDescriptor descriptor) { this.uuid = descriptor.getUuid(); this.title = "Descriptor"; } public String getTitle() { return title; } public UUID getUuid() { return uuid; } } } }
5. ExpandableListAdapter.java(可展開式列表,用於顯示藍芽Service)
這邊是使用ExpandableListView的列表式元件
這篇文章有專門介紹他喔
->碼農日常-『Android studio』ExpandableListView(抽屜式列表、可展開式ListView)之基本用法
->碼農日常-『Android studio』ExpandableListView+長按顯示選單視窗ContextMenu
public class ExpandableListAdapter extends BaseExpandableListAdapter { private List<ServiceInfo> serviceInfo = new ArrayList<>(); public OnChildClick onChildClick; public void setServiceInfo(List<BluetoothGattService> services){ for (BluetoothGattService s: services) { this.serviceInfo.add(new ServiceInfo(s)); } notifyDataSetChanged(); } @Override public int getGroupCount() { return serviceInfo.size(); } @Override public int getChildrenCount(int groupPosition) { return serviceInfo.get(groupPosition).getCharacteristicInfo().size(); } @Override public Object getGroup(int groupPosition) { return groupPosition; } @Override public Object getChild(int groupPosition, int childPosition) { return childPosition; } @Override public long getGroupId(int groupPosition) { return groupPosition; } @Override public long getChildId(int groupPosition, int childPosition) { return childPosition; } @Override public boolean hasStableIds() { return false; } @Override public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.service_item,parent,false); } convertView.setTag(R.layout.service_item, groupPosition); TextView tvTitle = convertView.findViewById(R.id.textView_Title); TextView tvUUID = convertView.findViewById(R.id.textView_UUID); TextView tvValue = convertView.findViewById(R.id.textView_Descriptors); tvTitle.setText(serviceInfo.get(groupPosition).getTitle()); tvUUID.setText("UUID: "+serviceInfo.get(groupPosition).getUuid().toString()); tvValue.setText("PRIMARY SERVICE"); return convertView; } @Override public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { if (convertView == null) { convertView = LayoutInflater.from(parent.getContext()).inflate(R.layout.characteristic_item,parent,false); } convertView.setTag(R.layout.characteristic_item, groupPosition); TextView tvTitle = convertView.findViewById(R.id.textView_Title); TextView tvUUID = convertView.findViewById(R.id.textView_UUID); TextView tvProperties = convertView.findViewById(R.id.textView_Properties); RecyclerView recyclerView = convertView.findViewById(R.id.recyclerview_des); ServiceInfo.CharacteristicInfo info = serviceInfo.get(groupPosition).getCharacteristicInfo().get(childPosition); DescriptorAdapter adapter = new DescriptorAdapter(info.getDescriptorsInfo()); recyclerView.setLayoutManager(new LinearLayoutManager(parent.getContext())); recyclerView.setAdapter(adapter); tvTitle.setText(info.getTitle()); tvUUID.setText("UUID: "+info.getUuid().toString()); tvProperties.setText("Properties: "+info.getPropertiesTags().toString()); convertView.setOnClickListener(v -> { onChildClick.onChildClick(info); }); return convertView; } @Override public boolean isChildSelectable(int groupPosition, int childPosition) { return true; } public interface OnChildClick{ void onChildClick(ServiceInfo.CharacteristicInfo info); } }
介面順便
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/textView_Title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="24dp" android:layout_marginTop="8dp" android:text="Title" android:textAppearance="@style/TextAppearance.AppCompat.Body2" android:textSize="16sp" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/textView_UUID" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:text="TextView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/textView_Title" app:layout_constraintTop_toBottomOf="@+id/textView_Title" /> <TextView android:id="@+id/textView_Descriptors" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginBottom="4dp" android:text="TextView" android:singleLine="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/textView_UUID" app:layout_constraintTop_toBottomOf="@+id/textView_UUID" /> </androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?android:attr/selectableItemBackground"> <TextView android:id="@+id/textView_Title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="32dp" android:layout_marginTop="8dp" android:text="Title" android:textAppearance="@style/TextAppearance.AppCompat.Body2" android:textSize="16sp" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/textView_UUID" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:text="TextView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/textView_Title" app:layout_constraintTop_toBottomOf="@+id/textView_Title" /> <TextView android:id="@+id/textView_Properties" android:layout_width="0dp" android:layout_height="wrap_content" android:singleLine="false" android:text="TextView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/textView_UUID" app:layout_constraintTop_toBottomOf="@+id/textView_UUID" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerview_des" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/textView_Properties" app:layout_constraintTop_toBottomOf="@+id/textView_Properties" /> </androidx.constraintlayout.widget.ConstraintLayout>
6. DescriptorAdapter.java(可展開式列表中的RecyclerView,用於顯示藍芽Descriptor)
public class DescriptorAdapter extends RecyclerView.Adapter<DescriptorAdapter.ViewHolder> { List<ServiceInfo.CharacteristicInfo.DescriptorsInfo> list; public DescriptorAdapter(List<ServiceInfo.CharacteristicInfo.DescriptorsInfo> list) { this.list = list; } public class ViewHolder extends RecyclerView.ViewHolder { TextView tvTitle,tvUuid; public ViewHolder(@NonNull View itemView) { super(itemView); tvTitle = itemView.findViewById(R.id.textView_Title); tvUuid = itemView.findViewById(R.id.textView_UUID); } } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.descriptor_item,parent,false)); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { holder.tvTitle.setText(list.get(position).getTitle()); holder.tvUuid.setText(list.get(position).getUuid().toString()); } @Override public int getItemCount() { return list.size(); } }
介面支援
descriptor_item.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/textView_Title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Title" android:textAppearance="@style/TextAppearance.AppCompat.Body2" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/textView_UUID" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:text="TextView" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="@+id/textView_Title" app:layout_constraintTop_toBottomOf="@+id/textView_Title" /> </androidx.constraintlayout.widget.ConstraintLayout>
7. DeviceInfoActivity.java(後半,安裝ExpandListViewv相關模組以及寫出資料)
因為中間的東西都是介面,實在是太攏長了(嘆
所以我中間真的超懶得再講些什麼( ・ὢ・ )
總之~~這是最後的重點了,要來把模組實作上去囉
請加入以下粉底白字內容
public class DeviceInfoActivity extends AppCompatActivity implements ExpandableListAdapter.OnChildClick { public static final String TAG = DeviceInfoActivity.class.getSimpleName()+"My"; public static final String INTENT_KEY = "GET_DEVICE"; private BluetoothLeService mBluetoothLeService; private ScannedData selectedDevice; private TextView tvAddress,tvStatus,tvRespond; private ExpandableListAdapter expandableListAdapter; private boolean isLedOn = false; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_device_control); selectedDevice = (ScannedData) getIntent().getSerializableExtra(INTENT_KEY); initBLE(); initUI(); } /**初始化藍芽*/ private void initBLE(){ /**綁定Service * @see BluetoothLeService*/ Intent bleService = new Intent(this, BluetoothLeService.class); bindService(bleService,mServiceConnection,BIND_AUTO_CREATE); /**設置廣播*/ IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED);//連接一個GATT服務 intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED);//從GATT服務中斷開連接 intentFilter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED);//查找GATT服務 intentFilter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE);//從服務中接受(收)數據 registerReceiver(mGattUpdateReceiver, intentFilter); if (mBluetoothLeService != null) mBluetoothLeService.connect(selectedDevice.getAddress()); } /**初始化UI*/ private void initUI(){ expandableListAdapter = new ExpandableListAdapter(); expandableListAdapter.onChildClick = this::onChildClick; ExpandableListView expandableListView = findViewById(R.id.gatt_services_list); expandableListView.setAdapter(expandableListAdapter); tvAddress = findViewById(R.id.device_address); tvStatus = findViewById(R.id.connection_state); tvRespond = findViewById(R.id.data_value); tvAddress.setText(selectedDevice.getAddress()); tvStatus.setText("未連線"); tvRespond.setText("---"); } /**藍芽已連接/已斷線資訊回傳*/ private ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder service) { mBluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService(); if (!mBluetoothLeService.initialize()) { finish(); } mBluetoothLeService.connect(selectedDevice.getAddress()); } @Override public void onServiceDisconnected(ComponentName componentName) { mBluetoothLeService.disconnect(); } }; private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); /**如果有連接*/ if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) { Log.d(TAG, "藍芽已連線"); tvStatus.setText("已連線"); } /**如果沒有連接*/ else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) { Log.d(TAG, "藍芽已斷開"); } /**找到GATT服務*/ else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) { Log.d(TAG, "已搜尋到GATT服務"); List<BluetoothGattService> gattList = mBluetoothLeService.getSupportedGattServices(); displayGattAtLogCat(gattList); expandableListAdapter.setServiceInfo(gattList); } /**接收來自藍芽傳回的資料*/ else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) { Log.d(TAG, "接收到藍芽資訊"); byte[] getByteData = intent.getByteArrayExtra(BluetoothLeService.EXTRA_DATA); StringBuilder stringBuilder = new StringBuilder(getByteData.length); for (byte byteChar : getByteData) stringBuilder.append(String.format("%02X ", byteChar)); String stringData = new String(getByteData); Log.d(TAG, "String: "+stringData+"\n" +"byte[]: "+BluetoothLeService.byteArrayToHexStr(getByteData)); tvRespond.setText("String: "+stringData+"\n" +"byte[]: "+BluetoothLeService.byteArrayToHexStr(getByteData)); isLedOn = BluetoothLeService.byteArrayToHexStr(getByteData).equals("486173206F6E"); } } };//onReceive /**將藍芽所有資訊顯示在Logcat*/ private void displayGattAtLogCat(List<BluetoothGattService> gattList){ for (BluetoothGattService service : gattList){ Log.d(TAG, "Service: "+service.getUuid().toString()); for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()){ Log.d(TAG, "\tCharacteristic: "+characteristic.getUuid().toString()+" ,Properties: "+ mBluetoothLeService.getPropertiesTagArray(characteristic.getProperties())); for (BluetoothGattDescriptor descriptor : characteristic.getDescriptors()){ Log.d(TAG, "\t\tDescriptor: "+descriptor.getUuid().toString()); } } } } /**關閉藍芽*/ private void closeBluetooth() { if (mBluetoothLeService == null) return; mBluetoothLeService.disconnect(); unbindService(mServiceConnection); unregisterReceiver(mGattUpdateReceiver); } @Override protected void onStop() { super.onStop(); closeBluetooth(); } /**點擊物件,即寫資訊給藍芽(或直接讀藍芽裝置資訊)*/ @Override public void onChildClick(ServiceInfo.CharacteristicInfo info) { String led = "off"; if (!isLedOn) led = "on"; mBluetoothLeService.sendValue(led,info.getCharacteristic()); } }
最後,請注意綠底白字的內容
綠底白字就是送出資料,而設置輸出的功能都寫在一下內容(在BluetoothLeService.java內)
/**送字串模組*/ public boolean sendValue(String value, BluetoothGattCharacteristic characteristic) { try { this.sendValue = value.getBytes(); setCharacteristicNotification(characteristic, true); return true; } catch (Exception e) { return false; } }
/**送出資訊*/ private void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, boolean enabled) { if (mBluetoothAdapter == null || mBluetoothGatt == null) { return; } if (characteristic != null) { for (BluetoothGattDescriptor dp : characteristic.getDescriptors()) { if (enabled) { dp.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); } else { dp.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); } /**送出 * @see onDescriptorWrite()*/ mBluetoothGatt.writeDescriptor(dp); } mBluetoothGatt.setCharacteristicNotification(characteristic, true); mBluetoothGatt.readCharacteristic(characteristic); } }
/**Descriptor寫出資訊給藍芽*/ @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { super.onDescriptorWrite(gatt, descriptor, status); Log.d(TAG, "送出資訊: Byte: " + byteArrayToHexStr(sendValue) + ", String: " + ascii2String(sendValue)); BluetoothGattCharacteristic RxChar = descriptor.getCharacteristic(); RxChar.setValue(sendValue); mBluetoothGatt.writeCharacteristic(RxChar); }
參考一下囉
當然,每個藍牙設備都是看韌體工程師怎麼寫,並不是說你連線後一定會像我這個樣子
如果想像我這樣顯示的話...那你就參考這篇文章,買這些設備來學習吧....
結語
這篇文章這是第二版本了(2021/6/25更)
沒辦法,一年後回頭看,問題太多邏輯太亂
連我自己都看得霧煞煞...
也有鑑於很多人問我問題,我才意識到這篇文章不能再繼續爛下去了
所以才買了Arduino模組來自己寫...
說真的啦,我看大概沒幾個部落格站長像我這麼拼...
以下為心情寫作,加減看就好
對我而言藍芽專案是我入公司後的第一項課題,我研究藍芽相關的技術也僅半年左右的時間
對我而言,我現在任職剛好滿一年,也是我畢業後第一年的工作
這篇文章對我自己也是個蠻重大的意義,表示自己在某種程度上已經漸漸克服藍芽了
甚至在寫作的過程中,又有了更多的新發現跟見解
也不知道這樣的解說模式,能不能真的讓你看懂呢?
藍芽系列就寫到這,感謝你們的點閱!
系列文章在此,請翻閱看看~~
碼農日常-『Android studio』Android 低功耗藍牙藍芽BLE (上)
碼農日常-『Android studio』Android 低功耗藍牙藍芽BLE (下)