Android蓝牙4.0 Ble读写数据详解 -2

Android蓝牙4.0 Ble读写数据详解 -2

上一篇说了如何扫描与链接蓝牙 这篇文章讲讲与蓝牙的数据传输,与一些踩到的坑。

先介绍一款调试工具,专门调试Ble蓝牙的app。名字叫:nRF-Connect 谷歌应用商店也能下载到。

这里我先连接一个蓝牙设备 贴几个截图。

UUID的话 就相当于钥匙,蓝牙设备当中有通道,那么通道是需要UUID进行匹配的

当连接上设备之后,可以看到UUID的通道 接下来,按照设备厂商提供的文档,找到我们需要的UUID通道

比如说我这里需要的是0x6a的Service通道 然后点开最后一个Service通道查看

展开Service后 可以看到有两个Characteristic通道

我们看Properties属性 一个是NOTIFY 一个是WRITE 也有可能会有READ这个属性的通道

可以拿这个app输出写出指令给蓝牙,在不清楚是蓝牙的问题还是自己的问题的时候,这个工具还是挺好使的。

Notify的话,需要注意这个Descriptors的UUID 这个在注册Notify的时候,需要用到,这里虽然看不全,但是之后可以通过打印得到。

简单说一下这三种属性的通道的用途

WRITE:顾名思义,写的意思,该通道是用来向蓝牙设备写出数据的通道

READ:向蓝牙设备进行读取数据的通道 这个通道有一个坑 后续会详细写上

Notify:该通道需要注册监听,这是一个通知的通道,蓝牙向你传输数据的话,就能直接收到监听。

我这边的话 因为一些原因,所以没有使用READ通道获取数据 只用了Notify通道 当然 也会讲讲怎么使用READ

准备工作

先将UUID管理起来,我这里的话 采用静态常量的形式保存起来了。

public class UUIDManager {
    /**
     * 服务的UUID
     */
    public static final String SERVICE_UUID = "00006a00-0000-1000-8000-00805f9b34fb";
    /**
     * 订阅通知的UUID
     */
    public static final String NOTIFY_UUID = "00006a02-0000-1000-8000-00805f9b34fb";
    /**
     * 写出数据的UUID
     */
    public static final String WRITE_UUID = "00006a02-0000-1000-8000-00805f9b34fb";

    /**
     * NOTIFY里面的Descriptor UUID
     */
    public static final String NOTIFY_DESCRIPTOR = "00002902-0000-1000-8000-00805f9b34fb";
    
}

处理通知回调接口

蓝牙的数据回调,其实并不是回调到主线程当中,所以如果接收到数据之后,就进行视图操作的话,是会失败的。

所以我打算切换到主线程进行回调,当然,也可以使用EventBus,不过我不喜欢这东西就没去用。

回调接口的话,打算使用list集合存储起来,然后回调到各个需要数据的地方。 创建以下三个类

/**
 * Created by Pencilso on 2017/4/20.
 * 蓝牙数据回调监听接口
 */
public interface BlueNotifyListener {
    public  void onNotify(Message notify);
}

/**
 * Created by Pencilso on 2017/4/25.
 * 处理回调所有接口
 */
public class NotifyThread implements Runnable {
    private List<BlueNotifyListener> listeners;
    private Message notify;

    @Override
    public void run() {
        if (notify == null || listeners==null)
            return;
        try {
            Iterator<BlueNotifyListener> iterator = listeners.iterator();
            while (iterator.hasNext()) {
                BlueNotifyListener next = iterator.next();
                if (next == null)
                    iterator.remove();
                else
                    next.onNotify(notify);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void setListeners(List<BlueNotifyListener> listeners) {
        this.listeners = listeners;
    }

    public void setNotify(Message notify) {
        this.notify = notify;
    }
}

/**
 * Created by Pencilso on 2017/4/26.
 * 蓝牙的Code类 用来自定义回调的标识
 */
public class BlueCodeUtils {
    /**
     * 蓝牙状态 已连接
     */
    public static final int BLUETOOTH_STATE_CONNECT = 0x1;
    /**
     * 蓝牙状态 已断开
     */
    public static final int BLUETOOTH_STATE_DISCONNECT = 0x2;
    
    //*******这些只是自定义的code  就不复制太多了
}

编写蓝牙的功能

新建BluetoothBinder类 继承自BluetoothGattCallback
然后把蓝牙的功能模块写在这里面。
主要的功能都在这个类里面,我把这个放到了服务里面,所以在创建BluetoothBinder的时候需要传递一个Service对象

import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Build;
import android.os.Message;
import android.support.annotation.RequiresApi;
import android.util.Log;

import java.util.List;
import java.util.UUID;

import cc.petnet.trenmotion.Interface.IBluetoothInterface;
import cc.petnet.trenmotion.utils.HandleUtils;
import cc.petnet.trenmotion.utils.LogUtils;

/**
 * Created by Pencilso on 2017/4/20.
 * 蓝牙操作的Binder
 */
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
public class BluetoothBinder extends BluetoothGattCallback implements IBluetoothInterface {
    private static BluetoothBinder bluetoothBinder;
    private final Service bluetoothService;//service服务
    private final BluetoothAdapter adapter;//蓝牙的适配器
    private List<BlueNotifyListener> notifyList;//监听的集合
    private BluetoothManager bluetoothManager;//蓝牙管理者
    private BluetoothGattService gattService;//通道服务
    private BluetoothGatt bluetoothGatt;

    public static IBluetoothInterface getInstace() {
        return bluetoothBinder;
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
    protected BluetoothBinder(BluetoothService bluetoothService) {
        bluetoothBinder = this;
        this.bluetoothService = bluetoothService;
        bluetoothManager = (BluetoothManager) bluetoothService.getSystemService(Context.BLUETOOTH_SERVICE);
        adapter = bluetoothManager.getAdapter();
    }

    @Override
    public void onDestroy() {
        bluetoothBinder = null;
    }

    @Override
    public <T extends BlueNotifyListener> void addNotifyListener(T notifyListener) {
        if (notifyListener != null)
            notifyList.add(notifyListener);
    }

    @Override
    public void removeNotifyListener(BlueNotifyListener notifyListener) {
        if (notifyListener != null)
            notifyList.remove(notifyListener);
    }

    /**
     * 广播蓝牙监听消息
     * 因为蓝牙发送过来的消息 并不是处于主线程当中的
     * 所以如果直接对蓝牙的数据展示视图的话  会展示不了的 这里的话  封装到主线程当中遍历回调数据
     *
     * @param notify
     */
    public void traverseListener(Message notify) {
        NotifyThread notifyThread = new NotifyThread();//创建一个遍历线程
        notifyThread.setListeners(notifyList);
        notifyThread.setNotify(notify);
        HandleUtils.getInstace().post(notifyThread);
    }

    /**
     * 系统的蓝牙是否已经打开
     *
     * @return
     */
    @Override
    public boolean isEnable() {
        return adapter.isEnabled();
    }

    @Override
    public void enableBluetooth(boolean enable) {
        if (enable)
            adapter.enable();
        else
            adapter.disable();
    }

    @SuppressWarnings("deprecation")
    @SuppressLint("NewApi")
    @Override
    public void startScanner(BluetoothAdapter.LeScanCallback callback) {
        adapter.startLeScan(callback);
    }

    @SuppressLint("NewApi")
    @SuppressWarnings("deprecation")
    @Override
    public void stopScanner(BluetoothAdapter.LeScanCallback callback) {
        adapter.stopLeScan(callback);
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
    @Override
    public void connectDevices(String address) {
        BluetoothDevice remoteDevice = adapter.getRemoteDevice(address);
        BluetoothGatt bluetoothGatt = remoteDevice.connectGatt(bluetoothService, false, this);
    }

    /**
     * 蓝牙设备状态的监听
     *
     * @param gatt
     * @param status
     * @param newState 蓝牙的状态被改变
     */
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
        Message message = new Message();
        switch (newState) {//对蓝牙反馈的状态进行判断
            case BluetoothProfile.STATE_CONNECTED://已链接
                message.what = BlueCodeUtils.BLUETOOTH_STATE_CONNECT;
                gatt.discoverServices();
                break;
            case BluetoothProfile.STATE_DISCONNECTED://已断开
                message.what = BlueCodeUtils.BLUETOOTH_STATE_DISCONNECT;
                break;
        }
        traverseListener(message);
        /**
         * 这里还有一个需要注意的,比如说蓝牙设备上有一些通道是一些参数之类的信息,比如最常见的版本号。
         * 一般这种情况 版本号都是定死在某一个通道上,直接读取,也不需要发送指令的。
         * 如果遇到这种情况,一定要在发现服务之后 再去读取这种数据  不要一连接成功就去获取
         */
    }

    /**
     * 发现服务
     *
     * @param gatt
     * @param status
     */
    @Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
        gattService = gatt.getService(UUID.fromString(UUIDManager.SERVICE_UUID));// 获取到服务的通道
        bluetoothGatt = gatt;
        //获取到Notify的Characteristic通道 这个根据协议来定  如果设备厂家给的协议不是Notify的话  就不用做以下操作了
        BluetoothGattCharacteristic notifyCharacteristic = gattService.getCharacteristic(UUID.fromString(UUIDManager.NOTIFY_UUID));
        BluetoothUtils.enableNotification(gatt, true, notifyCharacteristic);//注册Notify通知
    }

    /**
     * 向蓝牙写入数据
     *
     * @param data
     */
    @SuppressLint("NewApi")
    @Override
    public boolean writeBuletoothData(String data) {
        if (bluetoothService == null) {
            return false;
        }
        BluetoothGattCharacteristic writeCharact = gattService.getCharacteristic(UUID.fromString(UUIDManager.WRITE_UUID));
        bluetoothGatt.setCharacteristicNotification(writeCharact, true); // 设置监听
        // 当数据传递到蓝牙之后
        // 会回调BluetoothGattCallback里面的write方法
        writeCharact.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
        // 将需要传递的数据 打碎成16进制
        writeCharact.setValue(BluetoothUtils.getHexBytes(data));
        return bluetoothGatt.writeCharacteristic(writeCharact);
    }

    /**
     * 蓝牙Notify推送过来的数据
     *
     * @param gatt
     * @param characteristic
     */
    @Override
    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        super.onCharacteristicChanged(gatt, characteristic);
        //如果推送的是十六进制的数据的写法
        String data = BluetoothUtils.bytesToHexString(characteristic.getValue()); // 将字节转化为String字符串
        Message message = new Message();
        message.what = BlueCodeUtils.BLUETOOTH_PUSH_MESSAGE;
        message.obj = data;
        traverseListener(message);//回调数据
//       String data =  characteristic.getStringValue(0); // 使用场景  例如某个通道里面静态的定死了某一个值,就用这种写法获取 直接获取到String类型的数据
    }

    /**
     * 这里有一个坑  一定要注意,如果设备返回数据用的不是Notify的话 一定要注意这个问题
     * 这个方法是 向蓝牙设备写出数据成功之后回调的方法,写出成功之后干嘛呢? 主动去蓝牙获取数据,没错,自己主动去READ通道获取蓝牙数据
     * 如果用的是Notify的话 不用理会该方法   写出到蓝牙之后  等待Notify的监听 即onCharacteristicChanged方法回调。
     *
     * @param gatt
     * @param characteristic
     * @param status
     */
    @Override
    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        super.onCharacteristicWrite(gatt, characteristic, status);
        if (status == BluetoothGatt.GATT_SUCCESS) { //写出成功 接下来  该去读取蓝牙设备的数据了
            //这里的READUUID 应该是READ通道的UUID 不过我这边的设备没有用到READ通道  所以我这里就注释了 具体使用 视情况而定
//            BluetoothGattCharacteristic readCharact = gattService.getCharacteristic(UUID.fromString(READUUID));
//            gatt.readCharacteristic(readCharact);
        }
    }

    /**
     * 调用读取READ通道后返回的数据回调
     * 比如说 在onCharacteristicWrite里面调用 gatt.readCharacteristic(readCharact);之后 会回调该方法
     *
     * @param gatt
     * @param characteristic
     * @param status
     */
    @Override
    public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        super.onCharacteristicRead(gatt, characteristic, status);
        //如果推送的是十六进制的数据的写法
        String data = BluetoothUtils.bytesToHexString(characteristic.getValue()); // 将字节转化为String字符串
        Message message = new Message();
        message.what = BlueCodeUtils.BLUETOOTH_PUSH_MESSAGE;
        message.obj = data;
        traverseListener(message);//回调数据
//       String data =  characteristic.getStringValue(0); // 使用场景  例如某个通道里面静态的定死了某一个值,就用这种写法获取 直接获取到String类型的数据
    }
}

其实这个BluetoothBinder还定义了一个接口IBluetoothInterface,然后让BluetoothBinder实现,并且将BluetoothBinder设置单利,暴露方法,提供返回IBluetoothInterface对象

import android.bluetooth.BluetoothAdapter;

import cc.petnet.trenmotion.service.bluetooth.BlueNotifyListener;

/**
 * Created by Pencilso on 2017/4/20.
 */
public interface IBluetoothInterface {
    /**
     *
     * @param notifyListener 添加监听事件
     * @param <T> BlueNotifyListener监听回调接口
     */
    <T extends BlueNotifyListener> void addNotifyListener(T notifyListener);

    /**
     *
     * @param notifyListener 删除监听接口
     */
    void removeNotifyListener(BlueNotifyListener notifyListener);
    /**
     * 系统是否开启了蓝牙
     *
     * @return
     */
    boolean isEnable();

    /**
     * 打开或者关闭系统蓝牙
     *
     * @param enable
     */
    void enableBluetooth(boolean enable);

    /**
     * 启动扫描
     *
     * @param callback
     */
    void startScanner(BluetoothAdapter.LeScanCallback callback);

    /**
     * 停止扫描
     *
     * @param callback
     */
    void stopScanner(BluetoothAdapter.LeScanCallback callback);

    void onDestroy();

    /**
     * 连接设备
     *
     * @param address
     */
    void connectDevices(String address);

    /**
     * 向蓝牙写出数据
     * @param data
     * @return
     */
    public boolean writeBuletoothData(String data);
}

Handler工具 主要用来切换到主线程当中

public class HandleUtils {
    private static Handler handler = null;

    public static Handler getInstace() {
        if (handler == null)
            handler = new Handler();
        return handler;
    }
}

BluetoothUtils

public class BluetoothUtils {
    /**
     * 是否开启蓝牙的通知
     *
     * @param enable
     * @param characteristic
     * @return
     */
    @SuppressLint("NewApi")
    public static boolean enableNotification(BluetoothGatt bluetoothGatt, boolean enable, BluetoothGattCharacteristic characteristic) {
        if (bluetoothGatt == null || characteristic == null) {
            return false;
        }
        if (!bluetoothGatt.setCharacteristicNotification(characteristic, enable)) {
            return false;
        }
        //获取到Notify当中的Descriptor通道  然后再进行注册
        BluetoothGattDescriptor clientConfig = characteristic.getDescriptor(UUID.fromString(UUIDManager.NOTIFY_DESCRIPTOR));
        if (clientConfig == null) {
            return false;
        }
        if (enable) {
            clientConfig.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
        } else {
            clientConfig.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
        }
        return bluetoothGatt.writeDescriptor(clientConfig);
    }

    /**
     * 将字节 转换为字符串
     *
     * @param src 需要转换的字节数组
     * @return 返回转换完之后的数据
     */
    public static String bytesToHexString(byte[] src) {
        StringBuilder stringBuilder = new StringBuilder("");
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }
    /**
     * 将字符串转化为16进制的字节
     *
     * @param message
     *            需要被转换的字符
     * @return
     */
    public static byte[] getHexBytes(String message) {
        int len = message.length() / 2;
        char[] chars = message.toCharArray();

        String[] hexStr = new String[len];

        byte[] bytes = new byte[len];

        for (int i = 0, j = 0; j < len; i += 2, j++) {
            hexStr[j] = "" + chars[i] + chars[i + 1];
            bytes[j] = (byte) Integer.parseInt(hexStr[j], 16);
        }
        return bytes;
    }
}

蓝牙基本上到这已经结束了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容