學習藍牙低功耗的開發過程,要達到的效果是——利用兩臺Android手機,通過BLE4.0進行通信,可以發送和接收數據。
- 其中一臺Android手機T模擬發出廣播,作爲BLE設備(周邊設備),這個BLE設備在生產環境中就是我們用到的氣體檢測傳感器、智能手環、體重秤、血壓計等等;
- 另一臺Android手機B,作爲中央設備,搜索手機T發出的廣播並連接;
- 手機B可以接收手機T的數據,也可以發送數據給手機T;
- 當然手機T也可以通過通知發送數據給手機B。
1、先通過下面兩篇文章理解下,
https://blog.csdn.net/shunfa888/article/details/80140475
https://www.cnblogs.com/asam/p/8676339.html
BLE是什麼?低功耗藍牙協議棧包括什麼?以及藍牙的基礎知識等。
2、然後配合Android提供的官方文檔,瞭解藍牙相關知識,
https://developer.android.com/guide/topics/connectivity/bluetooth
https://developer.android.com/guide/topics/connectivity/bluetooth-le
這個時候可能已經花了至少2個學時學習BLE4.0,已經有了一定的認識,但是還別急着擼代碼,因爲我們可能只是知道其中的冰山一角。
3、接下來需要更進一步,瞭解開發的具體步驟,下面兩篇博客可以查閱一下,
https://blog.csdn.net/fu908323236/article/details/76208997
https://blog.csdn.net/u011371324/article/details/80568230
在學習BLE4.0基礎知識的基礎上,通過代碼一步一步的走進去,睜眼看世界。
4、這個時候我們大概已經有60%的知識點了,就可以有選擇了。其一,再專研BLE的實現代碼,自己寫一個模擬發出廣播的App給手機T,寫一個可以接收發送數據的App給手機B,實現BLE4.0的通信;其二,通過已有開源工具,實現低功耗藍牙的通信,比如這個開源項目就可使用:https://github.com/xiaoyaoyou1212/BLE。
5、我相信大多數人還是會再深研BLE,所以有必要更系統一點學習一下,
https://www.jianshu.com/u/4690d1fc40fe分爲四個部分介紹:
Android BLE4.0(基本知識)、Android BLE4.0(設備搜索)、Android BLE4.0(設備連接)、Android BLE4.0(藍牙通信)。
https://blog.csdn.net/likebamboo分爲六個部分:
Bluetooth LE(低功耗藍牙) - 從第一部分到第六部分
6、通過上面的學習和實踐,基本能設計出BLE相關代碼了,但是可能還跑不通,比如說掃描不到藍牙啊?連接藍牙後通信不成功呀?等等問題。這個時候就需要下面這個博客了,老實說,最後我的代碼就很大程度上跟着它走了,並且翻看它的文章次數用雙手是數不清的。
好了,不賣關子了,直奔主題。http://a1anwang.com/post-36.html這篇文章開發出一個App裝在手機T上使用,模擬發送廣播;http://a1anwang.com/post-47.html這篇文章開發出的App裝在手機B上作爲中央設備使用;當然該博客的其他文章對我幫助也很大,比如http://a1anwang.com/post-17.html等等。
7、開發完了兩個App,再回過頭來看看之前參閱過的文章,收穫又不一樣了,於是纔有了這篇文章的出現。
在這裏,我貼出模擬BLE設備發廣播的App源碼,而由於中央設備App稍微有點複雜(其實就是分了幾個包,邏輯更清楚一些)就不貼出了。感興趣的小夥伴可以去下載,可以直接運行查看效果的。
CSDN下載:
https://download.csdn.net/download/agg_bin/11045928,https://download.csdn.net/download/agg_bin/11045943
GitHub開源項目下載:
https://github.com/swu-agg/BLESend,https://github.com/swu-agg/BLEReceive
模擬BLE設備發廣播的App源碼如下:
1、Java文件BLEBroadcastActivity.java:
/** * <pre> * author : Agg * blog : https://blog.csdn.net/Agg_bin * time : 2019/03/15 * desc : BLE模擬設備,周邊 * reference : * </pre> */ public class BLEBroadcastActivity extends RxAppCompatActivity { private static final String TAG = BLEBroadcastActivity.class.getSimpleName(); private static final ParcelUuid PARCEL_UUID_1 = ParcelUuid.fromString("0000ccc0-0000-1000-8000-00805f9b34fb"); private static final ParcelUuid PARCEL_UUID_2 = ParcelUuid.fromString("0000bbb0-0000-1000-8000-00805f9b34fb"); private static final UUID SERVICE_UUID_1 = UUID.fromString("0000ccc0-0000-1000-8000-00805f9b34fb"); private static final UUID SERVICE_UUID_2 = UUID.fromString("0000bbb0-0000-1000-8000-00805f9b34fb"); private static final UUID CHARACTERISTIC_UUID_1 = UUID.fromString("0000ccc1-0000-1000-8000-00805f9b34fb"); private static final UUID CHARACTERISTIC_UUID_2 = UUID.fromString("0000ccc2-0000-1000-8000-00805f9b34fb"); private static final UUID CHARACTERISTIC_UUID_3 = UUID.fromString("0000bbb1-0000-1000-8000-00805f9b34fb"); private static final UUID DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); private static final byte[] BROADCAST_DATA = {0x12, 0x34, 0x56, 0x78}; private static final int MANUFACTURER_ID = 0xACAC; private BluetoothManager bluetoothManager; private BluetoothGattServer bluetoothGattServer; private BluetoothLeAdvertiser bluetoothLeAdvertiser; private List<BluetoothDevice> bluetoothDeviceList = new ArrayList<>(); // 建立通知關係的device隊列,當發送通知時,通知所有設備。 @BindView(R.id.et_info) EditText etInfo; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_blebroadcast); ButterKnife.bind(this); etInfo.setImeOptions(EditorInfo.IME_ACTION_SEND); etInfo.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_SEND || (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) { sendInfo(etInfo.getText().toString().trim()); hideSoftInput(); etInfo.setText(""); return true; } return false; }); askPermission(); } @SuppressLint("CheckResult") private void askPermission() { if (Build.VERSION.SDK_INT >= 23) { new RxPermissions(this).request(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}) .compose(bindUntilEvent(ActivityEvent.DESTROY)) .take(1) .observeOn(AndroidSchedulers.mainThread()) .subscribe(aBoolean -> { if (aBoolean) { isSupportBluetooth4(); } else { Toast.makeText(BLEBroadcastActivity.this, "未授予模糊定位權限", Toast.LENGTH_SHORT).show(); finish(); } }); } else { isSupportBluetooth4(); } } private void isSupportBluetooth4() { if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { Toast.makeText(BLEBroadcastActivity.this, "藍牙不支持BLE", Toast.LENGTH_SHORT).show(); finish(); } else if (!isOpenBluetooth()) { Toast.makeText(BLEBroadcastActivity.this, "此硬件平臺不支持藍牙", Toast.LENGTH_SHORT).show(); finish(); } } private boolean isOpenBluetooth() { bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (bluetoothAdapter == null) { return false; } boolean enable = bluetoothAdapter.enable();// 自動打開藍牙 if (!enable) { Toast.makeText(this, "請打開藍牙", Toast.LENGTH_SHORT).show(); finish(); } else { etInfo.postDelayed(this::setService, 1500); // 等待藍牙開啓後再使用(預計1.5秒以上就可以) } bluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser(); return true; } private void setService() { bluetoothGattServer = bluetoothManager.openGattServer(this, bluetoothGattServerCallback); // 可寫ccc1 BluetoothGattCharacteristic bluetoothGattCharacteristic1 = new BluetoothGattCharacteristic(CHARACTERISTIC_UUID_1, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE); // 可讀ccc2 BluetoothGattCharacteristic bluetoothGattCharacteristic2 = new BluetoothGattCharacteristic(CHARACTERISTIC_UUID_2, BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_READ); // service1ccc0 BluetoothGattService service1 = new BluetoothGattService(SERVICE_UUID_1, BluetoothGattService.SERVICE_TYPE_PRIMARY); service1.addCharacteristic(bluetoothGattCharacteristic1); service1.addCharacteristic(bluetoothGattCharacteristic2); bluetoothGattServer.addService(service1); // 可讀可寫可通知bbb1 BluetoothGattCharacteristic characteristic3 = new BluetoothGattCharacteristic(CHARACTERISTIC_UUID_3, BluetoothGattCharacteristic.PROPERTY_NOTIFY | BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_WRITE | BluetoothGattCharacteristic.PERMISSION_READ); characteristic3.addDescriptor(new BluetoothGattDescriptor(DESCRIPTOR_UUID, BluetoothGattDescriptor.PERMISSION_WRITE)); // service2bbb0 final BluetoothGattService service2 = new BluetoothGattService(SERVICE_UUID_2, BluetoothGattService.SERVICE_TYPE_PRIMARY); service2.addCharacteristic(characteristic3); new Thread(() -> { try { Thread.sleep(1000); bluetoothGattServer.addService(service2); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } private final BluetoothGattServerCallback bluetoothGattServerCallback = new BluetoothGattServerCallback() { @Override public void onConnectionStateChange(BluetoothDevice device, int status, int newState) { super.onConnectionStateChange(device, status, newState); // 這個device是中央設備, mac地址會 因爲 中央(手機)藍牙重啓而變化 if (newState == BluetoothProfile.STATE_CONNECTED) { Log.i(TAG, "連接成功"); Log.i(TAG, "onConnectionStateChange: " + status + " newState:" + newState + " deviceName:" + device.getName() + " mac:" + device.getAddress()); } } @Override public void onServiceAdded(int status, BluetoothGattService service) { super.onServiceAdded(status, service); Log.i(TAG, " onServiceAdded status:" + status + " service:" + service.getUuid().toString()); } @Override public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) { super.onCharacteristicReadRequest(device, requestId, offset, characteristic); Log.i(TAG, " onCharacteristicReadRequest requestId:" + requestId + " offset:" + offset + " characteristic:" + characteristic.getUuid().toString()); bluetoothGattServer.sendResponse(device, requestId, 0, offset, "agg coming".getBytes()); } @Override public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value); Log.e(TAG, " onCharacteristicWriteRequest requestId:" + requestId + " preparedWrite:" + preparedWrite + " responseNeeded:" + responseNeeded + " offset:" + offset + " value:" + new String(value) + " characteristic:" + characteristic.getUuid().toString()); runOnUiThread(() -> Toast.makeText(BLEBroadcastActivity.this, "收到請求:" + new String(value), Toast.LENGTH_SHORT).show()); bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value); } @Override public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) { super.onDescriptorReadRequest(device, requestId, offset, descriptor); Log.i(TAG, " onCharacteristicReadRequest requestId:" + requestId + " offset:" + offset + " descriptor:" + descriptor.getUuid().toString()); } int i = 0; @Override public void onDescriptorWriteRequest(final BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) { super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value); Log.i(TAG, " onDescriptorWriteRequest requestId:" + requestId + " preparedWrite:" + preparedWrite + " responseNeeded:" + responseNeeded + " offset:" + offset + " value:" + toHexString(value) + " characteristic:" + descriptor.getUuid().toString()); bluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value); // 添加到通知列表隊列:添加前先移除之前添加的! for (BluetoothDevice bluetoothDevice : bluetoothDeviceList) { if (bluetoothDevice.getAddress().equals(device.getAddress())) { bluetoothDeviceList.remove(bluetoothDevice); break; } } bluetoothDeviceList.add(device); // 循環通知3個數據 new Thread(() -> { while (i < 3) { try { Thread.sleep(1000); notifyData(device, ("通知數據" + i++).getBytes(), false); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } @Override public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) { super.onExecuteWrite(device, requestId, execute); Log.i(TAG, " onExecuteWrite requestId:" + requestId + " execute:" + execute); } @Override public void onNotificationSent(BluetoothDevice device, int status) { super.onNotificationSent(device, status); Log.i(TAG, " onNotificationSent status:" + status); } }; private AdvertiseCallback advertiseCallback = new AdvertiseCallback() { @Override public void onStartSuccess(AdvertiseSettings settingsInEffect) { super.onStartSuccess(settingsInEffect); Toast.makeText(BLEBroadcastActivity.this, "開啓BLE廣播成功", Toast.LENGTH_SHORT).show(); } @Override public void onStartFailure(int errorCode) { super.onStartFailure(errorCode); Toast.makeText(BLEBroadcastActivity.this, "開啓BLE廣播失敗,errorCode:" + errorCode, Toast.LENGTH_SHORT).show(); } }; private void sendInfo(String data) { Log.e(TAG, "sendInfo: " + data + ",bluetoothDeviceList.size():" + bluetoothDeviceList.size()); try { for (BluetoothDevice bluetoothDevice : bluetoothDeviceList) { boolean notifyData = notifyData(bluetoothDevice, data.getBytes(), false); Toast.makeText(this, "通知數據\"" + data + "\"給" + bluetoothDevice.getAddress() + "--------" + notifyData, Toast.LENGTH_LONG).show(); } } catch (Exception e) { Toast.makeText(this, "請打開廣播連接通信", Toast.LENGTH_SHORT).show(); } } private boolean notifyData(final BluetoothDevice device, byte[] value, final boolean confirm) { BluetoothGattCharacteristic characteristic = null; for (BluetoothGattService service : bluetoothGattServer.getServices()) { for (BluetoothGattCharacteristic mCharacteristic : service.getCharacteristics()) { if (mCharacteristic.getUuid().equals(CHARACTERISTIC_UUID_3)) { characteristic = mCharacteristic; break; } } } if (characteristic != null) { characteristic.setValue(value); return bluetoothGattServer.notifyCharacteristicChanged(device, characteristic, confirm); } return false; } private void hideSoftInput() { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); assert imm != null; imm.hideSoftInputFromWindow(etInfo.getWindowToken(), 0); // 強制隱藏鍵盤 } private AdvertiseSettings createAdvertiseSettings(boolean connectable, int timeoutMillis) { // 設置廣播的模式,低功耗,平衡和低延遲三種模式:對應 AdvertiseSettings.ADVERTISE_MODE_LOW_POWER ,ADVERTISE_MODE_BALANCED ,ADVERTISE_MODE_LOW_LATENCY // 從左右到右,廣播的間隔會越來越短 return new AdvertiseSettings.Builder().setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) // 設置是否可以連接。廣播分爲可連接廣播和不可連接廣播,一般不可連接廣播應用在iBeacon設備上,這樣APP無法連接上iBeacon設備 .setConnectable(connectable) // 設置廣播的信號強度,從左到右分別表示強度越來越強.。 // 常量有AdvertiseSettings.ADVERTISE_TX_POWER_ULTRA_LOW,ADVERTISE_TX_POWER_LOW,ADVERTISE_TX_POWER_MEDIUM,ADVERTISE_TX_POWER_HIGH // 舉例:當設置爲ADVERTISE_TX_POWER_ULTRA_LOW時,手機1和手機2放在一起,手機2掃描到的rssi信號強度爲-56左右; // 當設置爲ADVERTISE_TX_POWER_HIGH 時, 掃描到的信號強度爲-33左右,信號強度越大,表示手機和設備靠的越近。 .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) // 設置廣播的最長時間,最大值爲常量AdvertiseSettings.LIMITED_ADVERTISING_MAX_MILLIS = 180 * 1000; 180秒 // 設爲0時,代表無時間限制會一直廣播 .setTimeout(timeoutMillis) .build(); } private AdvertiseData createAdvertiseData(byte[] broadcastData) { return new AdvertiseData.Builder() .addServiceUuid(PARCEL_UUID_1) .addServiceUuid(PARCEL_UUID_2) .addServiceData(PARCEL_UUID_1, new byte[]{0x33, 0x33, 0x33, 0x33}) .addManufacturerData(MANUFACTURER_ID, broadcastData) .build(); } public static String toHexString(byte[] byteArray) { if (byteArray == null || byteArray.length < 1) return ""; final StringBuilder hexString = new StringBuilder(); for (byte aByteArray : byteArray) { if ((aByteArray & 0xff) < 0x10)//0~F前面不零 hexString.append("0"); hexString.append(Integer.toHexString(0xFF & aByteArray)); } return hexString.toString().toLowerCase(); } @OnClick(R.id.bt_open_broadcast) public void openBroadcast() { if (bluetoothLeAdvertiser != null) { bluetoothLeAdvertiser.stopAdvertising(advertiseCallback); bluetoothLeAdvertiser.startAdvertising(createAdvertiseSettings(true, 0), createAdvertiseData(BROADCAST_DATA), advertiseCallback); } } @Override protected void onDestroy() { super.onDestroy(); if (bluetoothLeAdvertiser != null) { bluetoothLeAdvertiser.stopAdvertising(advertiseCallback); bluetoothLeAdvertiser = null; } } }
2、佈局文件activity_blebroadcast.xml:
<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <EditText android:id="@+id/et_info" android:layout_width="match_parent" android:layout_height="29dp" android:layout_marginEnd="8dp" android:layout_marginStart="8dp" android:layout_marginTop="10dp" android:background="@drawable/et_round_bg_search" android:ems="10" android:gravity="center_vertical" android:hint="@string/et_info" android:inputType="text" android:paddingEnd="10dp" android:paddingStart="12dp" android:textColor="@color/colorPrimary" android:textColorHint="#999999" android:textSize="12sp" /> <Button android:id="@+id/bt_open_broadcast" android:layout_width="100dp" android:layout_height="60dp" android:layout_gravity="center" android:layout_marginBottom="20dp" android:background="@drawable/et_round_bg_search" android:contentDescription="@null" android:text="@string/open_broadcast" android:textColor="@color/colorPrimary" android:textSize="20sp" /> </merge>
3、AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.hy.ble.send"> <uses-permission android:name="android.permission.BLUETOOTH" /> <!--使用藍牙所需要的權限--> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <!--使用掃描和設置藍牙的權限(申明這一個權限必須申明上面一個權限)--> <!--在Android6.0及以上,還需要打開模糊定位的權限。如果應用沒有位置權限,藍牙掃描功能不能使用(其它藍牙操作例如連接藍牙設備和寫入數據不受影響)--> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-feature android:name="android.hardware.location.gps" /><!--在Android5.0之前,是默認申請GPS硬件功能的。而在Android5.0之後,需要在manifest 中申明GPS硬件模塊功能的使用--> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /><!--App只支持 BLE--> <application android:allowBackup="false" android:icon="@mipmap/expression_normal" android:label="@string/app_name" android:roundIcon="@mipmap/expression_normal" android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> <activity android:name=".BLEBroadcastActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
4、app的build.gradle
apply plugin: 'com.android.application' android { compileSdkVersion 27 defaultConfig { applicationId "com.hy.ble.send" minSdkVersion 21 targetSdkVersion 27 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } // Lambda expressions are not supported at language level '1.7' // Java 的版本配置 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' // rxpermissions implementation 'com.github.tbruyelle:rxpermissions:0.10.2' // recyclerview implementation 'com.android.support:recyclerview-v7:27.1.1' // butterknife implementation 'com.jakewharton:butterknife:8.4.0' annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0' // rxlifecycle implementation 'com.trello.rxlifecycle2:rxlifecycle-components:2.1.0' }
5、其他的values文件夾或者drawable就不列出來了,需要的可以去下載
https://download.csdn.net/download/agg_bin/11045928
https://download.csdn.net/download/agg_bin/11045943
或者
https://github.com/swu-agg/BLESend
https://github.com/swu-agg/BLEReceive