最近,公司有一个项目时关于手机蓝牙和硬件蓝牙相互通信的需求。基于之前很久没有学习硬件的知识,这次记录下来,以备下次需要时使用。
一、初识BLE蓝牙:
GATT:
Server:
Characteristic
二、蓝牙开发流程:
三、android BLE API简介:
BluetoothAdapter
BluetoothDevice
BluetoothGatt
BluetoothGattService
BluetoothGattCharacteristic
四、BLE与经典蓝牙的区别:
五、功能介绍:
六、具体代码使用:
Android 4.3(API Level 18)开始引入Bluetooth Low Energy(BLE,低功耗蓝牙)的核心功能并提供了相应的 API, 应用程序通过这些 API 扫描蓝牙设备、查询 services、读写设备的 characteristics(属性特征)等操作。
Android BLE 使用的蓝牙协议是 GATT 协议,有关该协议的详细内容可以参见蓝牙官方文档连接1。以下我引用一张官网的图来大概说明 Android 开发中我们需要了解的一些 Bluetooth Low Energy 的专业术语。
GATT(Generic Attribute Profile),描述了一种使用ATT的服务框架
该框架定义了服务(Server)和服务属性(characteristic)的过程(Procedure)及格式
Procedure定义了characteristic的发现、读、写、通知(Notifing)、指示(Indicating)
及配置characteristic的广播
GATT中最上层是Profile,Profile由一个或多个服务(Service)组成 服务是由Characteristics组成,或是其他服务的引用(Reference) Characteristic包含一个值(Value),可能包含该Value的相关信息。
一个低功耗蓝牙设备可以定义许多 Service, Service 可以理解为一个功能的集合。设备中每一个不同的 Service 都有一个 128 bit 的 UUID 作为这个 Service 的独立标志。蓝牙核心规范制定了两种不同的UUID,一种是基本的UUID,一种是代替基本UUID的16位UUID。所有的蓝牙技术联盟定义UUID共用了一个基本的UUID:
0x0000xxxx-0000-1000-8000-00805F9B34FB
为了进一步简化基本UUID,每一个蓝牙技术联盟定义的属性有一个唯一的16位UUID,以代替上面的基本UUID的‘x’部分。例如,心率测量特性使用0X2A37作为它的16位UUID,因此它完整的128位UUID为:
0x00002A37-0000-1000-8000-00805F9B34FB
在 Service 下面,又包括了许多的独立数据项,我们把这些独立的数据项称作 Characteristic。同样的,每一个 Characteristic 也有一个唯一的 UUID 作为标识符。在 Android 开发中,建立蓝牙连接后,我们说的通过蓝牙发送数据给外围设备就是往这些 Characteristic 中的 Value 字段写入数据;外围设备发送数据给手机就是监听这些 Charateristic 中的 Value 字段有没有变化,如果发生了变化,手机的 BLE API 就会收到一个监听的回调。
BluetoothAdapter 拥有基本的蓝牙操作,例如开启蓝牙扫描,使用已知的 MAC 地址 (BluetoothAdapter getRemoteDevice)实例化一个 BluetoothDevice 用于连接蓝牙设备的操作等等。
代表一个远程蓝牙设备。这个类可以让你连接所代表的蓝牙设备或者获取一些有关它的信息,例如它的名字,地址和绑定状态等等。
这个类提供了 Bluetooth GATT 的基本功能。例如重新连接蓝牙设备,发现蓝牙设备的 Service 等等。
这一个类通过 BluetoothGatt getService 获得,如果当前服务不可见那么将返回一个 null。这一个类对应上面说过的 Service。我们可以通过这个类的 getCharacteristic(UUID uuid) 进一步获取 Characteristic 实现蓝牙数据的双向传输。
这个类对应上面提到的 Characteristic。通过这个类定义需要往外围设备写入的数据和读取外围设备发送过来的数据。
咱们现在的蓝牙实际上分为了三类:单模、双模和经典。那么,最官方的蓝牙版本称呼就是,单模蓝牙、双模蓝牙和经典蓝牙。
在这其中,最前沿的当属单模蓝牙了,也就是低功耗蓝牙。这个蓝牙标准和经典蓝牙区别极大,在最初甚至考虑过加入WIFI阵营,但是因为蓝牙阵营这边条件较为优厚(比如授权费用极低)才并入了蓝牙标准。
基本概要:
蓝牙3.0以前的是传统蓝牙,4.0以后的是低功耗蓝牙,Android蓝牙在不同手机系统版本也有不同的蓝牙,当然也有不一样的调用方法。android4.3(API18)版本以下的对应的是传统蓝牙,在扫描的时候等待时间会比较长。android4.3以上的是低功耗蓝牙,但是android4.3版本到5.0版本的调用方法和android5.0以上的调用方法是不一样的。android用传统的蓝牙连接方式是无法和低功耗蓝牙模块建立通信通道的,因为通信的协议是不一样的。低功耗蓝牙是用GATT这种属性传输协议,而传统蓝牙则是通过Socket的方式进行数据的传输。
android蓝牙权限在6.0以上增加了一个模糊定位的权限,不开启部分手机无法进行扫描蓝牙发出的广播。
经典蓝牙:经典蓝牙设备发现其它经典蓝牙设备的方式是调用BluetoothAdapter的startDiscovery()方法。蓝牙最初的设计意图,是打电话放音乐。3.0版本以下的蓝牙,都称为“经典蓝牙”。功耗高、传输数据量大、传输距离只有10米。
低功耗蓝牙:低功耗蓝牙中则有一个主设备(Central)和从设备(Peripheral,也叫外围设备)的概念。主设备作为发现方,调用发现设备的方法,通过BluetoothAdapter的startLeScan()方法实现。从设备则作为被发现方,发出广播,以供发现。同样,这个startLeScan()方法也仅能够发现低功耗蓝牙从设备。通常说的蓝牙4.0(及以上版本)。低功耗,数据量小,距离50米左右。
使用SeekBar:默认-50db,用户拖动到某个进度蓝牙搜索范围就在某个范围内。例如 > -60db内的范围设备都能搜索出。
使用BEL蓝牙:当用户不设置范围时默认为-50db,贴近某个标签开始连接并且开始传输数据。
使用Hawk依赖保存数据 :记录seekBar的进度。
佳姝1:数据保存与获取等同于SharedPreferences
implementation 'com.orhanobut:hawk:2.0.1'
package com.dintech.graduation.util;
import com.orhanobut.hawk.Hawk;
public class PreferencesUtil {
public staticvoid put(String key, T value) {
Hawk.put(key, value);
}
public staticT get(String key, T defalutValue) {
return Hawk.get(key, defalutValue);
}
public void removeAll(){
Hawk.deleteAll();
}
}
佳姝1:添加蓝牙权限
佳姝2:开启蓝牙:
private static final long SCAN_PERIOD = 8000; private Handler mHandler = new Handler(); private ArrayList
mDeviceList = new ArrayList<>(); private BluetoothAdapter mBluetoothAdapter; Boolean isScanning = false; private Integer rssiKey; private BluetoothGatt bluetoothGatt; private BluetoothDevice device; private String address; private ProgressDialog progressDialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); if (!mBluetoothAdapter.isEnabled()) mBluetoothAdapter.enable(); rssiKey = PreferencesUtil.get("RssiKey", -50); }
佳姝3:蓝牙扫描及连接:
signal.setOnClickListener(v -> { if (isBluetoothValid()) { //判断设备是否支持Ble蓝牙 if (isBluetoothOpen()) { mDeviceList.clear(); //蓝牙已经打开,则开始进行蓝牙搜索 scanDevice(item); } else { //如果蓝牙没有打开,则弹出Dialog窗口提示用户进行打开,此处可以使用系统窗口或者自定义窗口 startActivity(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)); //打开蓝牙系统提示窗口 } } //在刷新数据时使用进度条等待 progressDialog = new ProgressDialog(MainActivity.this); progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); progressDialog.setCancelable(false); progressDialog.setCanceledOnTouchOutside(false); progressDialog.setMessage(getResources().getString(R.string.LOADING_IN)); progressDialog.show(); }); /** * 判断是否支持蓝牙设备 * * @return */ public boolean isBluetoothValid() { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR1 || !this.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { return false; } BluetoothManager bluetoothManager = (BluetoothManager) this.getSystemService(Context.BLUETOOTH_SERVICE); mBluetoothAdapter = bluetoothManager.getAdapter(); if (mBluetoothAdapter == null) { return false; } return true; } /** * 判断蓝牙是否打开 * * @return */ private boolean isBluetoothOpen() { // BluetoothManager bManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); // BluetoothAdapter mAdapter = bManager.getAdapter(); return mBluetoothAdapter.isEnabled(); }
/** * 蓝牙扫描 */ public void scanDevice(Paint item) { if (isScanning) { return; } mHandler.postDelayed(() -> { if (!isScanning) { return; } mBluetoothAdapter.stopLeScan(mLeScanCallback); isScanning = false; Mapmap = new HashMap(); Map map1 = new HashMap(); if (mDeviceList.size() > 0) { for (int i = 0; i < mDeviceList.size(); i++) { Integer rssi = mDeviceList.get(i).getRssi(); String address = mDeviceList.get(i).getAddress(); map.put(address, (map.get(address) == null) ? 1 : map.get(address) + 1); map1.put(address, (map1.get(address) == null) ? rssi : map1.get(address) + rssi); } ArrayList meanList = new ArrayList<>(); for (String key : map.keySet()) { BlueDevice blueDevice = new BlueDevice(); Integer count = map.get(key); Integer sum = map1.get(key); int mean = sum / count; blueDevice.setRssi(mean); blueDevice.setAddress(key); meanList.add(blueDevice); Log.e("TAG", "key地址 : " + key + " value次数 : " + count + " 平均值:" + mean); Log.e("TAG", "key地址 : " + key + " value1Rssi总和 : " + sum + " 平均值:" + mean); } Collections.sort(meanList); address = meanList.get(0).getAddress(); Bleep.getInstance().connect(address).done(device -> { syncScreen(device.getAddress(), item); progressDialog.dismiss(); }).enqueue(); } else { Toast.makeText(MainActivity.this, "范围内没有可连接设备", Toast.LENGTH_SHORT).show(); progressDialog.dismiss(); mPopupWindow.dismiss(); } }, Conf.SCAN_PERIOD); mBluetoothAdapter.startLeScan(mLeScanCallback); isScanning = true; } @Override public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) { // TODO Auto-generated method stub int type = 0; String deviceName = device.getName(); String address = device.getAddress(); Log.e("TAG", deviceName + "----" + address + "++" + rssi); // 获得已经搜索到的蓝牙设备 BlueDevice item = new BlueDevice(deviceName, address, rssi, device.getBondState()); //deviceName.contains("Dintech Pluto42") && Log.e("TAG", "seek: " + rssiKey); if (rssi >= rssiKey && !TextUtils.isEmpty(deviceName)) { if (mDeviceList.size() > 0) { for (int i = 0; i < mDeviceList.size(); i++) { String address1 = mDeviceList.get(i).getAddress(); if (address.equals(address1)) { type = 1; break; } } if (type == 0) { mDeviceList.add(item); } } else { mDeviceList.add(item); } } Collections.sort(mDeviceList); Log.e("TAG222", mDeviceList.size() + ""); } };
佳姝4:syncScreen(String device_id, Paint paint)上传数据
private synchronized void syncScreen(String device_id, Paint paint) { String json = ""; try { json = AssetUtil.getJsonString(MyAppLocation.getInstence(), getResources().getString(R.string.TEMPLATE_JSON)); } catch (IOException e) { e.printStackTrace(); } if (TextUtils.isEmpty(json)) return; Queue
writes = TemplateConverterImpl.getInstance().convert(json, paint); String imgSelect = paint.getImgSelect(); Bleep .getInstance() .connect(device_id) .done(device1 -> write(device1, writes, imgSelect)) .fail((device, e) -> Toast.makeText(MainActivity.this, e.getDescription(), Toast.LENGTH_SHORT).show()) .enqueue(); } public void write(BluetoothDevice device, Queue mockQueue, String imgSelect) { if (mockQueue.isEmpty()) { //向服务器上传数据 NeT.api().bindPaint(imgSelect, imgSelect, device.getAddress()).enqueue(new TCallback () { @Override public void done(Bind body) { if (body != null) { if (body.isSuccess() && body.getStatusCode() == 200) { Paint paint = paintBox.query().equal(Paint_.imgSelect, imgSelect).build().findFirst(); if (paint != null) { paint.setBind_mac(device.getAddress()); paintBox.put(paint); syncMoreItems(); } Toast.makeText(MainActivity.this, getResources(). getString(R.string.UPLOAD_SUCCE), Toast.LENGTH_LONG).show(); } } } @Override public void fail(String message) { Toast.makeText(MainActivity.this, message, Toast.LENGTH_SHORT).show(); } }); new Handler().postDelayed(() -> writeRssiLevel(device), 5000); if (progressDialog != null && progressDialog.isShowing()) progressDialog.dismiss(); new AlertDialog .Builder(this) .setMessage(getResources().getString(R.string.SYNCHRONIZING_DONE)) .setPositiveButton(getResources().getString(R.string.CLOSE), (dialog, which) -> { dialog.dismiss(); mPopupWindow.dismiss(); }) .show(); return; } String mockString = mockQueue.poll(); Bleep.getInstance().write(device, HexUtil.hexStringToBytes(mockString)) .done(device1 -> write(device1, mockQueue, imgSelect)) .fail((device12, e) -> { mockQueue.clear(); }) .enqueue(); } private void writeRssiLevel(BluetoothDevice device) { String rssiHex = PreferencesUtil.get(RX, "8006002400050000"); Bleep.getInstance() .write(device, HexUtil.hexStringToBytes(rssiHex)) .done(device12 -> Bleep.getInstance().disconnect(device12).enqueue()) .fail((device1, e) -> new AlertDialog.Builder(this) .setMessage(getResources().getString(R.string.SIGNAL_UPDATE_FAILED)) .setNegativeButton("NO", (dialog, which) -> dialog.dismiss()) .setPositiveButton("YES", (dialog, which) -> { dialog.dismiss(); writeRssiLevel(device1); }) .create() .show()) .enqueue(); }
佳姝5:BlueDevice类(数据排序)
package com.dintech.graduation.bean; import android.os.Build; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import java.util.Objects; public class BlueDevice implements Comparable
{ public String name; public String address; public int rssi; public int state; public BlueDevice() { name = ""; address = ""; rssi = 0; state = 0; } public BlueDevice(String name, String address, int rssi, int state) { this.name = name; this.address = address; this.rssi = rssi; this.state = state; } public String output(String ad) { String Bzuobiao = ""; if (ad.equals("CF:55:66:07:F5:65")) { Bzuobiao = "(1,1)"; } else if (ad.equals("69:9B:71:69:F3:B7")) { Bzuobiao = "(10,10)"; } else if (ad.equals("CC:78:AB:AB:A0:DA")) { Bzuobiao = "(20,20)"; } return Bzuobiao; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public int getRssi() { return rssi; } public void setRssi(int rssi) { this.rssi = rssi; } //排序从高到低 @Override public int compareTo(@NonNull BlueDevice o) { int i = o.getRssi() - this.getRssi(); return i; } @RequiresApi(api = Build.VERSION_CODES.KITKAT) @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; BlueDevice that = (BlueDevice) o; return rssi == that.rssi && state == that.state && Objects.equals(name, that.name) && Objects.equals(address, that.address); } @RequiresApi(api = Build.VERSION_CODES.KITKAT) @Override public int hashCode() { return Objects.hash(name, address, rssi, state); } }
佳姝6:SeekBar进度条(保存数据)
private ArrayList
mDeviceList = new ArrayList<>(); private ArrayList mlist = new ArrayList<>(); String currentValue = "-50"; String lowValue = "-100"; String highValue = "0"; final String tempValue = String.valueOf(0 - Integer.parseInt(lowValue)); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_template); Integer key = PreferencesUtil.get("RssiKey", -50); tvSignal.setText("当前信号率:" + key); seekbarSignal.setMax(Integer.parseInt(highValue) - Integer.parseInt(lowValue)); seekbarSignal.setProgress(key + 100); seekbarSignal.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { tvSignal.setText("当前信号率:" + String.valueOf(i - Integer.parseInt(tempValue))); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { mlist.clear(); int progress = seekBar.getProgress(); int max = seekBar.getMax(); int result = progress - max; PreferencesUtil.put("RssiKey", result); int low = Integer.parseInt(lowValue); tvSignal.setText("当前信号率:" + String.valueOf(progress + low)); for (int i = 0; i < mDeviceList.size(); i++) { BlueDevice blueDevice = mDeviceList.get(i); int rssi = mDeviceList.get(i).getRssi(); if (rssi > progress + low && rssi <= -40 && rssi >= -100) { mlist.add(blueDevice); } } } }); }
还有更多笔记?:https://blog.csdn.net/Lj_18210158431