这两天又在搞蓝牙,蓝牙伤我千百遍,我待蓝牙如初恋。

有位朋友说,做个appdemo,来和他的蓝牙模块进行交互。我发现我对蓝牙真的是连冰山一角都还没了解完。说说我都遇到了什么问题吧。

1.两个手机都打开蓝牙,如果离开设置蓝牙界面,难么你会发现你们都搜索不到彼此的设备。这不是你的错,这是谷歌的一个坑。

因为如下:

android 11 修改蓝牙型号 修改蓝牙设备_数据

解决:

//设置蓝牙可以被其他搜索到 
 blue.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);    

 //设置蓝牙可见的时间。0:是永久可见
 blue.setDiscoverableTimeout(0);//0:蓝牙永久可见

这个时候又郁闷了。BaseAdapter的源码里面setDiscoverableTimeout()和setScanMode()都被/*hide*/注释着。啊,是不是想要蓝牙现身有点曲折,我们想要用这两个方法还是有办法的。利用反射,调用这两个方法。

添加如下代码:

public void setDiscoverableTimeout(BluetoothAdapter adapter,int timeout) {
        try {
            Method setDiscoverableTimeout = BluetoothAdapter.class.getMethod("setDiscoverableTimeout", int.class);
            setDiscoverableTimeout.setAccessible(true);
            Method setScanMode =BluetoothAdapter.class.getMethod("setScanMode", int.class,int.class);
            setScanMode.setAccessible(true);

            setDiscoverableTimeout.invoke(adapter, timeout);
            setScanMode.invoke(adapter, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE,timeout);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

这样就可以调用setDiscoverableTimeout()和setScanMode()方法了,就可以让其他设备扫描到我的设备了。

2.监听手机蓝牙是否被了另一个设备匹配。

我第一次用的是这个方法

private BroadcastReceiver stateChangeRec=new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action=intent.getAction();
            if(action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)){
                tv_bluetooth_status.setText("蓝牙连接状态:连接成功");
                Log.i("tang","连接成功");
                //do something
            }else if(action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)){
                tv_bluetooth_status.setText("蓝牙连接状态:连接失败");
                Log.i("tang","连接失败");
            }
        }
    };

我会发现,在另外一台设备向我请求匹配的时候,就已经执行ACTION_BOND_STATE_CHANGED这个操作了。然后第一反应是绝对这个方法不适合我的情况。立刻重新找方法,

于是就得到了这个:

private BroadcastReceiver stateChangeRec=new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action=intent.getAction();
            if(action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)){
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                if (device.getBondState() == BluetoothDevice.BOND_BONDING) {
                    Log.i("tang","正在配对");
                } else if (device.getBondState() == BluetoothDevice.BOND_BONDED) {
                    Log.i("tang","完成配对");
                    tv_bluetooth_status.setText("蓝牙连接状态:连接成功");
                } else if (device.getBondState() == BluetoothDevice.BOND_NONE) {
                    Log.i("tang","取消配对");
                }
            }
          
        }
    };

这个就是在两个设备已经匹配完成的一个完整步骤。老夫也是松了一口气。

3.还有一个,那就是修改本机蓝牙设备的名字。网上都说的是setName("xx")就好。我好你个香蕉巴拉。。。

bluetoothAdapter.enable();//设置蓝牙为可用

bluetoothAdapter.setName(deviceName);//写入你想要设置的设备名字

setName其实是发送了一个广播,我们还需要来进行接收处理,完成整个操作。(很多博主都没说啊,坑死个人啊!)

这儿,我们需要发送一个改名字的广播!向全世界宣布,我的蓝牙我做主!

IntentFilter mFilter = new IntentFilter(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED);
mFilter.addAction(BluetoothAdapter.EXTRA_LOCAL_NAME);
registerReceiver(mReceiver, mFilter);

下面就是接受广播操作

//蓝牙广播接收器。修改名字时会调用。
    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            tv_current_name.setText("当前蓝牙名字:"+bluetoothAdapter.getName());
            String action = intent.getAction();
            if (BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED.equals(action)){
                Toast.makeText(context,"成功", Toast.LENGTH_SHORT).show();
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

                Set<BluetoothDevice> devices = bluetoothAdapter.getBondedDevices();
                for(int i=0; i<devices.size(); i++)
                {
                    BluetoothDevice device1 = (BluetoothDevice) devices.iterator().next();
                    Log.i("tang","2-2蓝牙名字="+device1.getName());
                    //System.out.println(device1.getName());
                }
            }
            else if(BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)){
                Toast.makeText(context,"finished", Toast.LENGTH_SHORT).show();
            }
        }
    };

这才是一个ojbk的操作。

这里面还有一个坑,真的是没找到有博主说过这个问题。setName(“xxx”)这里面的值,是有限制的!

广播发送的字节好像是限制早<=60个字节,所以setName的值不是无限的,(如果你要广播发送包含了名字)。60个字节,30个汉字,包含所发送的其他信息,setName的值,限制也是跟随你所发送的信息多少而决定的。

当蓝牙名字>限制的个数的时候,广播发送会失败,ErrorCode=18或者1,对于BLE蓝牙的ErrorCode网上也没有人说,也搜索不到。我也不知道为啥。(有谁知道为啥蓝牙名字>5的时候广播开启会失败,且ErrorCode=18/1,麻烦留言告知一声,谢谢!)

4.还有个误区。我在断开蓝牙的时候使用的是disconnet(),但是我的小伙伴用他的手机和我测试的时候说,蓝牙没有断开啊。我说不可能啊,我又添加了close,双保险,这下没问题了吧。我小伙伴依然说,蓝牙没断开啊,一看。他看的是“已配对设备”,我特么操着键盘给他扔过去。

已配对,不代表蓝牙没断开,只是说明两个设备在下次连接的时候不需要再次配对了。大家测试的时候不要进入这个误区了。

持续更新一波,我app已经做好。小伙伴说搜索不到,让我把设备变成从设备。我这个蓝牙小白,一脸蒙圈,然后又去百度。设备分为主设备和从设备。

话不多说,到底如何把一个设备变成从设备呢。代码敬上:

这是一个设置广播,打开广播的操作。

private void sendOutSheBei() {

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            settings = new AdvertiseSettings.Builder()
                    .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) //广播模式: 低功耗,平衡,低延迟
                    .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH) //发射功率级别: 极低,低,中,高
                    .setConnectable(true) //能否连接,广播分为可连接广播和不可连接广播
                    .build();

            //广播数据(必须,广播启动就会发送)
            advertiseData = new AdvertiseData.Builder()
                    .setIncludeDeviceName(true) //包含蓝牙名称
                    .setIncludeTxPowerLevel(true) //包含发射功率级别
                    .addManufacturerData(1, new byte[]{23, 33}) //设备厂商数据,自定义
                    .build();
            //扫描响应数据(可选,当客户端扫描时才发送)
            scanResponse = new AdvertiseData.Builder()
                    .addManufacturerData(2, new byte[]{66, 66}) //设备厂商数据,自定义
                    .addServiceUuid(new ParcelUuid(UUID_SERVICE)) //服务UUID
                    //      .addServiceData(new ParcelUuid(UUID_SERVICE), new byte[]{2}) //服务数据,自定义
                    .build();
            mBluetoothLeAdvertiser = bluetoothAdapter.getBluetoothLeAdvertiser();
        }

        mBluetoothLeAdvertiser.startAdvertising(settings, advertiseData, scanResponse, mAdvertiseCallback);
        if (mBluetoothLeAdvertiser == null) {
            Toast.makeText(this, "该设备不支持蓝牙低功耗从设备通讯", Toast.LENGTH_SHORT).show();
            this.finish();
            return;
        }
    }

这里面有一点需要注意,.setIncludeDeviceName(true) //包含蓝牙名称  我开始以为这只是是否传递蓝牙名字的一个设置,我给它修改为.setIncludeDeviceName(false) ,别人就搜不到我的蓝牙了。你要是想让别人搜索到你的蓝牙,那你就老老实实的写为true。

这上面的代码还不够,还需要一个监听器,监听我们的广播是不是打开了。

// BLE广播Callback
    private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
        @Override
        public void onStartSuccess(AdvertiseSettings settingsInEffect) {
            Log.i("tang","BLE广播开启成功");
        }

        @Override
        public void onStartFailure(int errorCode) {
            Log.i("tang","BLE广播开启失败,错误码"+errorCode);
        }
    };

我们一定还想要和其他设备进行信息交互,哪里监听?第一段代码里面就有一个监听回调,好的,现在敬上回调监听代码:

// BLE服务端Callback
    private BluetoothGattServerCallback mBluetoothGattServerCallback = new BluetoothGattServerCallback() {
        @Override
        public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
            Log.i("tang", "从设备连接状态="+String.format(status == 0 ? (newState == 2 ? "与[%s]连接成功" : "与[%s]连接断开") : ("与[%s]连接出错,错误码:" + status)));

            if(newState==2){
                //连接成功
                tv_bluetooth_status.setText("蓝牙连接状态:连接成功");
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        tv_bluetooth_status.setText("蓝牙连接状态:连接中断");
                        Toast.makeText(mContext, "蓝牙断开", Toast.LENGTH_SHORT).show();
                        mBluetoothLe.disconnect();
                        mBluetoothLe.close();
                    }
                }, getPostTime(deviceTime));
            }else{
                tv_bluetooth_status.setText("蓝牙连接状态:连接中断");
            }
        }

        @Override
        public void onServiceAdded(int status, BluetoothGattService service) {
            Log.i("tang", "从设备连接状态="+String.format(status == 0 ? "添加服务[%s]成功" : "添加服务[%s]失败,错误码:" + status, service.getUuid()));
        }

        @Override
        public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
            String response = "CHAR_" + (int) (Math.random() * 100); //模拟数据
            mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, response.getBytes());// 响应客户端
            Log.i("tang","客户端读取Characteristic[" + characteristic.getUuid() + "]:\n" + response);
        }

        @Override
        public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] requestBytes) {
            // 获取客户端发过来的数据
            String requestStr = new String(requestBytes);
            Log.i("tang","客户端写入Characteristic[" + characteristic.getUuid() + "]:\n" + requestStr);
        }

        @Override
        public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
            String response = "DESC_" + (int) (Math.random() * 100); //模拟数据
            mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, response.getBytes()); // 响应客户端
            Log.i("tang","客户端读取Descriptor[" + descriptor.getUuid() + "]:\n" + response);
        }

        @Override
        public void onDescriptorWriteRequest(final BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
            // 获取客户端发过来的数据
            String valueStr = Arrays.toString(value);
            mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);// 响应客户端
          

            // 简单模拟通知客户端Characteristic变化
            if (Arrays.toString(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE).equals(valueStr)) { //是否开启通知
                final BluetoothGattCharacteristic characteristic = descriptor.getCharacteristic();
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 5; i++) {
                            SystemClock.sleep(3000);
                            String response = "CHAR_" + (int) (Math.random() * 100); //模拟数据
                            characteristic.setValue(response);
                            mBluetoothGattServer.notifyCharacteristicChanged(device, characteristic, false);
                            Log.i("tang","通知客户端改变Characteristic[" + characteristic.getUuid() + "]:\n" + response);
                            //logTv("通知客户端改变Characteristic[" + characteristic.getUuid() + "]:\n" + response);
                        }
                    }
                }).start();
            }
        }

        @Override
        public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
            Log.i("tang",String.format("onExecuteWrite:%s,%s,%s,%s", device.getName(), device.getAddress(), requestId, execute));

        @Override
        public void onNotificationSent(BluetoothDevice device, int status) {
            Log.i("tang",String.format("onNotificationSent:%s,%s,%s", device.getName(), device.getAddress(), status));
        }

        @Override
        public void onMtuChanged(BluetoothDevice device, int mtu) {
            Log.i("tang",String.format("onMtuChanged:%s,%s,%s", device.getName(), device.getAddress(), mtu));
        }

    };

我做完了这些事之后,我发现,还有坑。我每次修改蓝牙本机名字之后,前几次没啥事儿,小伙伴突然给我说,不行了。一下回到解放前的名字了。我思考啊,思考啊。。。我觉得应该是发送广播里面设置名字的锅,它发广播只有一次,但是名字早给我发出去了,那我之后设置的不是都是打水漂?但是我仍然没把这一点想通,但是时间不等人啊,压力驱使我死马当活马医啊。

然后我就在每次修改名字的时候,发送一次广播,断开前一次的广播,重新发一次广播。很鸡肋,但是却有效,要是有小伙伴知道怎么处理,麻烦给我说一下。哎~但愿天堂没有bug。

不多就是这样,我遇到的就是这些坑。