本篇接續上篇碼農日常-『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模組來自己寫...
說真的啦,我看大概沒幾個部落格站長像我這麼拼...
以下為心情寫作,加減看就好
對我而言藍芽專案是我入公司後的第一項課題,我研究藍芽相關的技術也僅半年左右的時間
對我而言,我現在任職剛好滿一年,也是我畢業後第一年的工作
這篇文章對我自己也是個蠻重大的意義,表示自己在某種程度上已經漸漸克服藍芽了
甚至在寫作的過程中,又有了更多的新發現跟見解
也不知道這樣的解說模式,能不能真的讓你看懂呢?
藍芽系列就寫到這,感謝你們的點閱!
系列文章在此,請翻閱看看~~
^^|||太深奧了..叨叨只能佩服~
哈哈哈...我就是幹這行的,應該的啦! 感謝你的留言拜訪~
身為一個剛開始接觸BLE的菜鳥,真的覺得你的文章對我來說受益良多,謝謝你願意分享你理解的內容。
不會,我也只是分享我所理解的淺見而已;感謝你的留言鼓勵:D
您好 ! 看了您的文章讓我學到了不少東西 ,文章給了我很大的幫助 ! 想請問有關掃描的問題,在掃描裝置時,但我有用別台手機開啟藍牙,卻沒辦法掃描到他的名稱 ? 我把 device.getName()!=null 改成 == 才能掃描到裝置,不然都沒有名稱 請問有可能是甚麼問題導致的 ?
嗨早安,感謝留言 device.getName() != null 是為了把沒有名字的藍芽濾掉;因為通常沒有名字的藍芽裝置我都把他當作雜訊或是特殊裝置。 如果你想掃描所有裝置的話,直接把這行拿掉就好;通常沒有名稱的話都是藍芽硬體那邊本身沒有命名所致,這部份看要不要請你們的韌體工程師幫忙解決吧!
您好><, 我有個問題想請問您, 如果我是使用startDiscovery()這個函式去搜尋裝置跟MAC, 那後面接收資料的部分還能使用嗎?還是僅限BLE裝置? 因為我想做一個可以當手機或電腦藍牙傳資料過來, 然後可以查看封包內容是長怎樣的APP, 但是用LeScan會搜不到手機或電腦裝置的藍牙, 所以才想問看看如果換個搜尋方法那後面還可不可行。 不好意思,麻煩您了
哈囉多謝留言 startDiscovery能搜尋"所有"的藍牙 因為藍牙有很多版本,所以你在LeScan掃不到的裝置Discovery掃得到(因為手機跟電腦多數使用經典藍芽) 不過現在也有新的電腦手機是實裝BLE藍芽的,所以我範例中其實可以掃到我的MacBook 所以回答你的問題:"如果換個搜尋方法那後面還可不可行" 答案是你要先知道你要傳送的是經典藍芽還是BLE藍芽 如果是經典藍芽,那麼我也不能很肯定我後續的文章會對你有幫助 反之如果確定裝置為BLE,那後續的文章就能幫助到你 關於startLeScan跟startDiscovery的差別在這篇文章的第三大段的第二小段"設備搜索"的部分 https://www.jianshu.com/p/7d814c22a085 還請參閱囉:D
想請問個問題!! 上面是要手動點選特徵 才會顯示資料 想請問如何一次把資料顯示出來
哈囉多謝留言 特徵(characteristic)的取得會是在第二大段中SampleGattAttributes.java 的lookup()內 我這邊的寫法也是從那邊取得特徵,然後在DeviceControlActivity.java 中的displayGattServices()內顯示出來 至於要怎麼把資料顯示,這就是別的話題了 希望可以幫助到你
*****
*****
*****
*****
大大您好,如果想要將4個sensor的一筆data透過BLE傳輸並顯示在app上的話,請問該怎麼做呢? 是一樣透過連線的方式收data 還是可以透過廣播的方式(不建立連線)
Hi感謝留言來訪 單以經驗回你的話,我會建議使用廣播處理(因為我自己就是這樣做) 雖然韌體會比較吃重,但是以產品來說擴充性就會很強 一些淺見供你參考囉
謝謝您即時的回覆,不好意思請問有任何藍芽廣播處理的source code或是較好懂的教學嗎? 因為我對於物件導向語言並不熟悉,也沒有開發APP的經驗 但是這個禮拜就必須完成藍芽的APP了 我看的教學是https://www.youtube.com/watch?v=x1y4tEHDwk0&feature=youtu.be&ab_channel=InfoQ 裡面做的與我想做的相關,但是他並沒有提供code
廣播處理的話在我的網誌中藍芽(上)的文章就是講廣播處理了 ->https://thumbb13555.pixnet.net/blog/post/325826532-ble 那篇網誌也有提供一些我找過的參考資料,此外我工作做到的內容就是在做跟你提供的那個教學幾乎一樣的東西 你先參考我寫的那篇文章吧!看完後哪裡看不懂再留言給我,我會再跟你解釋 我有提供Source code,把我的Code載下來研究先吧 希望可以幫助到你
您好,請問大大如果再已經確定要讀取特徵值的UUID的情況下怎麼跳過點擊延展式列表的方式取得資料呢,嘗試過直接在readCharacteristic後加入UUID,目前的想法是將延展式列表的顯示部分刪掉然後將點擊延展式列表中的groupPosition和 childPosition直接賦予值,不知道是否可行,目前卡在不知怎麼把點擊事件改成讀取事件 請大大出手相救 感謝TT
我提供一個很北爛的作法,但是實測有效 假設~你的APP是連固定系列的裝置的話(也就是說Characteristic)不會改的話 你可以先找出讀取的groupPos..跟childPos..,然後把它寫死 就是在這邊 final BluetoothGattCharacteristic characteristic = mGattCharacteristics.get(groupPosition).get(childPosition); 改為 final BluetoothGattCharacteristic characteristic = mGattCharacteristics.get(2).get(1); 這樣就會取得類別BluetoothGattCharacteristic 了 而且可以把它存為static變數,這樣你個連線都可以很輕鬆地調用送出 這是我在公司專案的作法,參考看看吧
不確定我的想法正不正確,但要將點擊事件改為讀取事件應該能透過廣播辦到~?
廣播一般來說只能收值,不能傳值 要做到互相溝通的話一定要連線才能實踐
感謝救援,哈哈我原本也是想到這個北爛辦法,那請問因為現在他是寫在點擊事件也就是onChildClick之下所以還是得先點擊後才能顯示值,這邊的onChildClick應該要怎麼取消呢,抱歉問題一堆><
就把onChildClick拿掉就可以啦? 如果你是問要怎麼在連線完成後直接觸發 那你可以把觸發寫在DeviceControlActivity.java內的 BroadcastReceiver mGattUpdateReceiver的 /**如果找到GATT服務*/ else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {...} 判斷式內喔 我的專案是這樣寫的 else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) { displayGattServices(mBluetoothLeService.getSupportedGattServices()); BluetoothGattCharacteristic characteristic; try { characteristic = mGattCharacteristics.get(2).get(0); } catch (Exception e) { characteristic = mGattCharacteristics.get(2).get(1); } mBluetoothLeService.setCharacteristicNotification(characteristic, true); } 供你參考囉 此外我這樣寫,目前已經半牛多沒有被回報閃退了,所以應該OK啦ㄏㄏㄏ
我想請問有沒有辦法同時監聽2個不同service的characteristic? 我現在手上有一個專案,是需要監聽監聽不同service的characteristic, 而且在有需要的時候,在某個service的characteristic發送訊息(不是前面所述的2個,是第3個),這是有可能寫出來的嗎?
你好,我不確定你說的"同時監聽"的定義 不過如果你家的產品會一直丟值過來的話..那樣是OK的 具體來說就是在onDescriptorWrite抓出你要的service的characteristic 就OK了 第二個是要指定某個service的characteristic發送訊息吧? 那也是OK的 這部分則直接去改setCharacteristicNotification裡面的 UUID ServiceUUID = UUID.fromString(SampleGattAttributes.myGatt.get(5)); UUID TXUUID = UUID.fromString(SampleGattAttributes.myGatt.get(6)); 即可,請改為 UUID ServiceUUID = UUID.fromString("你所要的UUID"); 大致是這樣,試試看吧 是說你的名字真令人懷念XD 我也是剛入行就做BLE的樓主
*****
*****
*****
*****
隔行如隔山 我只能 拍拍拍拍拍啦
好窩好窩~謝謝你!
請問如果想把DeviceControlActivity改成寫在Fragment該如何獲取ScanData的資料呢?
是指說把整個Activity的內容寫在Frgment內嗎? 寫過去用建構子。返回用Interface Reference https://thumbb13555.pixnet.net/blog/post/326868843-interface%26numberpicker
應該說 我不太明白這行是怎麼獲取所選的資料 /**取得所選中的藍芽裝置之相關資料*/ selectedDevice = (ScannedData) getIntent().getSerializableExtra(INTENT_KEY); 方便解釋一下原理嗎?
這是Intent跳頁資訊傳遞.. 來源是你在上篇文章中寫道我少掉的那行,就是RecyclerView點擊回調事件 https://github.com/thumbb13555/BLE_Example/blob/master/app/src/main/java/com/jetec/ble_example/Controller/MainActivity.java 195行處是它的來源 所以請問你是不懂Intent的原理嗎?我可以再另外撰文解釋
沒想到你記得我... 菜雞問問題打擾抱歉 因為我是使用jetpack的導航組件 https://stackoverflow.com/questions/62114590/how-to-share-data-between-fragments-and-activities-in-android 查了一下似乎是類似這種寫法 但是我不知道scannedData的put方法該用哪種? 比方說我嘗試寫成這樣 NavController navController = NavHostFragment.findNavController(TrainerFragment.this); Bundle bundle = new Bundle(); bundle.putSerializable(DeviceControlFragment.INTENT_KEY,(Serializable) selectedDevice); navController.navigate(R.id.action_trainerFragment_to_deviceControlFragment,bundle); 然後取得 /**取得所選中的藍芽裝置之相關資料*/ selectedDevice = (ScannedData) getArguments().getSerializable(INTENT_KEY); 但他會顯示錯誤 ScannedData cannot be cast to java.io.Serializable
喔,這是因為ScaanedData是實體類,而intent要傳實體類的話,就必須讓實體類繼承Serializable介面類 請看這段程式->https://github.com/thumbb13555/BLE_Example/blob/master/app/src/main/java/com/jetec/ble_example/Module/Enitiy/ScannedData.java 實體類有繼承Serializable,所以在intent的putSerializable我這樣才不會出錯 --- 像你們有署名的我大多都會記得啦XD 我偶爾也會有些菜雞問題,別太在意~
原來是多了那個... 感謝
不會,有解決便好
*****
*****
推推~!!厲害 拍拍~~5!
感謝!
專業的好友 週二愉快 拍手+5
謝謝你!
好友超專業!!! 拍拍手+推
謝謝:D
下半場也來幫你拍拍手了 哈哈
哈哈哈~謝謝你!
好強...居然還更新了 感謝大大的專業解說 但又有問題了-.- 想問該如何像nrf Connect那樣設置一個按鈕開關Notify的資料 同時在另一個TextView上按鈕也可獲取Read的資料?
有點看不太懂你的意思 把示意圖寄mail給我
這裡真的是科技業同仁集散地(?
有沒有真的集散地不知道 但是新手集散地肯定是真的XD
只能說太厲害了 我還是看沒有><
XD 這個要很認真看才會懂~ 不過意外得還蠻多人看...(5k點閱了我記得...)
很多code都混在一起,很難速看,應該拉出來的一些工具類可以寫成util,註解寫得很好懂,但是程式碼可讀性普普,建議學些架構,不然這樣有點誤人子弟我覺得,純屬建議參考即可
謝謝建議。
不好意思想問下,我裝了這隻範例程式後點擊有write功能的Characteristic卻沒有任何回應也沒跳出文字輸入框之類的,是我寫入的方式有錯還是有其他問題
為了使範例程式不要太複雜,我並沒有設置那種按下會有對話框的功能 沒有回應的話請用我給的測試程式先測試好你的硬體,會不會有回應是看硬體怎麼寫的
那如果我想發送訊息的話我可以在DeviceInfoActivity內用 mBluetoothLeService.sendValue("我要發送的訊息",characteristic);的方式嗎
對,沒錯
您好,首先感謝您提供詳細的code參考,我想問一個問題就是: 我想在onchildclick裡面修改成: byte[] value = {0x01}; mBluetoothLeService.sendValue(value, info.getCharacteristic()); 這樣是否能成功?
會的,我有寫發byte的範例
您好 切換到 另一個 Activity 連線就斷掉, 應該如何解決,謝謝~~
在跳頁的時候,我有把連線斷開 看一下生命週期,找找看disconnect 在哪裏
Thanks!
BLE常見功能都有實作出來,很實用,謝謝分享
哈囉 不好意思,請問可以跟您要程式的檔案嗎,想要重頭一行一行理解
*****
*****
*****
*****