精华内容
下载资源
问答
  • Android 低功耗蓝牙开发

    千次阅读 2019-07-04 14:12:26
    Android 4.3(API Level 18)开始引入Bluetooth Low Energy(BLE,低功耗蓝牙)的核心功能并提供了相应的 API, 应用程序通过这些 API 扫描蓝牙设备、查询 services、读写设备的 characteristics(属性特征)等操作...

    初识低功耗蓝牙

    Android 4.3(API Level 18)开始引入Bluetooth Low Energy(BLE,低功耗蓝牙)的核心功能并提供了相应的 API, 应用程序通过这些 API 扫描蓝牙设备、查询 services、读写设备的 characteristics(属性特征)等操作。

    Android BLE 使用的蓝牙协议是 GATT 协议,有关该协议的详细内容可以参见蓝牙官方文档。以下我引用一张官网的图来大概说明 Android 开发中我们需要了解的一些 Bluetooth Low Energy 的专业术语。

    Service

    一个低功耗蓝牙设备可以定义许多 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

    Characteristic

    在 Service 下面,又包括了许多的独立数据项,我们把这些独立的数据项称作 Characteristic。同样的,每一个 Characteristic 也有一个唯一的 UUID 作为标识符。在 Android 开发中,建立蓝牙连接后,我们说的通过蓝牙发送数据给外围设备就是往这些 Characteristic 中的 Value 字段写入数据;外围设备发送数据给手机就是监听这些 Charateristic 中的 Value 字段有没有变化,如果发生了变化,手机的 BLE API 就会收到一个监听的回调。

    更详细的内容可以参见
    GATT Profile 简介
    通用属性配置文件(GATT)及其服务,特性与属性介绍
    GATT specification
    GATT Services
    蓝牙【GATT】协议介绍


    Android BLE API 简介

    BluetoothAdapter
    BluetoothAdapter 拥有基本的蓝牙操作,例如开启蓝牙扫描,使用已知的 MAC 地址 (BluetoothAdapter#getRemoteDevice)实例化一个 BluetoothDevice 用于连接蓝牙设备的操作等等。

    BluetoothDevice
    代表一个远程蓝牙设备。这个类可以让你连接所代表的蓝牙设备或者获取一些有关它的信息,例如它的名字,地址和绑定状态等等。

    BluetoothGatt
    这个类提供了 Bluetooth GATT 的基本功能。例如重新连接蓝牙设备,发现蓝牙设备的 Service 等等。

    BluetoothGattService
    这一个类通过 BluetoothGatt#getService 获得,如果当前服务不可见那么将返回一个 null。这一个类对应上面说过的 Service。我们可以通过这个类的 getCharacteristic(UUID uuid) 进一步获取 Characteristic 实现蓝牙数据的双向传输。

    BluetoothGattCharacteristic
    这个类对应上面提到的 Characteristic。通过这个类定义需要往外围设备写入的数据和读取外围设备发送过来的数据。


    Android 蓝牙开发示例

    第一步:声明所需要的权限

    <uses-permission android:name="android.permission.BLUETOOTH"/> 使用蓝牙所需要的权限
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 使用扫描和设置蓝牙的权限(申明这一个权限必须申明上面一个权限)
    

    在Android5.0之前,是默认申请GPS硬件功能的。而在Android 5.0 之后,需要在manifest 中申明GPS硬件模块功能的使用。

        <!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
        <uses-feature android:name="android.hardware.location.gps" />
    

    在 Android 6.0 及以上,还需要打开位置权限。如果应用没有位置权限,蓝牙扫描功能不能使用(其它蓝牙操作例如连接蓝牙设备和写入数据不受影响)。

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

    第二步:连接蓝牙前的初始化工作

    在建立蓝牙连接之前,需要确认设备支持 BLE。如果支持,再确认蓝牙是否开启。如果蓝牙没有开启,可以使用 BLuetoothAdapter 类来开启蓝牙。

    1. 获取 BluetoothAdapter
        private BluetoothAdapter mBluetoothAdapter;
            
        // Initializes Bluetooth adapter.
        final BluetoothManager bluetoothManager =
            (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        mBluetoothAdapter = bluetoothManager.getAdapter();
    
    1. 如果检测到蓝牙没有开启,尝试开启蓝牙
        // Ensures Bluetooth is available on the device and it is enabled. If not,
        // displays a dialog requesting user permission to enable Bluetooth.
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
                Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
        }
    

    第三步:扫描蓝牙设备

    外围设备开启蓝牙后,会广播出许多的关于该设备的数据信息,例如 mac 地址,uuid 等等。通过这些数据我们可以筛选出需要的设备。

    在 BluetoothAdapter 中,我们可以看到有两个扫描蓝牙的方法。第一个方法可以指定只扫描含有特定 UUID Service 的蓝牙设备,第二个方法则是扫描全部蓝牙设备。

    boolean startLeScan(UUID[] serviceUuids, BluetoothAdapter.LeScanCallback callback)
    
    boolean startLeScan(BluetoothAdapter.LeScanCallback callback)
    

    开启蓝牙扫描

    final BluetoothAdapter.LeScanCallback callback = new BluetoothAdapter.LeScanCallback() {
        @Override
        public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) {
            bluetoothDeviceArrayList.add(device);
            Log.d(TAG, "run: scanning...");
        }
    };
    
    mBluetoothAdapter.startLeScan(callback);
    

    在 LeScanCallback 回调的方法中,第一个参数是代表蓝牙设备的类,可以通过这个类建立蓝牙连接获取关于这一个设备的一系列详细的参数,例如名字,MAC 地址等等;第二个参数是蓝牙的信号强弱指标,通过蓝牙的信号指标,我们可以大概计算出蓝牙设备离手机的距离。计算公式为:d = 10^((abs(RSSI) - A) / (10 * n));第三个参数是蓝牙广播出来的广告数据。
    当执行上面的代码之后,一旦发现蓝牙设备,LeScanCallback 就会被回调,直到 stopLeScan 被调用。出现在回调中的设备会重复出现,所以如果我们需要通过 BluetoothDevice 获取外围设备的地址手动过滤掉已经发现的外围设备。

    停止蓝牙扫描

    void    stopLeScan(BluetoothAdapter.LeScanCallback callback)
    

    通过调用 BluetoothAdapter#stopLeScan 可以停止正在进行的蓝牙扫描。这里需要注意的是,传入的回调必须是开启蓝牙扫描时传入的回调,否则蓝牙扫描不会停止。

    由于蓝牙扫描的操作比较消耗手机的能量。所以我们不能一直开着蓝牙,必须设置一段时间之后关闭蓝牙扫描。示例代码如下:

    private void scanLeDevice(final boolean enable) {
        if (enable) {
            // Stops scanning after a pre-defined scan period.
            // 预先定义停止蓝牙扫描的时间(因为蓝牙扫描需要消耗较多的电量)
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
                }
            }, SCAN_PERIOD);
            mScanning = true;
    
            // 定义一个回调接口供扫描结束处理
            mBluetoothAdapter.startLeScan(mLeScanCallback);
        } else {
            mScanning = false;
            mBluetoothAdapter.stopLeScan(mLeScanCallback);
        }
    }
    

    第四步:连接蓝牙设备

    连接蓝牙设备可以通过 BluetoothDevice#ConnectGatt 方法连接,也可以通过 BluetoothGatt#connect 方法进行重新连接。以下分别是两个方法的官方说明:

    BluetoothDevice#connectGatt
    BluetoothGatt   connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback)
    

    第二个参数表示是否需要自动连接。如果设置为 true, 表示如果设备断开了,会不断的尝试自动连接。设置为 false 表示只进行一次连接尝试。
    第三个参数是连接后进行的一系列操作的回调,例如连接和断开连接的回调,发现服务的回调,成功写入数据,成功读取数据的回调等等。

    BluetoothGatt#connect
    boolean connect()
    

    调用这一个方法相当与调用 BluetoothDevice#connectGatt 且第二个参数 autoConnect 设置为 true。

    当调用蓝牙的连接方法之后,蓝牙会异步执行蓝牙连接的操作,如果连接成功会回调 BluetoothGattCalbackl#onConnectionStateChange 方法。这个方法运行的线程是一个 Binder 线程,所以不建议直接在这个线程处理耗时的任务,因为这可能导致蓝牙相关的线程被阻塞。

    void    onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
    

    这一个方法有三个参数,第一个就蓝牙设备的 Gatt 服务连接类。
    第二个参数代表是否成功执行了连接操作,如果为 BluetoothGatt.GATT_SUCCESS 表示成功执行连接操作,第三个参数才有效,否则说明这次连接尝试不成功。有时候,我们会遇到 status == 133 的情况,根据网上大部分人的说法,这是因为 Android 最多支持连接 6 到 7 个左右的蓝牙设备,如果超出了这个数量就无法再连接了。所以当我们断开蓝牙设备的连接时,还必须调用 BluetoothGatt#close 方法释放连接资源。否则,在多次尝试连接蓝牙设备之后很快就会超出这一个限制,导致出现这一个错误再也无法连接蓝牙设备。
    第三个参数代表当前设备的连接状态,如果 newState == BluetoothProfile.STATE_CONNECTED 说明设备已经连接,可以进行下一步的操作了(发现蓝牙服务,也就是 Service)。当蓝牙设备断开连接时,这一个方法也会被回调,其中的 newState == BluetoothProfile.STATE_DISCONNECTED。

    第五步:发现服务

    在成功连接到蓝牙设备之后才能进行这一个步骤,也就是说在 BluetoothGattCalbackl#onConnectionStateChang 方法被成功回调且表示成功连接之后调用 BluetoothGatt#discoverService 这一个方法。当这一个方法被调用之后,系统会异步执行发现服务的过程,直到 BluetoothGattCallback#onServicesDiscovered 被系统回调之后,手机设备和蓝牙设备才算是真正建立了可通信的连接。

    到这一步,我们已经成功和蓝牙设备建立了可通信的连接,接下来就可以执行相应的蓝牙通信操作了,例如写入数据,读取蓝牙设备的数据等等。

    读取数据

    当我们发现服务之后就可以通过 BluetoothGatt#getService 获取 BluetoothGattService,接着通过 BluetoothGattService#getCharactristic 获取 BluetoothGattCharactristic。
    通过 BluetoothGattCharactristic#readCharacteristic 方法可以通知系统去读取特定的数据。如果系统读取到了蓝牙设备发送过来的数据就会调用 BluetoothGattCallback#onCharacteristicRead 方法。通过 BluetoothGattCharacteristic#getValue 可以读取到蓝牙设备的数据。以下是代码示例:

    @Override
    public void onCharacteristicRead(final BluetoothGatt gatt,
                                        final BluetoothGattCharacteristic characteristic,
                                        final int status) {
    
        Log.d(TAG, "callback characteristic read status " + status
                + " in thread " + Thread.currentThread());
        if (status == BluetoothGatt.GATT_SUCCESS) {
            Log.d(TAG, "read value: " + characteristic.getValue());
        }
    
    }
    
    
    // 读取数据
    BluetoothGattService service = gattt.getService(SERVICE_UUID);
    BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(CHARACTER_UUID);
    gatt.readCharacteristic();
    

    写入数据

    和读取数据一样,在执行写入数据前需要获取到 BluetoothGattCharactristic。接着执行一下步骤:

    1. 调用 BluetoothGattCharactristic#setValue 传入需要写入的数据(蓝牙最多单次1支持 20 个字节数据的传输,如果需要传输的数据大于这一个字节则需要分包传输)。
    2. 调用 BluetoothGattCharactristic#writeCharacteristic 方法通知系统异步往设备写入数据。
    3. 系统回调 BluetoothGattCallback#onCharacteristicWrite 方法通知数据已经完成写入。此时,我们需要执行 BluetoothGattCharactristic#getValue 方法检查一下写入的数据是否我们需要发送的数据,如果不是按照项目的需要判断是否需要重发。
      以下是示例代码:
    @Override
    public void onCharacteristicWrite(final BluetoothGatt gatt,
                                        final BluetoothGattCharacteristic characteristic,
                                        final int status) {
        Log.d(TAG, "callback characteristic write in thread " + Thread.currentThread());
        if(!characteristic.getValue().equal(sendValue)) {
            // 执行重发策略
            gatt.writeCharacteristic(characteristic);
        }
    }
    
    //往蓝牙数据通道的写入数据
    BluetoothGattService service = gattt.getService(SERVICE_UUID);
    BluetoothGattCharacteristic characteristic = gatt.getCharacteristic(CHARACTER_UUID);
    characteristic.setValue(sendValue);
    gatt.writeCharacteristic(characteristic);
    

    向蓝牙设备注册监听实现实时读取蓝牙设备的数据

    BLE app通常需要获取设备中characteristic 变化的通知。下面的代码演示了怎么为一个Characteristic 设置一个监听。

    mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
    
    BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
            UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    mBluetoothGatt.writeDescriptor(descriptor);
    

    值得注意的是,除了通过 BluetoothGatt#setCharacteristicNotification 开启 Android 端接收通知的开关,还需要往 Characteristic 的 Descriptor 属性写入开启通知的数据开关使得当硬件的数据改变时,主动往手机发送数据。

    最后一步:断开连接

    当我们连接蓝牙设备完成一系列的蓝牙操作之后就可以断开蓝牙设备的连接了。通过 BluetoothGatt#disconnect 可以断开正在连接的蓝牙设备。当这一个方法被调用之后,系统会异步回调 BluetoothGattCallback#onConnectionStateChange 方法。通过这个方法的 newState 参数可以判断是连接成功还是断开成功的回调。
    由于 Android 蓝牙连接设备的资源有限,当我们执行断开蓝牙操作之后必须执行 BluetoothGatt#close 方法释放资源。需要注意的是通过 BluetoothGatt#close 方法也可以执行断开蓝牙的操作,不过 BluetoothGattCallback#onConnectionStateChange 将不会收到任何回调。此时如果执行 BluetoothGatt#connect 方法会得到一个蓝牙 API 的空指针异常。所以,我们推荐的写法是当蓝牙成功连接之后,通过 BluetoothGatt#disconnect 断开蓝牙的连接,紧接着在 BluetoothGattCallback#onConnectionStateChange 执行 BluetoothGatt#close 方法释放资源。
    以下是代码示例:

        @Override
        public void onConnectionStateChange(final BluetoothGatt gatt, final int status,
                                        final int newState) {
            Log.d(TAG, "onConnectionStateChange: thread "
                    + Thread.currentThread() + " status " + newState);
    
            if (status != BluetoothGatt.GATT_SUCCESS) {
                String err = "Cannot connect device with error status: " + status;
          // 当尝试连接失败的时候调用 disconnect 方法是不会引起这个方法回调的,所以这里
                    //   直接回调就可以了。
                gatt.close();
                Log.e(TAG, err);
                return;
            }
    
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                gatt.discoverService();
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                gatt.close();
            }
        }
    

    蓝牙操作的注意事项

    1. 蓝牙的写入操作( 包括 Descriptor 的写入操作), 读取操作必须序列化进行. 写入数据和读取数据是不能同时进行的, 如果调用了写入数据的方法, 马上调用又调用写入数据或者读取数据的方法,第二次调用的方法会立即返回 false, 代表当前无法进行操作. 详情可以参考 蓝牙读写操作返回 false,为什么多次读写只有一次回调?
    2. Android 连接外围设备的数量有限,当不需要连接蓝牙设备的时候,必须调用 BluetoothGatt#close 方法释放资源。详细的参考可以看这里 Android BLE 蓝牙开发的各种坑
    3. 蓝牙 API 连接蓝牙设备的超时时间大概在 20s 左右,具体时间看系统实现。有时候某些设备进行蓝牙连接的时间会很长,大概十多秒。如果自己手动设置了连接超时时间(例如通过 Handler#postDelay 设置了 5s 后没有进入 BluetoothGattCallback#onConnectionStateChange 就执行 BluetoothGatt#close 操作强制释放断开连接释放资源)在某些设备上可能会导致接下来几次的连接尝试都会在 BluetoothGattCallback#onConnectionStateChange 返回 state == 133。另外可以参考这篇吐槽 Android 中 BLE 连接出现“BluetoothGatt status 133”的解决方法
    4. 所有的蓝牙操作使用 Handler 固定在一条线程操作,这样能省去很多因为线程不同步导致的麻烦

     

    近段项目涉及蓝牙开发,这篇是逻辑相对比较清晰和全面的博文,感谢作者

    原文地址:https://www.jianshu.com/p/3a372af38103

    展开全文
  • Android低功耗蓝牙开发官方示例Demo

    千次下载 热门讨论 2015-12-17 09:41:44
    谷歌官方提供的Android平台上的关于低功耗蓝牙开发的示例代码,该示例包含了Android低功耗蓝牙开发的完整过程:(低功耗蓝牙可简称“BLE”) 1、声明蓝牙权限 2、设置BLE 3、扫描BLE 4、连接到GATT服务器(即低...
  • Android低功耗蓝牙开发

    2020-08-20 17:55:19
    Android Ble基础操作初始化工作蓝牙扫描、停止扫描蓝牙连接绑定、移除设备读写链接 初始化工作 RxBleClient rxBleClient = RxBleClient.create(context); //打开蓝牙 //Intent enableBtIntent = new //Intent...

    初始化工作

    RxBleClient rxBleClient = RxBleClient.create(context);
    //打开蓝牙
    //Intent enableBtIntent = new //Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    //int REQUEST_ENABLE_BT = 1;
    //context.startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    

    蓝牙扫描、停止扫描

    public void stopScan() {
    		new Thread(new Runnable() {
    
    			@SuppressWarnings({ "deprecation" })
    			@Override
    			public void run() {
    				BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
    				Log.i(TAG, "stopScan-bt--isEnabled:" + adapter.isEnabled());
    
    				if (!adapter.isEnabled()) {
    					adapter.enable();
    				}
    				adapter.stopLeScan(leScanCallback);
    			}
    		}).start();
    	
    	}
    	
    public void startScan() {
    		Log.d(TAG, "startScan in");
    		new Thread(new Runnable() {
    
    			@SuppressWarnings("deprecation")
    			@Override
    			public void run() {
    				// TODO Auto-generated method stub
    				BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
    				Log.i(TAG, "startScan-bt--isEnabled:" + adapter.isEnabled());
    				if (!adapter.isEnabled()) {
    					adapter.enable();
    				}
    				while (!adapter.startLeScan(leScanCallback)) {
    				}
    			}
    		}).start();
    }
    private LeScanCallback leScanCallback = new LeScanCallback() {
    
    		@Override
    		public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
    			Log.d(TAG, "onLeScan--isConnected() is " + isConnected()
    					+ "\n device.getName() is " + device.getName());
    
    	};
    

    蓝牙连接

    private void receive(String mac) {
    		bleDevice = rxBleClient.getBleDevice(mac);
    		createBound(bleDevice.getBluetoothDevice());
    		bleDevice
    				.observeConnectionStateChanges()//监听蓝牙连接状态
    				// .compose(bindUntilEvent(DESTROY))
    				.observeOn(AndroidSchedulers.mainThread())
    				// .subscribe();
    				.subscribe(
    						new Consumer<RxBleConnection.RxBleConnectionState>() {
    
    							@Override
    							public void accept(
    									RxBleConnection.RxBleConnectionState newState)
    									throws Exception {
    								Log.i(TAG,
    										"ConnectionStateChanges:"
    												+ newState.name());
    								if (newState == RxBleConnectionState.DISCONNECTED) {
    									startScan();
    								} else if (newState == RxBleConnectionState.CONNECTED) {
    									stopScan();
    
    								}
    							}
    						});
    		connectionDisposable = bleDevice.establishConnection(true)
    				// .compose(bindUntilEvent(DESTROY))
    				.observeOn(AndroidSchedulers.mainThread())
    				.doFinally(new Action() {
    
    					@Override
    					public void run() throws Exception {
    						connectionDisposable.dispose();
    						connectionDisposable = null;
    					}
    				})
    
    				.subscribe(new Consumer<RxBleConnection>() {
    
    					@Override
    					public void accept(RxBleConnection connection)
    							throws Exception {
    
    						onConnectionReceived(connection);
    					}
    				}, new Consumer<Throwable>() {
    
    					@Override
    					public void accept(Throwable t) throws Exception {
    						Log.i(TAG, "connect-Exception:" + t.getMessage());
    						
    					}
    				});
    	}
    

    绑定、移除设备

    	private void createBound(BluetoothDevice device) {
    		if (null == device)
    			return;
    		int bondsState = device.getBondState();
    		if (bondsState == BluetoothDevice.BOND_NONE) {
    			try {
    				Method m = device.getClass().getMethod("createBond",
    						(Class[]) null);
    				m.invoke(device, (Object[]) null);
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    		}
    	}
    
    	private void removeBound(BluetoothDevice device) {
    		if (null == device)
    			return;
    		int bondsState = device.getBondState();
    		if (bondsState == BluetoothDevice.BOND_BONDED) {
    			try {
    				Method m = device.getClass().getMethod("removeBond",
    						(Class[]) null);
    				m.invoke(device, (Object[]) null);
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    		}
    	}
    
    

    private void onConnectionReceived(RxBleConnection connection) {
    		// noinspection ConstantConditions
    		Log.i(TAG, "onConnectionReceived:" + connection);
    		this.rxBleConnection = connection;
    		onNotify();
    	}
    
    	public void onNotify() {
    
    		Log.i(TAG, "onNotify:");
    		if (isConnected()) {
    			rxBleConnection
    					.setupNotification(/*UUID*/)
    					.flatMap(
    							new Function<ObservableSource<byte[]>, ObservableSource<byte[]>>() {
    
    								@Override
    								public ObservableSource<byte[]> apply(
    										@NonNull ObservableSource<byte[]> arg0)
    										throws Exception {
    									return arg0;
    								}
    							}).observeOn(Schedulers.io())
    					.subscribe(new Consumer<byte[]>() {
    
    						@Override
    						public void accept(byte[] arg0) throws Exception {
    							//onNotificationReceived(arg0);
    
    						}
    					}, new Consumer<Throwable>() {
    
    						@Override
    						public void accept(Throwable arg0) throws Exception {
    							// onNotificationSetupFailure();
    
    							Log.i(TAG,
    									"received-Exception:" + arg0.getMessage());
    							
    						}
    					});
    			rxBleConnection
    					.setupNotification(/*UUID*/)
    					.flatMap(
    							new Function<ObservableSource<byte[]>, ObservableSource<byte[]>>() {
    
    								@Override
    								public ObservableSource<byte[]> apply(
    										@NonNull ObservableSource<byte[]> arg0)
    										throws Exception {
    									return arg0;
    								}
    							}).observeOn(AndroidSchedulers.mainThread())
    
    					.subscribe(new Consumer<byte[]>() {
    
    						@Override
    						public void accept(byte[] arg0) throws Exception {
    							//onNotificationReceivedControl(arg0);
    							//收到蓝牙设备发送信息
    						}
    					}, new Consumer<Throwable>() {
    
    						@Override
    						public void accept(Throwable arg0) throws Exception {
    							// onNotificationSetupFailure();
    
    							Log.i(TAG, "control-Exception:" + arg0.getMessage());
    							
    						}
    					});
    		}
    	}
    

    
    	private void onWriteFtc(byte[] data) {
    		Log.d(TAG, "onWriteFtc data is " + new String(data)
    				+ "and isconnected is" + isConnected());
    		if (isConnected()) {
    			rxBleConnection
    					.writeCharacteristic(
    							/*UUID*/, data)
    					.observeOn(Schedulers.io())
    					.subscribe(new Consumer<byte[]>() {
    
    						@Override
    						public void accept(byte[] arg0) throws Exception {
    							// TODO Auto-generated method stub
    							//String str = HexString.bytesToHex(arg0);
    							//Log.d(TAG, "onWriteFtcSuccess:" + str);
    							
    						}
    					}, new Consumer<Throwable>() {
    
    						@Override
    						public void accept(Throwable arg0) throws Exception {
    							// TODO Auto-generated method stub
    							Log.d(TAG, "onWriteFailure:" + arg0.getMessage());
    							
    						}
    					});
    		}
    	}
    
    

    链接

    RxAndroidBle工具包.
    RxAndroidBle更多明细.

    展开全文
  • Android 低功耗蓝牙开发(扫描、连接)前言正文一、项目配置二、权限请求三、扫描低功耗蓝牙四、显示扫描设备五、连接设备六、源码 前言   之间我写过蓝牙开发的文章,只不过是针对于经典蓝牙,可以理解为普通蓝牙...

    前言

      之前我写过蓝牙开发的文章,只不过是针对于经典蓝牙,可以理解为普通蓝牙,连接的对象是经典蓝牙,列如手机蓝牙、蓝牙耳机等设备。而也有读者说在学习低功耗蓝牙,因此就有了这篇文章,一方面是为了丰富蓝牙的使用,一方面也是为了帮助看我文章的读者,我会讲的很细,很多人也说我在记流水账,不过这不重要,重要的是你从流水账里学到了什么。

    正文

      首先明白低功耗蓝牙是什么?

      蓝牙低能耗(Bluetooth Low Energy,或称Bluetooth LE、BLE,旧商标Bluetooth Smart)也称低功耗蓝牙,是蓝牙技术联盟设计和销售的一种个人局域网技术,旨在用于医疗保健、运动健身、信标、安防、家庭娱乐等领域的新兴应用。相较经典蓝牙,低功耗蓝牙旨在保持同等通信范围的同时显著降低功耗和成本。

      概念已经了解了,下面创建一个名为BleDemo的项目来写这篇文章。
    在这里插入图片描述

    一、项目配置

      首先进行项目的配置,一个是build.gradle配置,一个是AndroidManifest.xml配置。
    先进行项目的build.gradle的配置,添加jitpack仓库。

    maven { url "https://jitpack.io"}
    

    在这里插入图片描述

    再进行app的build.gradle的配置,这里需要添加几个依赖库,

    //蓝牙扫描库
    implementation 'no.nordicsemi.android.support.v18:scanner:1.5.0'
    //权限请求 支持Androidx
    implementation 'pub.devrel:easypermissions:3.0.0'
    //让你的适配器一目了然,告别代码冗余
    implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.4'
    

    在这里插入图片描述
    改完了build.gradle记得要Sync Now。

    这个库是Nordic公司开发的,在蓝牙领域很出名的公司。这个版本是适配androidx的,一般现在创建新项目都是默认支持androidx的,不支持的话就说明你的AS该更新了。如果要支持support请到GitHub上去适配。其他的库或多或少都有接触过就不介绍了。

    下面配置AndroidManifest.xml。

    	<!-- 蓝牙权限 -->
        <uses-permission android:name="android.permission.BLUETOOTH" />
        <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> 
        <!-- 支持ble的设备 -->
        <uses-feature
            android:name="android.hardware.bluetooth_le"
            android:required="true" /> 
        <!-- 定位权限 -->
        <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
        <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    

    添加位置如下图所示
    在这里插入图片描述
      在Android 6.0以后的系统BLE scan需要申请location的相关权限才能支持BLE的一些功能,比如发现附近的beacons设备。

      这是开发的时候必须用到的权限,并非权限滥用。而在Android6.0以后则有了动态权限的申请,这里就说明一下等下为是什么要请求定位权限,后面就不要问我为什么扫描一个蓝牙还要打开定位权限这样的问题了。

    二、权限请求

      这里主要是定位权限的请求,还有就是获得定位之后,蓝牙是否有打开也需要进行处理,下面进行具体的编码。
    在MainActivity中新增一个方法,代码如下:

    	/**
         * 检查Android版本
         */
        private void checkAndroidVersion() {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                //Android 6.0及以上动态请求权限
                
            } else {
                //检查蓝牙是否打开
                
            }
        }
    

    这里进行Android版本的判断,6.0及以上则请求权限,6.0一下则判断蓝牙是否打开。

    下面先写这个蓝牙是否打开的判断

    	/**
         * 请求打开蓝牙
         */
        private static final int REQUEST_ENABLE_BLUETOOTH = 100;
        /**
         * 蓝牙适配器
         */
        private BluetoothAdapter bluetoothAdapter;
    
    	/**
         * 是否打开蓝牙
         */
        public void openBluetooth() {
            //获取蓝牙适配器
            bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
            if (bluetoothAdapter != null) {//是否支持蓝牙
                if (bluetoothAdapter.isEnabled()) {//打开
                    showMsg("蓝牙已打开");
                } else {//未打开
                    startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), REQUEST_ENABLE_BLUETOOTH);
                }
            } else {
                showMsg("你的设备不支持蓝牙");
            }
        }
    
        /**
         * Toast提示
         *
         * @param msg 内容
         */
        private void showMsg(String msg) {
            Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
        }
    

    这里会有一个页面的返回结果,代码如下:

    	@Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
            if (resultCode == Activity.RESULT_OK) {
                if (requestCode == REQUEST_ENABLE_BLUETOOTH) {
                    if (bluetoothAdapter.isEnabled()) {
                        //蓝牙已打开
                        showMsg("蓝牙已打开");
                    } else {
                        showMsg("请打开蓝牙");
                    }
                }
            }
        }
    

    那么现在对于蓝牙是否打开的结果进行了处理,下面进行动态权限的请求。

    	/**
         * 权限请求码
         */
        public static final int REQUEST_PERMISSION_CODE = 9527;
    	
    	/**
         * 请求权限
         */
        @AfterPermissionGranted(REQUEST_PERMISSION_CODE)
        private void requestPermission() {
            String[] perms = {Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.ACCESS_COARSE_LOCATION,};
            if (EasyPermissions.hasPermissions(this, perms)) {
                //权限通过之后检查有没有打开蓝牙
                openBluetooth();
            } else {
                // 没有权限
                EasyPermissions.requestPermissions(this, "App需要定位权限", REQUEST_PERMISSION_CODE, perms);
            }
        }
    

    这里会检查权限,有权限检查有没有打开蓝牙,没有权限则请求权限,请求权限的结果代码如下:

    	/**
         * 权限请求结果
         */
        @Override
        public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
            // 将结果转发给 EasyPermissions
    		EasyPermissions.onRequestPermissionsResult(REQUEST_PERMISSION_CODE, permissions, grantResults, this);
        }
    

    这个结果会通过@AfterPermissionGranted注解将结果返回给这个requestPermission方法,然后重新检查权限结果。下面只要在checkAndroidVersion中调用这个requestPermission()方法和openBluetooth()方法即可,如下图所示:
    在这里插入图片描述
    现在就形成了一个逻辑链,不过还需要一个地方去调用这个checkAndroidVersion()方法,就直接在onCreate中调用吧。
    在这里插入图片描述
    继续下一步。

    三、扫描低功耗蓝牙

      扫描低功耗蓝牙,首先要有触发的地方,其次要有显示结果的地方,这些都需要进行UI的处理,那么下面进行布局的修改和增加,修改activity_main.xml,代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <!--设备列表-->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/rv_device"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_above="@+id/btn_start_scan"
            android:overScrollMode="never" />
    
        <!--开始扫描-->
        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_start_scan"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_above="@+id/btn_stop_scan"
            android:layout_margin="6dp"
            android:insetTop="0dp"
            android:insetBottom="0dp"
            android:text="开始扫描" />
        <!--停止扫描-->
        <com.google.android.material.button.MaterialButton
            android:id="@+id/btn_stop_scan"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_alignParentBottom="true"
            android:layout_margin="6dp"
            android:insetTop="0dp"
            android:insetBottom="0dp"
            android:text="停止扫描" />
    </RelativeLayout>
    

    下面进行列表item的布局编写,在layout下新建一个item_device_rv.xml文件,文件代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/white"
        android:foreground="?attr/selectableItemBackground"
        android:orientation="vertical">
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center_vertical"
            android:padding="16dp">
    
            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:src="@drawable/ic_bluetooth_blue" />
    
            <LinearLayout
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:orientation="vertical"
                android:paddingStart="12dp">
    
                <TextView
                    android:id="@+id/tv_device_name"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:ellipsize="end"
                    android:singleLine="true"
                    android:text="设备名称"
                    android:textColor="@color/black"
                    android:textSize="16sp" />
    
                <TextView
                    android:id="@+id/tv_mac_address"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="8dp"
                    android:ellipsize="end"
                    android:singleLine="true"
                    android:text="Mac地址" />
            </LinearLayout>
    
            <TextView
                android:id="@+id/tv_rssi"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="信号强度" />
        </LinearLayout>
    
        <View
            android:layout_width="match_parent"
            android:layout_height="0.5dp"
            android:background="#EEE" />
    </LinearLayout>
    

    这里面有一个图标,使用路径绘制的ic_bluetooth_blue.xml,放在drawable文件夹下,代码如下:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:width="36dp"
        android:height="36dp"
        android:autoMirrored="true"
        android:tint="#42A5F5"
        android:viewportWidth="24.0"
        android:viewportHeight="24.0">
    
        <path
            android:fillColor="@android:color/white"
            android:pathData="M14.58,12.36l1.38,1.38c0.28,0.28 0.75,0.14 0.84,-0.24c0.12,-0.48 0.18,-0.99 0.18,-1.5c0,-0.51 -0.06,-1.01 -0.18,-1.48c-0.09,-0.38 -0.56,-0.52 -0.84,-0.24l-1.39,1.38C14.39,11.85 14.39,12.17 14.58,12.36zM18.72,7.51l-0.05,0.05c-0.25,0.25 -0.3,0.62 -0.16,0.94c0.47,1.07 0.73,2.25 0.73,3.49c0,1.24 -0.26,2.42 -0.73,3.49c-0.14,0.32 -0.09,0.69 0.16,0.94l0,0c0.41,0.41 1.1,0.29 1.35,-0.23c0.63,-1.3 0.98,-2.76 0.98,-4.3c-0.01,-1.48 -0.34,-2.89 -0.93,-4.16C19.83,7.22 19.13,7.1 18.72,7.51zM15,7l-4.79,-4.79C10.07,2.07 9.89,2 9.71,2h0C9.32,2 9,2.32 9,2.71v6.88L5.12,5.7c-0.39,-0.39 -1.02,-0.39 -1.41,0l0,0c-0.39,0.39 -0.39,1.02 0,1.41L8.59,12l-4.89,4.89c-0.39,0.39 -0.39,1.02 0,1.41h0c0.39,0.39 1.02,0.39 1.41,0L9,14.41v6.88C9,21.68 9.32,22 9.71,22h0c0.19,0 0.37,-0.07 0.5,-0.21L15,17c0.39,-0.39 0.39,-1.02 0,-1.42L11.41,12L15,8.42C15.39,8.03 15.39,7.39 15,7zM11,5.83l1.88,1.88L11,9.59V5.83zM12.88,16.29L11,18.17v-3.76L12.88,16.29z" />
    
    </vector>
    
    

    好了,现在针对于这个布局方面的内容告一段落,下面先运行一下了:在这里插入图片描述
    进行下一步操作。

    先进行页面的初始化。新增一个initView的方法。

    	private static final String TAG = MainActivity.class.getSimpleName();
    	/**
         * nordic扫描回调
         */
        private ScanCallback scanCallback;
    	
    	/**
         * 初始化
         */
        private void initView() {
            RecyclerView rvDevice = findViewById(R.id.rv_device);
            findViewById(R.id.btn_start_scan).setOnClickListener(v -> startScanDevice());
            findViewById(R.id.btn_stop_scan).setOnClickListener(v -> stopScanDevice());
            //扫描结果回调
            scanCallback = new ScanCallback() {
                @Override
                public void onScanResult(int callbackType, @NonNull ScanResult result) {
    
                    Log.d(TAG, "name:" + result.getDevice().getName() + ",rssi:" + result.getRssi());
                }
    
                @Override
                public void onScanFailed(int errorCode) {
                    throw new RuntimeException("Scan error");
                }
            };
        }
    

    这个initView主要是页面的初始化,列表在后面进行配置,根据扫描结果来定,然后就是配置扫描回调,这里注意导包的问题,不要到错了包。
    在这里插入图片描述
    然后还有一个开始扫描和停止扫描的方法。

    	/**
         * 开始扫描设备
         */
        public void startScanDevice() {
            BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
            scanner.startScan(scanCallback);
        }
    
        /**
         * 停止扫描设备
         */
        public void stopScanDevice() {
            BluetoothLeScannerCompat scanner = BluetoothLeScannerCompat.getScanner();
            scanner.stopScan(scanCallback);
        }
    

    下面在onCreate方法中调用initView()方法。
    在这里插入图片描述
    下面就可以开始运行了。运行之后点击开始扫描按钮,就会扫描附近的低功耗蓝牙设备,(请在附近有已打开低功耗蓝牙时进行扫描)可以在日志栏处进行打印。
    在这里插入图片描述
    这里很明显,扫描到了一些蓝牙设备,并且很多设备没有设备名称。既然有了结果,那么下面就是将扫描到的结果显示在列表上,这样才更直观。

    四、显示扫描设备

      下面将扫描结果渲染到列表上,首先明确列表要显示扫描设备的那些信息,从item来看有设备名、Mac地址、信号强度。那么可以根据这一个扫描的信息构建一个设备类,新建一个BleDevice类,代码如下:

    package com.llw.bledemo.bean;
    
    import android.bluetooth.BluetoothDevice;
    
    /**
     * @author llw
     * @description BleDevice
     * @date 2021/7/21 19:20
     */
    public class BleDevice {
        private BluetoothDevice device;
        private int rssi;
        private String realName;//真实名称
    
        /**
         * 构造Device
         * @param device 蓝牙设备
         * @param rssi 信号强度
         * @param realName 真实名称
         */
        public BleDevice(BluetoothDevice device, int rssi, String realName) {
            this.device = device;
            this.rssi = rssi;
            this.realName = realName;
        }
    
        public BluetoothDevice getDevice(){
            return device;
        }
    
        public int getRssi(){
            return rssi;
        }
    
        public void setRssi(int rssi) {
            this.rssi = rssi;
        }
    
        public String getRealName(){
            return realName;
        }
    
        public void setRealName(String realName) {
            this.realName = realName;
        }
    
        @Override
        public boolean equals(Object object) {
            if(object instanceof BleDevice){
                final BleDevice that =(BleDevice) object;
                return device.getAddress().equals(that.device.getAddress());
            }
            return super.equals(object);
        }
    }
    
    

    下面来写这个适配器,新建一个BleDeviceAdapter类,代码如下:

    package com.llw.bledemo.adapter;
    
    import com.chad.library.adapter.base.BaseQuickAdapter;
    import com.chad.library.adapter.base.viewholder.BaseViewHolder;
    import com.llw.bledemo.R;
    import com.llw.bledemo.bean.BleDevice;
    
    import java.util.List;
    
    /**
     * @author llw
     * @description BleDeviceAdapter
     * @date 2021/7/21 19:34
     */
    public class BleDeviceAdapter extends BaseQuickAdapter<BleDevice, BaseViewHolder> {
    
        public BleDeviceAdapter(int layoutResId, List<BleDevice> data) {
            super(layoutResId, data);
        }
    
        @Override
        protected void convert(BaseViewHolder holder, BleDevice bleDevice) {
            holder.setText(R.id.tv_device_name, bleDevice.getRealName())
                    .setText(R.id.tv_mac_address, bleDevice.getDevice().getAddress())
                    .setText(R.id.tv_rssi, bleDevice.getRssi() + " dBm");
        }
    }
    
    

    下面回到MainActivity中对列表进行适配,先定义变量

    	/**
         * 设备列表
         */
        private List<BleDevice> mList = new ArrayList<>();
    
        /**
         * 列表适配器
         */
        private BleDeviceAdapter deviceAdapter;
    

    然后在initView方法中进行列表配置,代码如下:

    	//列表配置
        deviceAdapter = new BleDeviceAdapter(R.layout.item_device_rv, mList);
        rvDevice.setLayoutManager(new LinearLayoutManager(this));
        //启用动画
        deviceAdapter.setAnimationEnable(true);
        //设置动画方式
        deviceAdapter.setAnimationWithDefault(BaseQuickAdapter.AnimationType.SlideInRight);
        rvDevice.setAdapter(deviceAdapter);
    

    添加位置如下:
    在这里插入图片描述
    下面就是将扫描结果添加到列表中了,可以写一个方法addDeviceList(),代码如下:

    	/**
         * 添加到设备列表
         *
         * @param bleDevice 蓝牙设备
         */
        private void addDeviceList(BleDevice bleDevice) {
            if (!mList.contains(bleDevice)) {
                bleDevice.setRealName(bleDevice.getRealName() == null ? "UNKNOWN" : bleDevice.getRealName());
                mList.add(bleDevice);
            } else {
                //更新设备信号强度值
                for (BleDevice device : mList) {
                    device.setRssi(bleDevice.getRssi());
                }
            }
            //刷新列表适配器
            deviceAdapter.notifyDataSetChanged();
        }
    

    然后在扫描的回调中进行调用即可。
    在这里插入图片描述
    点击开始的时候清理一下列表。
    在这里插入图片描述

    下面运行一下:
    在这里插入图片描述
    增加一个表示搜索的效果,在activity_main.xml中增加

    	<androidx.core.widget.ContentLoadingProgressBar
            android:id="@+id/loading_progress_bar"
            style="?android:attr/progressBarStyleHorizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:indeterminate="true"
            android:indeterminateTint="@color/purple_200"
            android:visibility="invisible"
            tools:ignore="UnusedAttribute"/>
    

    然后这个进度条设置在列表的上面。

    在这里插入图片描述
    回到MainActivity,创建变量:

    	/**
         * 加载进度条
         */
        private ContentLoadingProgressBar loadingProgressBar;
    

    绑定视图
    在这里插入图片描述
    控制视图
    在这里插入图片描述
    运行一下:
    在这里插入图片描述

    五、连接设备

      连接Ble设备其实也很简单,难的是连接之外的东西,先来构想一下连接功能的业务逻辑,点击设备列表中的设备,进行连接,先显示一个加载布局,表示现在正在连接,然后停止扫描,在根据设备的mac地址去连接这个设备,然后在连接设备的回调中处理连接设备的结果。嗯,就是这样。下面来编码,首先是加载布局的问题。在activity_main.xml中增加如下布局代码:

    	<!--加载布局-->
        <LinearLayout
            android:id="@+id/lay_connecting_loading"
            android:layout_centerInParent="true"
            android:layout_width="160dp"
            android:layout_height="160dp"
            android:orientation="vertical"
            android:visibility="invisible"
            android:background="@color/white"
            android:gravity="center">
    
            <ProgressBar
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:indeterminate="true"
                android:indeterminateTint="@color/purple_200" />
    
            <TextView
                android:layout_marginTop="12dp"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="连接中..."
                android:textColor="@color/black"
                android:textSize="@dimen/sp_14" />
        </LinearLayout>
    

    添加位置如下图
    在这里插入图片描述
    然后在MainActivity中创建变量

    	/**
         * 等待连接
         */
        private LinearLayout layConnectingLoading;
    

    绑定视图
    在这里插入图片描述
    下面新增一个方法,用来连接设备。在点击设备列表Item的时候调用。

    	/**
         * 连接设备
         *
         * @param bleDevice 蓝牙设备
         */
        private void connectDevice(BleDevice bleDevice) {
            //显示连接等待布局
            layConnectingLoading.setVisibility(View.VISIBLE);
    
            //停止扫描
            stopScanDevice();
    
            //获取远程设备
            BluetoothDevice device = bleDevice.getDevice();
    		//连接gatt
            device.connectGatt(this, false, new BluetoothGattCallback() {
                @Override
                public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
                    switch (newState) {
                        case BluetoothProfile.STATE_CONNECTED://连接成功
                            Log.d(TAG,"连接成功");
                            runOnUiThread(() -> {
                                layConnectingLoading.setVisibility(View.GONE);
                                showMsg("连接成功");
                            });
                            break;
                        case BluetoothProfile.STATE_DISCONNECTED://断开连接
                            Log.d(TAG,"断开连接");
                            runOnUiThread(() -> {
                                layConnectingLoading.setVisibility(View.GONE);
                                showMsg("断开连接");
                            });
                            break;
                        default:
                            break;
                    }
                }
            });
        }
    

    在initView()中设置列表点击。

    	//item点击事件
        deviceAdapter.setOnItemClickListener((adapter, view, position) -> {
            //连接设备
            connectDevice(mList.get(position));
        });
    

    在这里插入图片描述
    OK,下面运行一下:

    在这里插入图片描述
    这个布局背景是白色的不是很明显,改一下好了。在drawable文件夹下新建一个shape_loading_bg.xml,里面的代码如下:

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android">
        <solid android:color="@color/white" />
        <corners android:radius="20dp" />
        <stroke
            android:width="1dp"
            android:color="@color/purple_500" />
    </shape>
    

    然后设置到这个布局中
    在这里插入图片描述
    运行看看
    在这里插入图片描述
    嗯,还可以,就这样了。

    有连接设备就自然有断开连接设备。再增加两个变量

    	/**
         * Gatt
         */
        private BluetoothGatt bluetoothGatt;
    
        /**
         * 设备是否连接
         */
        private boolean isConnected = false;
    

    修改connectDevice()中的代码,如下图所示
    在这里插入图片描述
    再新建一个断开连接的方法,代码如下:

    	/**
         * 断开设备连接
         */
        private void disconnectDevice() {
            if (isConnected && bluetoothGatt != null) {
                bluetoothGatt.disconnect();
            }
        }
    

    六、源码

    源码地址Github:BleDemo

    源码地址CSDN:BleDemo.rar

    这篇文章就到这里了,有问题的可以评论区留言或者私信我都行,山高水长,后会有期~

    展开全文
  • 我集成了API21前后蓝牙版本,做成了一个BLE蓝牙调用库,方便快捷。希望能够帮助有需要的朋友们,内付demo仅供参考!
  • Android 低功耗蓝牙开发与遇到的坑

    千次阅读 2019-06-19 10:36:41
    1.搜索:我是传统的蓝牙搜索➕低功耗蓝牙搜索一起使用的 经典蓝牙:调用 mBluetoothAdapter.startDiscovery(); 这时我们需要注册一个通知来监听回调 完整代码如下: /** * 普通设备搜索 */ public void ...

    过程 搜索-配对-连接-通信

    目前配对的环节可以省略了

    1.搜索:我是传统的蓝牙搜索➕低功耗蓝牙搜索一起使用的

    经典蓝牙:调用

    mBluetoothAdapter.startDiscovery();

    这时我们需要注册一个通知来监听回调

    完整代码如下:

    /**
     * 普通设备搜索
     */
    public void searchDevices() {
        try {
            checkNotNull(mOnSearchDeviceListener);
            if (mBondedList == null) mBondedList = new ArrayList<>();
            if (mNewList == null) mNewList = new ArrayList<>();
            if (mBluetoothAdapter == null) {
                mOnSearchDeviceListener.onError(new NullPointerException(Constants.DEVICE_HAS_NOT_BLUETOOTH_MODULE));
                return;
            }
            if (mReceiver == null) mReceiver = new Receiver();
            // ACTION_FOUND
            IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
            context.registerReceiver(mReceiver, filter);
            // ACTION_DISCOVERY_FINISHED
            filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
            context.registerReceiver(mReceiver, filter);
            mNeedunRegister = true;
            mBondedList.clear();
            mNewList.clear();
            if (mBluetoothAdapter.isDiscovering())
                mBluetoothAdapter.cancelDiscovery();
            mBluetoothAdapter.startDiscovery();
            mOnSearchDeviceListener.onStartDiscovery();
    
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 搜索蓝牙广播
     */
    private class Receiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            try {
                String action = intent.getAction();
                if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {
                    if (mOnSearchDeviceListener != null)
                        mOnSearchDeviceListener.onStartDiscovery();
                } else if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                    BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                    if (device.getBondState() == BluetoothDevice.BOND_NONE) {
                        if (paar != null && !paar.containsKey(device.getAddress())) {
                            paar.put(device.getAddress(), "mac:" + device.getAddress());
                            if (mNewList != null) {
                                int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
                                SearchResult searchResult = new SearchResult(device, rssi, null);
                                mNewList.add(searchResult);
                            }
                        }
                    } else if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
                        if (mBondedList != null) {
                            int rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE);
                            SearchResult searchResult = new SearchResult(device, rssi, null);
                            mBondedList.add(searchResult);
                        }
                    }
                } else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
                    if (mOnSearchDeviceListener != null)
                        mOnSearchDeviceListener.onSearchCompleted(mBondedList, mNewList);
                    searchBLEDevices();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    可以在ACTION_FOUND的判断中写回调,这样多次回调,有数据更新的效果,放在ACTION_DISCOVERY_FINISHED会结束统一给数据

    低功耗蓝牙搜索:

    mBluetoothAdapter.startLeScan(mLeScanCallback);

    完整代码如下

    /**
     * ble搜索的回调
     */
    private final BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
    
        @Override
        public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
            if (!paar.containsKey(device.getAddress())) {
                Log.i("ble", "device " + device.getAddress() + "   " + device.getName());
                paar.put(device.getAddress(), "mac:" + device.getAddress());
                SearchResult searchResult = new SearchResult(device, rssi, null);
                mNewList.add(searchResult);
            } else {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
                    if (mOnSearchDeviceListener != null)
                        mOnSearchDeviceListener.onSearchCompleted(mBondedList, mNewList);
                }
            }
        }
    
    };
    
    
    /**
     * ble设备搜索
     */
    @SuppressWarnings("deprecation")
    public void searchBLEDevices() {
        try {
            checkNotNull(mOnSearchDeviceListener);
            if (mBondedList == null) mBondedList = new ArrayList<>();
            if (mNewList == null) mNewList = new ArrayList<>();
            if (mBluetoothAdapter == null) {
                mOnSearchDeviceListener.onError(new NullPointerException(Constants.DEVICE_HAS_NOT_BLUETOOTH_MODULE));
                return;
            }
            if (mBluetoothAdapter.isDiscovering())
                mBluetoothAdapter.stopLeScan(mLeScanCallback);
            mBluetoothAdapter.startLeScan(mLeScanCallback);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    注意点:蓝牙的权限

    <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-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    没有Location,有可能导致搜不到设备

    2.连接

    device = mBluetoothAdapter.getRemoteDevice(mDeviceAddress);
    //设为false,不自动连接
    mBluetoothGatt = device.connectGatt(context, false, mGattCallback);
    private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
        @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                isContect = true;
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        onConnectListen.onConnectSuccess();
                    }
                });
    
                mBluetoothGatt.discoverServices();
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                isContect = false;
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        onConnectListen.onConnectFail();
                    }
                });
                mBluetoothGatt.close();
            }
        }
    
        @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
    
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.i(TAG, "onServicesDiscovered");
                //发现设备,遍历服务,初始化特征
                initBLE(gatt);
            } else {
                Log.d("TAG", "onServicesDiscovered fail-->" + status);
            }
    
        }
    
        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
            super.onCharacteristicRead(gatt, characteristic, status);
    
            Log.e(TAG, "读数据成功  " + HexUtil.formatHexString(characteristic.getValue(), true));
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (onReadListen != null) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            String res="";
                            try {
                                 res = new String(characteristic.getValue(),"UTF-8");
                            } catch (UnsupportedEncodingException e) {
                                e.printStackTrace();
                            }
                            onReadListen.onSuccess(res);
    
                        }
                    });
                }
            } else {
                if (onReadListen != null) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            onReadListen.onFail("读失败:"+status);
                        }
                    });
                }
            }
        }
    
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
    
            if (onNotifyListen != null) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        onNotifyListen.onSuccess("数据变化");
    
                    }
                });
            }
        }
    
        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, final int status) {
            super.onDescriptorWrite(gatt, descriptor, status);
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (onNotifyListen != null) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            onNotifyListen.onSuccess("设置成功");
    
                        }
                    });
                }
            } else {
                if (onNotifyListen != null) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            onNotifyListen.onFail("通知失败:"+status);
                        }
                    });
                }
            }
        }
    
        @Override
        public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorRead(gatt, descriptor, status);
    
        }
    
        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, final int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (onWriteListen != null) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            onWriteListen.onSuccess("发送成功");
    
                        }
                    });
                }
            } else {
                if (onWriteListen != null) {
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            onWriteListen.onFail("发送失败" + status);
                        }
                    });
                }
            }
    
        }
    
    };

     

     

     

    展开全文
  • 1.app收不到蓝牙返回的数据。  需要把写的特征值的通知权限设置为true bluetoothGatt.setCharacteristicNotification(characteristicF2, true); 2.断开蓝牙连接。  有时候会发现调用disconnect断开蓝牙连接时...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 4,096
精华内容 1,638
关键字:

安卓低功耗蓝牙开发