【Android BLE】藍牙開發「防丟器」的相關知識點(一):掃描并識別設備

關于BLE相關的知識,借助于之前做的一個應用(防丟器),在此做一個整理筆記。怕自己懶,先立個FLAG,這將是一個系列的文章:

1.準備知識:

  • BLE:Bluetooth Low Energy

  • Github上的庫:Bluetooth-LE-Library---Android(雖然沒有引入,但是他這個demo里有另外幾個類寫的很好,可直接用)

  • 其他介紹BLE原理和工作機制的文章(本文主要從代碼角度入手)

  • 應用簡要邏輯介紹: a.掃描并篩選出特定的設備,不是所有藍牙設備都展示,只顯示自家的;b.連接并寫入認證信息到ble設備,該認證視定制情況,實現設備綁定;c.定時檢測已綁定設備連接狀態,未連接的由應用發起主動連接;d.監聽藍牙設備狀態,比如設備發過來的指令、斷開事件、信號強度;e.根據設備狀態進行對應提醒,如斷開報警,強度弱時提示距離偏大,接收到ble設備的單擊事件時執行拍照(這里其實還充當了自拍器按鍵,但是我并沒有找到直接控制系統相機快門的方法,采用的是自己搞了一個拍照界面⊙﹏⊙‖∣,有人會的話,請留言,萬分感謝),接收到雙擊事件時手機報警(相當于尋找一時不知道丟到哪里的手機);f.設備以及應用的一些設置,如勿擾區域、勿擾時段、記錄設備定位信息等;g.最后還有一個設備升級,可自行參考CRS相關support文檔。

2.權限申請以及最低API設置

這一步是很容易被忽略但又非常關鍵的,被這個坑到過(┬_┬)

  1. AndroidManifest.xml主配置文件里添加:
<!-- 藍牙相關 -->
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

<uses-feature
    android:name="android.hardware.bluetooth_le"
    android:required="true"/>
  1. build.gradle里如下設置:
defaultConfig {
    applicationId "com.powerstick.beaglepro"
    minSdkVersion 18
    targetSdkVersion 19
    versionCode 8
    versionName "1.0.8"
}

3.初始化藍牙掃描相關類并掃描、解析、展示(用到android.bluetooth.*)

1.BluetoothLeScanner.java如下

import android.bluetooth.BluetoothAdapter;
import android.os.Handler;

import com.tencent.bugly.crashreport.BuglyLog;

public class BluetoothLeScanner {
    private final String TAG = "BluetoothLeScanner";

    private final Handler mHandler;
    private final BluetoothAdapter.LeScanCallback mLeScanCallback;
    private final BluetoothUtils mBluetoothUtils;
    private boolean mScanning;

    public BluetoothLeScanner(final BluetoothAdapter.LeScanCallback leScanCallback, final BluetoothUtils
            bluetoothUtils) {
        mHandler = new Handler();
        mLeScanCallback = leScanCallback;
        mBluetoothUtils = bluetoothUtils;
    }

    public boolean isScanning() {
        return mScanning;
    }

    public void scanLeDevice(final int duration, final boolean enable) {
        if (enable) {
            if (mScanning) {
                return;
            }
            BuglyLog.d(TAG, "~ Starting Scan");
            if (duration > 0) {
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        BuglyLog.d(TAG, "~ Stopping Scan (timeout)");
                        mScanning = false;
                        mBluetoothUtils.getBluetoothAdapter().stopLeScan(mLeScanCallback);
                    }
                }, duration);
            }
            mScanning = true;
            mBluetoothUtils.getBluetoothAdapter().startLeScan(mLeScanCallback);
        } else {
            BuglyLog.d(TAG, "~ Stopping Scan");
            mScanning = false;
            mBluetoothUtils.getBluetoothAdapter().stopLeScan(mLeScanCallback);
        }
    }
}

2.在Activity中實例化并進行掃描

// 藍牙信息相關
private BluetoothUtils mBluetoothUtils;
private BluetoothLeScanner mScanner;

mBluetoothUtils = new BluetoothUtils(this);
mScanner = new BluetoothLeScanner(mLeScanCallback, mBluetoothUtils);
startScan();

private void startScan() {
    final boolean mIsBluetoothOn = mBluetoothUtils.isBluetoothOn();
    final boolean mIsBluetoothLePresent = mBluetoothUtils.isBluetoothLeSupported();

    mBluetoothUtils.askUserToEnableBluetoothIfNeeded();
    if (mIsBluetoothOn && mIsBluetoothLePresent) {
        mScanner.scanLeDevice(-1, true);
    }
}

*以上代碼沒有注釋應該沒問題,這其中還有判斷手機是否打開藍牙的代碼行,需注意。

3.解析掃描得到的結果,并根據實際需要在頁面內展示
上面用到的mLeScanCallback如下:

private final BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                BuglyLog.i("--------->", "name:"+device.getName());
                // 這里刪減了業務邏輯,只做部分知識介紹
                // 對于掃描到的設備,進行額外信息的獲取,大多是為了只在掃描配對頁顯示自家產品,剔除一般設備
                List<AdRecord> adRecord = AdRecordUtils.parseScanRecordAsList(scanRecord);
                for (int i = 0; i < adRecord.size(); i++) {

                    AdRecord record = adRecord.get(i);
                    if (record.getType() == 0xFF) {
                        String mac = ByteUtils.byteArrayToHexString(record.getData());
                        // 直接獲取出來的字符串形如:[AA,BB,CC...]處理成AA:BB:CC...的樣子
                        mac = mac.replace("[", "").replace("]", "").replace(", ", ":");
                        BuglyLog.e("****", mac);

                        // 我們的設備額外信息就是MAC
                        if (TextUtils.equals(mac, device.getAddress())) {
                            // 添加到UI頁面,等著點擊配對
                        }
                    }
                }
            }
        });
    }
};

4.相關工具類

1.ByteUtils.java字節處理,在后面的手機與BLE設備通信處經常需要用到

import java.nio.ByteBuffer;

public class ByteUtils {

    private static final String HEXES = "0123456789ABCDEF";

    private ByteUtils() {
    }

    /**
     * 字節數組轉換成16進制的字符串表示: [01, 30, FF, AA]
     */
    public static String byteArrayToHexString(final byte[] array) {
        final StringBuilder sb = new StringBuilder();
        boolean firstEntry = true;
        sb.append('[');

        for (final byte b : array) {
            if (!firstEntry) {
                sb.append(", ");
            }
            sb.append(HEXES.charAt((b & 0xF0) >> 4));
            sb.append(HEXES.charAt((b & 0x0F)));
            firstEntry = false;
        }

        sb.append(']');
        return sb.toString();
    }

    /**
     * 檢查一個字節數組是否以另一個字節數組為開始
     *
     * @param array  the array
     * @param prefix the prefix
     * @return boolean
     */
    public static boolean doesArrayBeginWith(final byte[] array, final byte[] prefix) {
        if (array.length < prefix.length) {
            return false;
        }

        for (int i = 0; i < prefix.length; i++) {
            if (array[i] != prefix[i]) {
                return false;
            }
        }

        return true;
    }

    /**
     * 將一個長度為2的字節數組轉為int
     */
    public static int getIntFrom2ByteArray(final byte[] input) {
        final byte[] result = new byte[4];

        result[0] = 0;
        result[1] = 0;
        result[2] = input[0];
        result[3] = input[1];

        return ByteUtils.getIntFromByteArray(result);
    }

    /**
     * Converts a byte to an int, preserving the sign.
     * <p/>
     * For example, FF will be converted to 255 and not -1.
     *
     * @param bite the byte
     * @return the int from byte
     */
    public static int getIntFromByte(final byte bite) {
        return bite & 0xFF;
    }

    /**
     * Converts a byte array to an int.
     *
     * @param bytes the bytes
     * @return the int from byte array
     */
    public static int getIntFromByteArray(final byte[] bytes) {
        return ByteBuffer.wrap(bytes).getInt();
    }

    /**
     * Converts a byte array to a long.
     *
     * @param bytes the bytes
     * @return the long from byte array
     */
    public static long getLongFromByteArray(final byte[] bytes) {
        return ByteBuffer.wrap(bytes).getLong();
    }


    /**
     * Inverts an byte array in place.
     *
     * @param array the array
     */
    public static void invertArray(final byte[] array) {
        final int size = array.length;
        byte temp;

        for (int i = 0; i < size / 2; i++) {
            temp = array[i];
            array[i] = array[size - 1 - i];
            array[size - 1 - i] = temp;
        }
    }
}

2.AdRecordUtils.java從掃描到的設備中解析各自的額外信息

package com.powerstick.beaglepro.util;

import android.annotation.SuppressLint;
import android.util.SparseArray;

import com.afap.utils.ByteUtils;
import com.powerstick.beaglepro.model.AdRecord;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class AdRecordUtils {

    private AdRecordUtils(){
        // TO AVOID INSTANTIATION
    }

    public static String getRecordDataAsString(final AdRecord nameRecord) {
        if (nameRecord == null) {
            return "";
        }
        return new String(nameRecord.getData());
    }

    public static byte[] getServiceData(final AdRecord serviceData) {
        if (serviceData == null) {
            return null;
        }
        if (serviceData.getType() != AdRecord.TYPE_SERVICE_DATA) return null;

        final byte[] raw = serviceData.getData();
        //Chop out the uuid
        return Arrays.copyOfRange(raw, 2, raw.length);
    }

    public static int getServiceDataUuid(final AdRecord serviceData) {
        if (serviceData == null) {
            return -1;
        }
        if (serviceData.getType() != AdRecord.TYPE_SERVICE_DATA) return -1;

        final byte[] raw = serviceData.getData();
        //Find UUID data in byte array
        int uuid = (raw[1] & 0xFF) << 8;
        uuid += (raw[0] & 0xFF);

        return uuid;
    }

    /*
     * Read out all the AD structures from the raw scan record
     */
    public static List<AdRecord> parseScanRecordAsList(final byte[] scanRecord) {
        final List<AdRecord> records = new ArrayList<>();

        int index = 0;
        while (index < scanRecord.length) {
            final int length = scanRecord[index++];
            //Done once we run out of records
            if (length == 0) break;

            final int type = ByteUtils.getIntFromByte(scanRecord[index]);

            //Done if our record isn't a valid type
            if (type == 0) break;

            final byte[] data = Arrays.copyOfRange(scanRecord, index + 1, index + length);

            records.add(new AdRecord(length, type, data));

            //Advance
            index += length;
        }

        return Collections.unmodifiableList(records);
    }

    @SuppressLint("UseSparseArrays")
    public static Map<Integer, AdRecord> parseScanRecordAsMap(final byte[] scanRecord) {
        final Map<Integer, AdRecord> records = new HashMap<>();

        int index = 0;
        while (index < scanRecord.length) {
            final int length = scanRecord[index++];
            //Done once we run out of records
            if (length == 0) break;

            final int type = ByteUtils.getIntFromByte(scanRecord[index]);

            //Done if our record isn't a valid type
            if (type == 0) break;

            final byte[] data = Arrays.copyOfRange(scanRecord, index + 1, index + length);

            records.put(type, new AdRecord(length, type, data));

            //Advance
            index += length;
        }

        return Collections.unmodifiableMap(records);
    }

    public static SparseArray<AdRecord> parseScanRecordAsSparseArray(final byte[] scanRecord) {
        final SparseArray<AdRecord> records = new SparseArray<>();

        int index = 0;
        while (index < scanRecord.length) {
            final int length = scanRecord[index++];
            //Done once we run out of records
            if (length == 0) break;

            final int type = ByteUtils.getIntFromByte(scanRecord[index]);

            //Done if our record isn't a valid type
            if (type == 0) break;

            final byte[] data = Arrays.copyOfRange(scanRecord, index + 1, index + length);

            records.put(type, new AdRecord(length, type, data));

            //Advance
            index += length;
        }

        return records;
    }
}

以上,基本涵蓋了藍牙設備掃描與篩選,頁面如何顯示要根據實際需要了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容