学习蓝牙低功耗的开发过程,要达到的效果是——利用两台Android手机,通过BLE4.0进行通信,可以发送和接收数据。
- 其中一台Android手机T模拟发出广播,作为BLE设备(周边设备),这个BLE设备在生产环境中就是我们用到的气体检测传感器、智能手环、体重秤、血压计等等;
- 另一台Android手机B,作为中央设备,搜索手机T发出的广播并连接;
- 手机B可以接收手机T的数据,也可以发送数据给手机T;
- 当然手机T也可以通过通知发送数据给手机B。
1、先通过下面两篇文章理解下,
https://blog.youkuaiyun.com/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.youkuaiyun.com/fu908323236/article/details/76208997
https://blog.youkuaiyun.com/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.youkuaiyun.com/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稍微有点复杂(其实就是分了几个包,逻辑更清楚一些)就不贴出了。感兴趣的小伙伴可以去下载,可以直接运行查看效果的。
优快云下载:
https://download.youkuaiyun.com/download/agg_bin/11045928,https://download.youkuaiyun.com/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.youkuaiyun.com/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.youkuaiyun.com/download/agg_bin/11045928
https://download.youkuaiyun.com/download/agg_bin/11045943
或者
https://github.com/swu-agg/BLESend
https://github.com/swu-agg/BLEReceive