Android BLE開發(fā)

Android上BLE功能的逐步演進

藍牙在Android發(fā)展過程如下:

  • Android4.3開始龙填,開始支持BLE功能周荐,但只支持Central Mode(中心模式)
  • Android5.0開始,開始支持Peripheral Mode(外圍模式)

中心模式和外圍模式是什么意思?

  • Central Mode:Android端作為中心設(shè)備,連接其他外圍設(shè)備。
  • Peripheral Mode:Android端作為外圍設(shè)備疆股,被其他中心設(shè)備連接。在Android5.0支持外圍模式之后倒槐,才實現(xiàn)了兩臺Android手機通過BLE進行相互通信。

BLE通信基礎(chǔ)

  • ATT:全稱 Attribute Protocol附井,中文名"屬性協(xié)議"讨越。它是BLE通信的基礎(chǔ)。ATT把數(shù)據(jù)封裝永毅,向外暴露為"屬性"把跨,提供"屬性"的為服務(wù)端,獲取"屬性"的為客戶端沼死。ATT是專門為低功耗藍牙設(shè)計的着逐,結(jié)構(gòu)非常簡單,數(shù)據(jù)長度很短意蛀。
  • GATT:全稱 Generic Attribute Profile耸别,中文名"通用屬性配置文件"。它是在ATT的基礎(chǔ)上县钥,對ATT進行的進一步邏輯封裝秀姐,定義數(shù)據(jù)的交互方式和含義。GATT是我們做BLE開發(fā)的時候直接接觸的概念若贮。
  • GATT層級:GATT按照層級定義了4個概念:配置文件(profile)省有、服務(wù)(Service)痒留、特征(Characteristic)和描述(Descriptor)。他們的關(guān)系是這樣的蠢沿,profile定義了一個實際的應(yīng)用場景伸头,一個profile包含若干個service,一個service包含若干個characteristic舷蟀,一個characteristic可以包含若干個descriptor恤磷。


    GATT層級
  • Profile
    Profile 并不是實際存在于 BLE 外設(shè)上的,它只是一個被 Bluetooth SIG 或者外設(shè)設(shè)計者預(yù)先定義的 Service 的集合雪侥。例如心率Profile(Heart Rate Profile)就是結(jié)合了 Heart Rate Service 和 Device Information Service碗殷。所有官方通過 GATT Profile 的列表可以從這里找到。

  • Service
    Service 是把數(shù)據(jù)分成一個個的獨立邏輯項速缨,它包含一個或者多個 Characteristic锌妻。每個 Service 有一個 UUID 唯一標識。 UUID 有 16 bit 的旬牲,或者 128 bit 的仿粹。16 bit 的 UUID 是官方通過認證的,需要花錢購買原茅,128 bit 是自定義的吭历,這個就可以自己隨便設(shè)置。官方通過了一些標準 Service擂橘,完整列表在這里晌区。以 Heart Rate Service為例,可以看到它的官方通過 16 bit UUID 是 0x180D通贞,包含 3 個 Characteristic:Heart Rate Measurement, Body Sensor LocationHeart Rate Control Point朗若,并且定義了只有第一個是必須的,它是可選實現(xiàn)的昌罩。

  • Characteristic
    需要重點提一下Characteristic哭懈, 它定義了數(shù)值和操作,包含一個Characteristic聲明茎用、Characteristic屬性遣总、值、值的描述(Optional)轨功。通常我們講的 BLE 通信旭斥,其實就是對 Characteristic 的讀寫或者訂閱通知。比如在實際操作過程中夯辖,我對某一個Characteristic進行讀琉预,就是獲取這個Characteristic的value。

  • UUID
    Service蒿褂、Characteristic 和 Descriptor 都是使用 UUID 唯一標示的圆米。

    UUID 是全局唯一標識卒暂,它是 128bit 的值,為了便于識別和閱讀娄帖,一般以 “8位-4位-4位-4位-12位”的16進制標示也祠,比如“12345678-abcd-1000-8000-123456000000”。

    但是近速,128bit的UUID 太長诈嘿,考慮到在低功耗藍牙中,數(shù)據(jù)長度非常受限的情況削葱,藍牙又使用了所謂的 16 bit 或者 32 bit 的 UUID奖亚,形式如下:“0000XXXX-0000-1000-8000-00805F9B34FB”。除了 “XXXX” 那幾位以外析砸,其他都是固定昔字,所以說,其實 16 bit UUID 是對應(yīng)了一個 128 bit 的 UUID首繁。這樣一來作郭,UUID 就大幅減少了,例如 16 bit UUID只有有限的 65536(16的四次方) 個弦疮。與此同時夹攒,因為數(shù)量有限,所以 16 bit UUID 并不能隨便使用胁塞。藍牙技術(shù)聯(lián)盟已經(jīng)預(yù)先定義了一些 UUID咏尝,我們可以直接使用,比如“00001011-0000-1000-8000-00805F9B34FB”就一個是常見于BLE設(shè)備中的UUID啸罢。當然也可以花錢定制自定義的UUID状土。

作為外圍設(shè)備開發(fā)實現(xiàn)

1.聲明權(quán)限

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  • android.permission.BLUETOOTH:這個權(quán)限允許程序連接到已配對的藍牙設(shè)備
  • android.permission.BLUETOOTH_ADMIN:這個權(quán)限允許程序發(fā)現(xiàn)和配對藍牙設(shè)備
  • android.permission.ACCESS_COARSE_LOCATION和android.permission_ACCESS_FINE_LOCATION這兩個權(quán)限是Android6.0后必須添加的,藍牙掃描周圍設(shè)備需要獲取模糊的位置信息伺糠。這兩個權(quán)限屬于同一組危險權(quán)限,在AndroidManifest文件聲明后還需要在運行時動態(tài)獲取斥季。

2.開發(fā)流程

1.獲取藍牙設(shè)備管理類
mBluetoothManager =(BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE)
2.獲取藍牙適配器
mAdapter = mBluetoothManager.getAdapter();
3.獲取服務(wù)
mService = new BluetoothGattService(UUID.fromString(SERVICE_WIFI), BluetoothGattService.SERVICE_TYPE_PRIMARY);
4.獲取一個特征
charWifiNameAndPassword = new BluetoothGattCharacteristic(UUID.fromString(CHARACTERISTIC_SET_WIFI_NAME_AND_PASSWORD), BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_WRITE | BluetoothGattCharacteristic.PERMISSION_READ);
5.獲取一個描述
BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(UUID.fromString(CHARACTERISTIC_CONFIG), BluetoothGattDescriptor.PERMISSION_READ);
6.將描述加入到特征中
charWifiNameAndPassword.addDescriptor(descriptor);
7.將特征加入到服務(wù)中
mService.addCharacteristic(charWifiNameAndPassword);
8.獲取Server
mBluetoothGattServer = mBluetoothManager.openGattServer(mContext, new BluetoothGattServerCallback() {....}
9.將服務(wù)加入到Server
mBluetoothGattServer.addService(mService);
10.開啟ble廣播
mAdvertiser.startAdvertising(settings, data, new AdvertiseCallback() {......}

3.代碼實現(xiàn)

public class BleManager {
    public static final String TAG = BleManager.class.getName();
    private Context mContext;
    private static BleManager manager;
    //藍牙設(shè)備管理類
    private BluetoothManager mBluetoothManager;
    //藍牙設(shè)配器
    private BluetoothAdapter mAdapter;
    private BluetoothLeAdvertiser mAdvertiser;
    //藍牙Server
    private BluetoothGattServer mBluetoothGattServer;
    //藍牙服務(wù)
    private BluetoothGattService mService;
    //藍牙特征
    private BluetoothGattCharacteristic charWifiNameAndPassword;
    //服務(wù)UUID
    public static final String SERVICE_WIFI = "00001111-0000-1000-8000-00805f9b34fb";
    //特征UUID
    public static final String CHARACTERISTIC_SET_WIFI_NAME_AND_PASSWORD = "00008888-0000-1000-8000-00805f9b34fb";
    //描述UUID
    public static final String CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb";

    public static BleManager getInstance(Context context) {
        if (manager == null) {
            manager = new BleManager(context);
        }
        return manager;
    }

    public BleManager(Context context) {
        mContext = context;
        //1.獲取管理類
        mBluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
        //判斷設(shè)備是否支持藍牙
        if (mBluetoothManager == null) {
            return;
        }
        //2.獲取藍牙適配器
        mAdapter = mBluetoothManager.getAdapter();
        if (!mAdapter.isEnabled()) {
            mAdapter.enable();
        }
    }

    /**
     * 開啟藍牙并開啟ble廣播
     */
    public synchronized void openBle() {
        //如果是調(diào)用closeBle來關(guān)閉藍牙的训桶,會將bluetoothAdapter,bluetoothReceiver置為null,需要重新賦值
        if (mAdapter == null) {
            mAdapter = mBluetoothManager.getAdapter();
        }
        if (!mAdapter.isEnabled()) {
            mAdapter.enable();
        }
        if (Build.VERSION.SDK_INT >= 21 && mAdapter.isMultipleAdvertisementSupported()) {
            if (initService()) {
                //10.啟動ble廣播
                startAdvertise();
            } else {
                Log.d(TAG, "開啟ble失敗");
            }
        } else {
            Log.d(TAG, "開啟ble失敗");
            if (!mAdapter.isMultipleAdvertisementSupported()) {
                Log.d(TAG, "您的設(shè)備不支持藍牙從模式");
            }
        }

    }

    /**
     * 開啟ble廣播
     */
    private void startAdvertise() {
        mAdvertiser = mAdapter.getBluetoothLeAdvertiser();
        AdvertiseSettings settings = new AdvertiseSettings.Builder()
                .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
                .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
                .setConnectable(true)
                .setTimeout(0)
                .build();
        ParcelUuid parcelUuid = new ParcelUuid(UUID.fromString(SERVICE_WIFI));
        AdvertiseData data = new AdvertiseData.Builder()
                .setIncludeDeviceName(true)
                .addServiceUuid(parcelUuid)
                .build();
        mAdvertiser.startAdvertising(settings, data, new AdvertiseCallback() {
            @Override
            public void onStartSuccess(AdvertiseSettings settingsInEffect) {
                super.onStartSuccess(settingsInEffect);
                Log.d(TAG, "開啟ble廣播成功");
            }

            @Override
            public void onStartFailure(int errorCode) {
                super.onStartFailure(errorCode);
                Log.d(TAG, "開啟ble廣播失敗");
            }
        });

    }

    private boolean initService() {
        //3.獲取服務(wù)
        mService = new BluetoothGattService(UUID.fromString(SERVICE_WIFI), BluetoothGattService.SERVICE_TYPE_PRIMARY);
        //4.獲取一個特征
        charWifiNameAndPassword = new BluetoothGattCharacteristic(UUID.fromString(CHARACTERISTIC_SET_WIFI_NAME_AND_PASSWORD), BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_READ, BluetoothGattCharacteristic.PERMISSION_WRITE | BluetoothGattCharacteristic.PERMISSION_READ);
        //5.獲取一個描述
        BluetoothGattDescriptor descriptor = new BluetoothGattDescriptor(UUID.fromString(CHARACTERISTIC_CONFIG), BluetoothGattDescriptor.PERMISSION_READ);
        descriptor.setValue("WIFI ACCOUNT".getBytes(Charset.forName("UTF-8")));
        //6.將描述加入到特征中
        charWifiNameAndPassword.addDescriptor(descriptor);
        //7.將特征加入到服務(wù)中
        mService.addCharacteristic(charWifiNameAndPassword);
        //8.獲取周邊
        mBluetoothGattServer = mBluetoothManager.openGattServer(mContext, new BluetoothGattServerCallback() {
            @Override
            public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
                super.onConnectionStateChange(device, status, newState);
                //連接狀態(tài)發(fā)生變化回調(diào)
                Log.d(TAG, "連接狀態(tài)發(fā)生改變:" + newState);
            }

            @Override
            public void onServiceAdded(int status, BluetoothGattService service) {
                super.onServiceAdded(status, service);
                //當周邊添加到服務(wù)成功時的回調(diào)
                Log.d(TAG, "服務(wù)添加成功");
            }

            @Override
            public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
                super.onCharacteristicReadRequest(device, requestId, offset, characteristic);
                //當遠程設(shè)備請求讀取本地特征時回調(diào)
                //必須調(diào)用BluetoothGattServer.sendResponse
                Log.d(TAG, "遠程設(shè)備讀取本地特征");
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
            }

            @Override
            public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
                super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value);
                //當遠程設(shè)備請求寫入本地特征時回調(diào)
                //通常我們講的BLE通信酣倾,其實就說對characteristic的讀寫或者訂閱
                //必須調(diào)用BluetoothGattServer.sendResponse
                try {
                    Log.d(TAG, "遠程設(shè)備寫入本地特征:" + new String(value, "UTF-8"));
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, value);
            }

            @Override
            public void onDescriptorReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattDescriptor descriptor) {
                super.onDescriptorReadRequest(device, requestId, offset, descriptor);
                //當遠程設(shè)備請求讀取本地描述時回調(diào)
                //必須調(diào)用BluetoothGattServer.sendResponse
                Log.d(TAG, "遠程設(shè)備讀取本地描述");
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
            }

            @Override
            public void onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
                super.onDescriptorWriteRequest(device, requestId, descriptor, preparedWrite, responseNeeded, offset, value);
                //當遠程設(shè)備請求寫入本地描述時回調(diào)
                //必須調(diào)用BluetoothGattServer.sendResponse
                Log.d(TAG, "遠程設(shè)備寫入本地描述:" + value);
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
            }

            @Override
            public void onExecuteWrite(BluetoothDevice device, int requestId, boolean execute) {
                super.onExecuteWrite(device, requestId, execute);
                //執(zhí)行本地設(shè)備所有掛起的寫操作
                //必須調(diào)用BluetoothGattServer.sendResponse
                Log.d(TAG, "執(zhí)行所有掛起的寫操作");
                mBluetoothGattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null);
            }

            @Override
            public void onNotificationSent(BluetoothDevice device, int status) {
                super.onNotificationSent(device, status);
                //當通知發(fā)送到遠程設(shè)備時的回調(diào)
                Log.d(TAG, "通知發(fā)送成功");
            }

            /**
             * MTU(Maxximum Transmission Unit)最大傳輸單元:指在一個協(xié)議數(shù)據(jù)單元中(PDU,Protocol Data Unit)有效的最大傳輸Byte
             * AndroidMTU一般為23舵揭,發(fā)送長包需要更改MTU(5.1(API21)開始支持MTU修改)或者分包發(fā)送
             * core spec里面定義了ATT的默認MTU為23bytes,除去ATT的opcode一個字節(jié)以及ATT的handle2個字節(jié)后躁锡,剩余20個bytes留給GATT
             * MTU是不可以協(xié)商的午绳,只是通知對方,雙方在知道對方的極限后會選擇一個較小的值作為以后的MTU
             */
            @Override
            public void onMtuChanged(BluetoothDevice device, int mtu) {
                super.onMtuChanged(device, mtu);
                //MTU更改時的回調(diào)
                Log.d(TAG, "MTU發(fā)生更改:" + mtu);
            }

            /**
             * PHY(Physical):物理接口收發(fā)器映之,實現(xiàn)OSI模型的物理層
             * 當調(diào)用BluetoothGattServer.setPreferredPhy時拦焚,或者遠程設(shè)備更改了PHY時回調(diào)
             * 低功耗藍牙5.0協(xié)議中蜡坊,定義了兩種調(diào)制方案。這兩種方案都采用了GFSK調(diào)制赎败。區(qū)別在于symbol rate不同秕衙,一種1 Msym/s,另一種2Msym/s僵刮。
             * 其中1 Msym/s是符合低功耗藍牙5.0協(xié)議的設(shè)備所必須支持的据忘。
             * 在1 Msym/s調(diào)制下,低功耗藍牙5.0協(xié)議定義了兩種PHY:(1)LE 1MPHY ,即信息數(shù)據(jù)不變嗎怨愤,信息數(shù)據(jù)的傳輸速率就為1Mb/s
             *                                                 (2)LE Coded PHY嘶摊,即信息數(shù)據(jù)編碼方式署辉,信息數(shù)據(jù)的傳輸速率為125kb/s或者500kb/s
             * 在2 Msym/s調(diào)制下,低功耗藍牙5.0協(xié)議僅定義了一種PHY:LE 2MPHY,即信息數(shù)據(jù)不編碼汉规,信息數(shù)據(jù)的傳輸速率就為2Mb/s
             */
            @Override
            public void onPhyUpdate(BluetoothDevice device, int txPhy, int rxPhy, int status) {
                super.onPhyUpdate(device, txPhy, rxPhy, status);
                //當調(diào)用了BluetoothGattServer.setPreferredPhy時,或者遠程設(shè)備更改了PHY時回調(diào)
                Log.d(TAG, "onPhyUpdate");
            }

            @Override
            public void onPhyRead(BluetoothDevice device, int txPhy, int rxPhy, int status) {
                super.onPhyRead(device, txPhy, rxPhy, status);
                //當調(diào)用了BluetoothGattServer.readPhy時回調(diào)
                Log.d(TAG, "onPhyRead");
            }
        });
        //9.將服務(wù)加入到周邊
        return mBluetoothGattServer.addService(mService);

    }

}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末辈赋,一起剝皮案震驚了整個濱河市鲫忍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钥屈,老刑警劉巖悟民,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異篷就,居然都是意外死亡射亏,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門竭业,熙熙樓的掌柜王于貴愁眉苦臉地迎上來智润,“玉大人,你說我怎么就攤上這事未辆】弑粒” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵咐柜,是天一觀的道長兼蜈。 經(jīng)常有香客問我,道長拙友,這世上最難降的妖魔是什么为狸? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮遗契,結(jié)果婚禮上辐棒,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好漾根,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布泰涂。 她就那樣靜靜地躺著,像睡著了一般立叛。 火紅的嫁衣襯著肌膚如雪负敏。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天秘蛇,我揣著相機與錄音其做,去河邊找鬼。 笑死赁还,一個胖子當著我的面吹牛妖泄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播艘策,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼蹈胡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了朋蔫?” 一聲冷哼從身側(cè)響起罚渐,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驯妄,沒想到半個月后荷并,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡青扔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年源织,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片微猖。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡谈息,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出凛剥,到底是詐尸還是另有隱情侠仇,我是刑警寧澤,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布犁珠,位于F島的核電站傅瞻,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏盲憎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一胳挎、第九天 我趴在偏房一處隱蔽的房頂上張望饼疙。 院中可真熱鬧,春花似錦、人聲如沸窑眯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽磅甩。三九已至炊林,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間卷要,已是汗流浹背渣聚。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留僧叉,地道東北人奕枝。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像瓶堕,于是被迫代替她去往敵國和親隘道。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容