赞
踩
google官方对BLE蓝牙的解释
简述:API级别:Android 4.3(API 级别 18)引入。低功耗蓝牙区别于“经典蓝牙”。
局限:最多只支持20个字节(后面会展示)。
低功耗蓝牙优势:1.低功耗,使用纽扣电池就可运行数月至数年;2.小体积、低成本;3.与现有的大部分手机、平板电脑和计算机兼容。(百度百科)
1.蓝牙开发模块(如果有现成的模块可以直接进行调试)
2.串口调试工具(文章末尾会给出软件的下载方式)
3.支持BLE蓝牙的Android手机。
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- 仅支持低耗蓝牙 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
搜索设备准备:
1.当前设备是否支持BLE蓝牙功能
2.设备的蓝牙功能是否处于开启状态
3.判断设备的api是否需要开启定位权限
(PS:至于为什么要开启定位权限,这你得问Google了)
3.1GPS是否打开了
3.2是否拥有GPS权限,需要使用GPS才能使用蓝牙设备
这里的蓝牙,定位开关状态,权限获取比较麻烦但是不复杂
代码:
package com.my.mwble; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import android.Manifest; import android.app.Activity; import android.app.AlertDialog; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.view.View; import android.widget.Toast; import com.my.mwble.util.BleUtil; import com.my.mwble.util.GpsUtil; import com.my.mwble.util.LogUtil; import com.my.mwble.util.ToastUtil; /** * Created by Android Studio. * User: mwb * Date: 2020/10/24 0024 * Time: 上午 11:32 * Describe:BLE蓝牙基础 */ public class MainActivity extends AppCompatActivity implements View.OnClickListener { private BluetoothAdapter bluetoothAdapter; private static int REQUEST_ENABLE_BT = 1; // 打开蓝牙页面请求代码 private static final int REQUEST_CODE_ACCESS_COARSE_LOCATION = 1; // 位置权限 private static final int SET_GPS_OPEN_STATE = 2; // 设置GPS是否打开了 private static final int REQUEST_STORY_CODE = 3; // 文件读取权限 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); initDevice(); } private void initView() { findViewById(R.id.btn_seach).setOnClickListener(this); } private void initDevice() { BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); bluetoothAdapter = bluetoothManager.getAdapter(); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_seach: // 搜索蓝牙设备 seach(); break; } } /** * 搜索设备 * 1.当前设备是否支持BLE蓝牙功能 * 2.设备的蓝牙功能是否处于开启状态 * 2.1 没有开启则去开启 * 3.判断设备的api是否需要开启定位权限 * (PS:至于为什么要开启定位权限,这你得问Google了) * 3.1GPS是否打开了 * 3.2是否拥有GPS权限,需要使用GPS才能使用蓝牙设备 */ private void seach() { // 当前的系统版本 < Android 4.3 API=18,目前市面大部分系统都在6.0了...这个判断几乎可以不用写了。可省略 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { ToastUtil.show(this, "当前设备系统版本不支持BLE蓝牙功能!请升级系统版本到4.3以上"); return; } //1. 当前设备是否支持BLE蓝牙设备 if (BleUtil.checkDeviceSupportBleBlueTooth(this)) { // 2.判断蓝牙设备是否打开了 if (checkBlueIsOpen()) { // 3.断设备的api是否需要开启定位权限 checkGPS(); } else { // 没有打开,跳转到系统蓝牙页面 Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); } } else { ToastUtil.show(this, "当前设备不支持BLE蓝牙功能!"); } } /** * GPS是否开启了 */ private void checkGPS() { // 3.1GPS是否打开了 if (GpsUtil.isOPen(this)) { // GPS已经开启了 checkGpsPermission(); } else { // 3.2是否拥有GPS权限,需要使用GPS才能使用蓝牙设备 tipGPSSetting(); } } /** * 蓝牙是否打开了 * * @return true 打开了,false 没有打开 */ private boolean checkBlueIsOpen() { if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) { return false; } else { return true; } } /** * 搜索蓝牙设备 */ private void seachBlueTooth() { ToastUtil.show(this, "开始搜索蓝牙设备"); } /** * 提示需要开启蓝牙 */ private void tipGPSSetting() { AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setTitle("提示"); builder.setMessage("安卓6.0以后使用蓝牙需要开启定位功能,但本应用不会使用到您的位置信息,开始定位只是为了扫描到蓝牙设备。是否确定打开"); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { GpsUtil.openGPS(MainActivity.this, SET_GPS_OPEN_STATE); } }); builder.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ToastUtil.show(MainActivity.this, "您无法使用此功能"); } }); builder.show(); } /** * 蓝牙需要的定位权限 */ private void checkGpsPermission() { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { // 如果当前版本是9.0(包含)以下的版本 if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { String[] strings = {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}; ActivityCompat.requestPermissions(this, strings, REQUEST_CODE_ACCESS_COARSE_LOCATION); } else { seachBlueTooth(); } } else { // 10.0系统 if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, "android.permission.ACCESS_BACKGROUND_LOCATION") != PackageManager.PERMISSION_GRANTED) { String[] strings = {android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_COARSE_LOCATION, "android.permission.ACCESS_BACKGROUND_LOCATION"}; ActivityCompat.requestPermissions(this, strings, REQUEST_CODE_ACCESS_COARSE_LOCATION); } else { seachBlueTooth(); } } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_ENABLE_BT) { // 从蓝牙页面返回了,在检查一次是否打开了 if (checkBlueIsOpen()) { // 蓝牙打开了 seach(); } else { AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setTitle("提示"); builder.setMessage("蓝牙没有打开将无法使用此功能,是否确定打开"); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { seach(); // 再次执行搜索 } }); builder.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ToastUtil.show(MainActivity.this, "您无法使用此功能"); } }); builder.setCancelable(false); builder.show(); } }else if (requestCode == SET_GPS_OPEN_STATE) { // GPS是否打开了 if (GpsUtil.isOPen(this)) { // GPS打开了 checkGpsPermission(); } else { tipGPSSetting(); } } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case REQUEST_CODE_ACCESS_COARSE_LOCATION: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 得到了权限 seachBlueTooth(); } else { AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setTitle("提示"); builder.setMessage("安卓6.0以后使用蓝牙需要开启定位功能,但本应用不会使用到您的位置信息,开启定位只是为了扫描到蓝牙设备。是否确定打开"); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { launchAppDetailsSettings(MainActivity.this); } }); builder.setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { ToastUtil.show(MainActivity.this, "您无法使用此功能"); } }); builder.setCancelable(false); builder.show(); } break; default: super.onRequestPermissionsResult(requestCode, permissions, grantResults); break; } } /** * 跳转权限Activity */ public void launchAppDetailsSettings(Activity activity) { Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.parse("package:" + activity.getPackageName())); if (!isIntentAvailable(this, intent)) { ToastUtil.show(this, "请手动跳转到权限页面,给予权限!"); return; } activity.startActivity(intent); } /** * 意图是否可用 * * @param intent The intent. * @return {@code true}: yes<br>{@code false}: no */ public boolean isIntentAvailable(Activity activity, Intent intent) { return activity .getPackageManager() .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) .size() > 0; } }
看一下效果:GIF太大了,就截几个图吧…
注意:搜索到的设备可能会多次出现需要我们自己进行筛选
关键代码:
/** * 搜索蓝牙设备 * 创建搜索callback 返回扫描到的信息 * 创建定时任务,在指定时间内结束蓝牙扫描,蓝牙扫描是一个很耗电的操作! */ private void seachBlueTooth() { ToastUtil.show(this, "开始搜索蓝牙设备"); mBluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); mBluetoothLeScanner.startScan(null, createScanSetting(), scanCallback); bluetoothAdapter.startDiscovery(); handler.postDelayed(new Runnable() { // 指定时间内停止蓝牙搜索 @Override public void run() { closeSeach(); } }, SCAN_PERIOD); deviceData.clear(); } /** * 回调 */ private ScanCallback scanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { BluetoothDevice device = result.getDevice(); LogUtil.i("name:" + result.getDevice().getName() + ";强度:" + result.getRssi()); if (device != null) { if (deviceData.size() > 0) { if (!deviceData.contains(device)) { // 扫描到会有很多重复的数据,剔除,只添加第一次扫描到的设备 deviceData.add(device); } } else { deviceData.add(device); } adapter.setData(deviceData); } } @Override public void onBatchScanResults(List<ScanResult> results) { super.onBatchScanResults(results); } @Override public void onScanFailed(int errorCode) { super.onScanFailed(errorCode); } };
效果图:
至此我们就得到了扫描到了设备信息了。
概述:每个BLE蓝牙设备都会包含几个服务Service
而每个Service中还包含了多个Characteristics(特征)
他们的关系如下图:
开启通信我们还需要绑定指定Service中的Characteristics(特征)。
至于使用哪个Service或者哪个Characteristics(特征)需要跟你们的硬件开发人员进行沟通。
关键代码:
/** * 绑定蓝牙 * * @param device */ private void bindBlueTooth(BluetoothDevice device) { //连接设备 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mBluetoothGatt = device.connectGatt(this, false, mGattCallback, BluetoothDevice.TRANSPORT_LE); } else { mBluetoothGatt = device.connectGatt(this, false, mGattCallback); } } //定义蓝牙Gatt回调类 public class mWBluetoothGattCallback extends BluetoothGattCallback { //连接状态回调 @Override public void onConnectionStateChange(BluetoothGatt gatt, final int status, final int newState) { super.onConnectionStateChange(gatt, status, newState); // status 用于返回操作是否成功,会返回异常码。 // newState 返回连接状态,如BluetoothProfile#STATE_DISCONNECTED、BluetoothProfile#STATE_CONNECTED runOnUiThread(new Runnable() { @Override public void run() { //操作成功的情况下 if (status == BluetoothGatt.GATT_SUCCESS) { //判断是否连接码 if (newState == BluetoothProfile.STATE_CONNECTED) { runOnUiThread(new Runnable() { @Override public void run() { ToastUtil.show(MainActivity.this, "蓝牙已连接"); LogUtil.i("设备已连接上,开始扫描服务"); // 发现服务 mBluetoothGatt.discoverServices(); } }); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { //判断是否断开连接码 ToastUtil.show(MainActivity.this, "连接已断开"); } } else { //异常码 // 重连次数不大于最大重连次数 if (reConnectionNum < maxConnectionNum) { // 重连次数自增 reConnectionNum++; LogUtil.i("重新连接:" + reConnectionNum); // 连接设备 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mBluetoothGatt = mBluetoothDevice.connectGatt(MainActivity.this, false, mGattCallback, BluetoothDevice.TRANSPORT_LE); } else { mBluetoothGatt = mBluetoothDevice.connectGatt(MainActivity.this, false, mGattCallback); } } else { // 断开连接,失败回调 ToastUtil.show(MainActivity.this, "蓝牙连接失败,建议重启APP,或者重启蓝牙,或重启设备"); closeBLE(); } } } }); } //服务发现回调 @Override public void onServicesDiscovered(final BluetoothGatt gatt, int status) { super.onServicesDiscovered(gatt, status); if (status == BluetoothGatt.GATT_SUCCESS) { runOnUiThread(new Runnable() { @Override public void run() { LogUtil.i("mmmm:" + mBluetoothGatt.getServices().size()); for (int i = 0; i < mBluetoothGatt.getServices().size(); i++) { LogUtil.i("mmmm service:" + mBluetoothGatt.getServices().get(i).getUuid()); for (int k = 0; k < mBluetoothGatt.getServices().get(i).getCharacteristics().size(); k++) { LogUtil.i("mmmm Characteristic:" + mBluetoothGatt.getServices().get(i).getCharacteristics().get(k).getUuid()); } } } }); } } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { super.onCharacteristicRead(gatt, characteristic, status); } //特征写入回调 @Override public void onCharacteristicWrite(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) { super.onCharacteristicWrite(gatt, characteristic, status); } //外设特征值改变回调 @Override public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { super.onCharacteristicChanged(gatt, characteristic); } //描述写入回调 @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { super.onDescriptorWrite(gatt, descriptor, status); LogUtil.i("开启监听成功"); } }
Service的Uuid,和Characteristics(特征)的Uuid
修改 onServicesDiscovered中的代码:
//服务发现回调 @Override public void onServicesDiscovered(final BluetoothGatt gatt, int status) { super.onServicesDiscovered(gatt, status); if (status == BluetoothGatt.GATT_SUCCESS) { runOnUiThread(new Runnable() { @Override public void run() { // LogUtil.i("mmmm:" + mBluetoothGatt.getServices().size()); // for (int i = 0; i < mBluetoothGatt.getServices().size(); i++) { // LogUtil.i("mmmm service:" + mBluetoothGatt.getServices().get(i).getUuid()); // // // for (int k = 0; k < mBluetoothGatt.getServices().get(i).getCharacteristics().size(); k++) { // LogUtil.i("mmmm Characteristic:" + mBluetoothGatt.getServices().get(i).getCharacteristics().get(k).getUuid()); // } // // } //获取指定uuid的service BluetoothGattService gattService = mBluetoothGatt.getService(UUID.fromString(UUDI_1)); // bluetoothGattServiceList.add(gattService); //获取到特定的服务不为空 if (gattService != null) { LogUtil.i("获取服务成功!"); BluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(UUID.fromString(CHARACTERISTIC_UUID_1)); mGattCharacteristic = gattCharacteristic; if (gattCharacteristic != null) { LogUtil.i("获取特征成功!"); boolean isEnableNotification = mBluetoothGatt.setCharacteristicNotification(gattCharacteristic, true); if (isEnableNotification) { LogUtil.i("开启通知成功!"); //通过GATt实体类将,特征值写入到外设中。 mBluetoothGatt.writeCharacteristic(gattCharacteristic); //如果只是需要读取外设的特征值: //通过Gatt对象读取特定特征(Characteristic)的特征值 mBluetoothGatt.readCharacteristic(gattCharacteristic); } else { LogUtil.i("开启通知失败!"); } } else { LogUtil.i("获取特征失败!"); } } else { //获取特定服务失败 LogUtil.i("获取服务失败!"); } } }); } }
修改onCharacteristicChanged中的代码
//外设特征值改变回调
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
final byte[] value = characteristic.getValue();
runOnUiThread(new Runnable() {
@Override
public void run() {
// value为设备发送的数据,根据数据协议进行解析
LogUtil.i("原始数据:" + new String(characteristic.getValue()));
LogUtil.i("设备发送数据:" + DigitalTrans.byte2hex(value)); // 这是一个byte转16进制的工具类,后面会给完整的代码,所以现在不用纠结
}
});
}
再次连接设备,连接成功后我们来进行测试
数据接收成功。需要注意的地方是,接收到的数据是byte类型的,使用的时候需要自己进行转化。
至此接收数据完成。
修改代码:
/**
* 发送数据
* 将输入的16进制转化为byte发送
*/
private void sendMsg(String msg) {
if (null == mGattCharacteristic || null == mBluetoothGatt) {
ToastUtil.show(MainActivity.this, "请先连接蓝牙设备");
return;
}
mGattCharacteristic.setValue(NumUtil.hexString2Bytes(msg));
mBluetoothGatt.writeCharacteristic(mGattCharacteristic);
}
关闭蓝牙:
/**
* 关闭BLE蓝牙连接
*/
public void closeBLE() {
if (mBluetoothGatt == null) {
return;
}
mBluetoothGatt.disconnect();
mBluetoothGatt.close();
mBluetoothGatt = null;
ToastUtil.show(this, "蓝牙已断开");
}
蓝牙发送数据大于20个的问题:
这明明是40个数据才拆分了啊,你这是不是欺负老实人吗?
请听我狡辩:
从XCOM串口工具中我勾选了16进制发送,Byte是从0-255的无符号类型。16进制的最大表示FF = 255, 所以两个16进制代表一个Byte,在实际的开发中我们用到的也会是16进制根据规定的协议进行沟通。
可以看到数据被拆分了,如果数据大于20个字节需要进行拼包操作。
如果有时间的话我以后会发拼包的功能实现。
=2020/11/25=
新增内容:
手机端向BLE蓝牙发送数据最大也是20个字节。
测试如下:
手机端写入数据如下图:
我们再来看看接收到的参数:
如果手机发送的信息超过了20个字节你就该使用拼包的操作了。
,如果有什么问题,请留言进行沟通。
代码和工具类奉上:
XCOM串口调试工具:链接:https://pan.baidu.com/s/1i-9W31CjXd-551mqphi_hg
提取码:95vv
代码:https://github.com/No98K/MWBle
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。