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閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容