Android Ble藍芽入門

不用89k發表於2020-10-24

Android BLE藍芽入門

一、什麼是BLE藍芽

google官方對BLE藍芽的解釋
簡述:API級別:Android 4.3(API 級別 18)引入。低功耗藍芽區別於“經典藍芽”。
侷限:最多隻支援20個資料(後面會展示)。

低功耗藍芽優勢:1.低功耗,使用鈕釦電池就可執行數月至數年;2.小體積、低成本;3.與現有的大部分手機、平板電腦和計算機相容。(百度百科)

二、硬體準備工作

1.藍芽開發模組(如果有現成的模組可以直接進行除錯)
藍芽開發模組
2.串列埠除錯工具(文章末尾會給出軟體的下載方式)

串列埠工具

3.支援BLE藍芽的Android手機。
在這裡插入圖片描述

三、準備開發

1.許可權新增
  <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <!-- 僅支援低耗藍芽 -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

    <uses-feature
        android:name="android.hardware.bluetooth_le"
        android:required="true" />
2.檢查開關和許可權

搜尋裝置準備:
1.當前裝置是否支援BLE藍芽功能
2.裝置的藍芽功能是否處於開啟狀態
3.判斷裝置的api是否需要開啟定位許可權
(PS:至於為什麼要開啟定位許可權,這你得問Google了)
3.1GPS是否開啟了
3.2是否擁有GPS許可權,需要使用GPS才能使用藍芽裝置
這裡的藍芽,定位開關狀態,許可權獲取比較麻煩但是不復雜
程式碼:

package com.my.mwble;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.Toast;

import com.my.mwble.util.BleUtil;
import com.my.mwble.util.GpsUtil;
import com.my.mwble.util.LogUtil;
import com.my.mwble.util.ToastUtil;

/**
 * Created by Android Studio.
 * User: mwb
 * Date: 2020/10/24 0024
 * Time: 上午 11:32
 * Describe:BLE藍芽基礎
 */
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private BluetoothAdapter bluetoothAdapter;
    private static int REQUEST_ENABLE_BT = 1; // 開啟藍芽頁面請求程式碼
    private static final int REQUEST_CODE_ACCESS_COARSE_LOCATION = 1; // 位置許可權
    private static final int SET_GPS_OPEN_STATE = 2; // 設定GPS是否開啟了
    private static final int REQUEST_STORY_CODE = 3; // 檔案讀取許可權

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
        initDevice();
    }

    private void initView() {
        findViewById(R.id.btn_seach).setOnClickListener(this);
    }

    private void initDevice() {
        BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
        bluetoothAdapter = bluetoothManager.getAdapter();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_seach: // 搜尋藍芽裝置
                seach();
                break;
        }
    }

    /**
     * 搜尋裝置
     * 1.當前裝置是否支援BLE藍芽功能
     * 2.裝置的藍芽功能是否處於開啟狀態
     * 2.1 沒有開啟則去開啟
     * 3.判斷裝置的api是否需要開啟定位許可權
     * (PS:至於為什麼要開啟定位許可權,這你得問Google了)
     * 3.1GPS是否開啟了
     * 3.2是否擁有GPS許可權,需要使用GPS才能使用藍芽裝置
     */
    private void seach() {
        // 當前的系統版本 < Android 4.3 API=18,目前市面大部分系統都在6.0了...這個判斷幾乎可以不用寫了。可省略
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            ToastUtil.show(this, "當前裝置系統版本不支援BLE藍芽功能!請升級系統版本到4.3以上");
            return;
        }

        //1. 當前裝置是否支援BLE藍芽裝置
        if (BleUtil.checkDeviceSupportBleBlueTooth(this)) {
            // 2.判斷藍芽裝置是否開啟了
            if (checkBlueIsOpen()) {
                // 3.斷裝置的api是否需要開啟定位許可權
                checkGPS();
            } else { // 沒有開啟,跳轉到系統藍芽頁面
                Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
                startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
            }
        } else {
            ToastUtil.show(this, "當前裝置不支援BLE藍芽功能!");
        }
    }

    /**
     * GPS是否開啟了
     */
    private void checkGPS() {
        // 3.1GPS是否開啟了
        if (GpsUtil.isOPen(this)) { // GPS已經開啟了
            checkGpsPermission();
        } else {
            // 3.2是否擁有GPS許可權,需要使用GPS才能使用藍芽裝置
            tipGPSSetting();
        }
    }

    /**
     * 藍芽是否開啟了
     *
     * @return true 開啟了,false 沒有開啟
     */
    private boolean checkBlueIsOpen() {
        if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * 搜尋藍芽裝置
     */
    private void seachBlueTooth() {
        ToastUtil.show(this, "開始搜尋藍芽裝置");
    }

    /**
     * 提示需要開啟藍芽
     */
    private void tipGPSSetting() {
        AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
        builder.setTitle("提示");
        builder.setMessage("安卓6.0以後使用藍芽需要開啟定位功能,但本應用不會使用到您的位置資訊,開始定位只是為了掃描到藍芽裝置。是否確定開啟");
        builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                GpsUtil.openGPS(MainActivity.this, SET_GPS_OPEN_STATE);
            }
        });

        builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                ToastUtil.show(MainActivity.this, "您無法使用此功能");
            }
        });
        builder.show();
    }

    /**
     * 藍芽需要的定位許可權
     */
    private void checkGpsPermission() {
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { // 如果當前版本是9.0(包含)以下的版本
            if (ActivityCompat.checkSelfPermission(this,
                    Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
                    || ActivityCompat.checkSelfPermission(this,
                    Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
                String[] strings =
                        {Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION};
                ActivityCompat.requestPermissions(this, strings, REQUEST_CODE_ACCESS_COARSE_LOCATION);
            } else {
                seachBlueTooth();
            }
        } else {
            // 10.0系統
            if (ActivityCompat.checkSelfPermission(this,
                    Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
                    || ActivityCompat.checkSelfPermission(this,
                    Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
                    || ActivityCompat.checkSelfPermission(this,
                    "android.permission.ACCESS_BACKGROUND_LOCATION") != PackageManager.PERMISSION_GRANTED) {
                String[] strings = {android.Manifest.permission.ACCESS_FINE_LOCATION,
                        android.Manifest.permission.ACCESS_COARSE_LOCATION,
                        "android.permission.ACCESS_BACKGROUND_LOCATION"};
                ActivityCompat.requestPermissions(this, strings, REQUEST_CODE_ACCESS_COARSE_LOCATION);
            } else {
                seachBlueTooth();
            }
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_ENABLE_BT) { // 從藍芽頁面返回了,在檢查一次是否開啟了
            if (checkBlueIsOpen()) {
                // 藍芽開啟了
                seach();
            } else {
                AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                builder.setTitle("提示");
                builder.setMessage("藍芽沒有開啟將無法使用此功能,是否確定開啟");
                builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        seach(); // 再次執行搜尋
                    }
                });

                builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {

                        ToastUtil.show(MainActivity.this, "您無法使用此功能");
                    }
                });
                builder.setCancelable(false);
                builder.show();
            }
        }else if (requestCode == SET_GPS_OPEN_STATE) { // GPS是否開啟了
            if (GpsUtil.isOPen(this)) { // GPS開啟了
                checkGpsPermission();
            } else {
                tipGPSSetting();
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case REQUEST_CODE_ACCESS_COARSE_LOCATION:
                if (grantResults.length > 0
                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 得到了許可權
                    seachBlueTooth();
                } else {

                    AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
                    builder.setTitle("提示");
                    builder.setMessage("安卓6.0以後使用藍芽需要開啟定位功能,但本應用不會使用到您的位置資訊,開啟定位只是為了掃描到藍芽裝置。是否確定開啟");
                    builder.setPositiveButton("確定", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            launchAppDetailsSettings(MainActivity.this);
                        }
                    });

                    builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {

                            ToastUtil.show(MainActivity.this, "您無法使用此功能");
                        }
                    });
                    builder.setCancelable(false);
                    builder.show();

                }
                break;
            default:
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
                break;
        }
    }

    /**
     * 跳轉許可權Activity
     */
    public void launchAppDetailsSettings(Activity activity) {
        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
        intent.setData(Uri.parse("package:" + activity.getPackageName()));

        if (!isIntentAvailable(this, intent)) {
            ToastUtil.show(this, "請手動跳轉到許可權頁面,給予許可權!");
            return;
        }
        activity.startActivity(intent);
    }

    /**
     * 意圖是否可用
     *
     * @param intent The intent.
     * @return {@code true}: yes<br>{@code false}: no
     */
    public boolean isIntentAvailable(Activity activity, Intent intent) {
        return activity
                .getPackageManager()
                .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
                .size() > 0;
    }
}

看一下效果:GIF太大了,就截幾個圖吧…
效果

3.搜尋裝置

注意:搜尋到的裝置可能會多次出現需要我們自己進行篩選
多次出現的裝置

關鍵程式碼:

  /**
     * 搜尋藍芽裝置
     * 建立搜尋callback 返回掃描到的資訊
     * 建立定時任務,在指定時間內結束藍芽掃描,藍芽掃描是一個很耗電的操作!
     */
    private void seachBlueTooth() {
        ToastUtil.show(this, "開始搜尋藍芽裝置");
        mBluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
        mBluetoothLeScanner.startScan(null, createScanSetting(), scanCallback);

        bluetoothAdapter.startDiscovery();

        handler.postDelayed(new Runnable() { // 指定時間內停止藍芽搜尋
            @Override
            public void run() {
                closeSeach();
            }
        }, SCAN_PERIOD);

        deviceData.clear();
    }

    /**
     * 回撥
     */
    private ScanCallback scanCallback = new ScanCallback() {
        @Override
        public void onScanResult(int callbackType, ScanResult result) {
            BluetoothDevice device = result.getDevice();
            LogUtil.i("name:" + result.getDevice().getName() + ";強度:" + result.getRssi());

            if (device != null) {

                if (deviceData.size() > 0) {

                    if (!deviceData.contains(device)) { // 掃描到會有很多重複的資料,剔除,只新增第一次掃描到的裝置
                        deviceData.add(device);
                    }
                } else {
                    deviceData.add(device);
                }
                adapter.setData(deviceData);
            }
        }

        @Override
        public void onBatchScanResults(List<ScanResult> results) {
            super.onBatchScanResults(results);
        }

        @Override
        public void onScanFailed(int errorCode) {
            super.onScanFailed(errorCode);
        }
    };

效果圖:

掃描到的裝置資訊

至此我們就得到了掃描到了裝置資訊了。

4.連線BLE藍芽裝置

概述:每個BLE藍芽裝置都會包含幾個服務Service
而每個Service中還包含了多個Characteristics(特徵)

他們的關係如下圖:
在這裡插入圖片描述
開啟通訊我們還需要繫結指定Service中的Characteristics(特徵)。
至於使用哪個Service或者哪個Characteristics(特徵)需要跟你們的硬體開發人員進行溝通。

獲取當前裝置的Service UUID,和特徵的UUID

關鍵程式碼:

 /**
     * 繫結藍芽
     *
     * @param device
     */
    private void bindBlueTooth(BluetoothDevice device) {
        //連線裝置
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            mBluetoothGatt = device.connectGatt(this,
                    false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
        } else {
            mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
        }
    }

    //定義藍芽Gatt回撥類
    public class mWBluetoothGattCallback extends BluetoothGattCallback {
        //連線狀態回撥
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, final int status, final int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            // status 用於返回操作是否成功,會返回異常碼。
            // newState 返回連線狀態,如BluetoothProfile#STATE_DISCONNECTED、BluetoothProfile#STATE_CONNECTED

            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    //操作成功的情況下
                    if (status == BluetoothGatt.GATT_SUCCESS) {
                        //判斷是否連線碼
                        if (newState == BluetoothProfile.STATE_CONNECTED) {
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    ToastUtil.show(MainActivity.this, "藍芽已連線");
                                    LogUtil.i("裝置已連線上,開始掃描服務");

                                    // 發現服務
                                    mBluetoothGatt.discoverServices();
                                }
                            });
                        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                            //判斷是否斷開連線碼
                            ToastUtil.show(MainActivity.this, "連線已斷開");
                        }
                    } else {
                        //異常碼
                        // 重連次數不大於最大重連次數
                        if (reConnectionNum < maxConnectionNum) {
                            // 重連次數自增
                            reConnectionNum++;
                            LogUtil.i("重新連線:" + reConnectionNum);
                            // 連線裝置
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                                mBluetoothGatt = mBluetoothDevice.connectGatt(MainActivity.this,
                                        false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
                            } else {
                                mBluetoothGatt = mBluetoothDevice.connectGatt(MainActivity.this, false, mGattCallback);
                            }


                        } else {
                            // 斷開連線,失敗回撥
                            ToastUtil.show(MainActivity.this, "藍芽連線失敗,建議重啟APP,或者重啟藍芽,或重啟裝置");
                            closeBLE();
                        }

                    }
                }
            });
        }

        //服務發現回撥
        @Override
        public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);

            if (status == BluetoothGatt.GATT_SUCCESS) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {

                        LogUtil.i("mmmm:" + mBluetoothGatt.getServices().size());
                        for (int i = 0; i < mBluetoothGatt.getServices().size(); i++) {
                            LogUtil.i("mmmm service:" + mBluetoothGatt.getServices().get(i).getUuid());


                            for (int k = 0; k < mBluetoothGatt.getServices().get(i).getCharacteristics().size(); k++) {
                                LogUtil.i("mmmm      Characteristic:" + mBluetoothGatt.getServices().get(i).getCharacteristics().get(k).getUuid());
                            }

                        }
                    }
                });
            }

        }

        @Override
        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
            super.onCharacteristicRead(gatt, characteristic, status);
        }

        //特徵寫入回撥
        @Override
        public void onCharacteristicWrite(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
            super.onCharacteristicWrite(gatt, characteristic, status);
        }

        //外設特徵值改變回撥
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
        }

        //描述寫入回撥
        @Override
        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
            super.onDescriptorWrite(gatt, descriptor, status);
            LogUtil.i("開啟監聽成功");
        }
    }

Service的Uuid,和Characteristics(特徵)的Uuid
在這裡插入圖片描述

配置Uuid連線裝置:

修改 onServicesDiscovered中的程式碼:

  //服務發現回撥
        @Override
        public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);

            if (status == BluetoothGatt.GATT_SUCCESS) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {

//                        LogUtil.i("mmmm:" + mBluetoothGatt.getServices().size());
//                        for (int i = 0; i < mBluetoothGatt.getServices().size(); i++) {
//                            LogUtil.i("mmmm service:" + mBluetoothGatt.getServices().get(i).getUuid());
//
//
//                            for (int k = 0; k < mBluetoothGatt.getServices().get(i).getCharacteristics().size(); k++) {
//                                LogUtil.i("mmmm      Characteristic:" + mBluetoothGatt.getServices().get(i).getCharacteristics().get(k).getUuid());
//                            }
//
//                        }

                        //獲取指定uuid的service
                        BluetoothGattService gattService = mBluetoothGatt.getService(UUID.fromString(UUDI_1));
//                        bluetoothGattServiceList.add(gattService);
                        //獲取到特定的服務不為空
                        if (gattService != null) {
                            LogUtil.i("獲取服務成功!");

                            BluetoothGattCharacteristic gattCharacteristic =
                                    gattService.getCharacteristic(UUID.fromString(CHARACTERISTIC_UUID_1));

                            mGattCharacteristic = gattCharacteristic;

                            if (gattCharacteristic != null) {
                                LogUtil.i("獲取特徵成功!");


                                boolean isEnableNotification = mBluetoothGatt.setCharacteristicNotification(gattCharacteristic, true);

                                if (isEnableNotification) {
                                    LogUtil.i("開啟通知成功!");
                                    //通過GATt實體類將,特徵值寫入到外設中。
                                    mBluetoothGatt.writeCharacteristic(gattCharacteristic);
                                    //如果只是需要讀取外設的特徵值:
                                    //通過Gatt物件讀取特定特徵(Characteristic)的特徵值
                                    mBluetoothGatt.readCharacteristic(gattCharacteristic);
                                } else {
                                    LogUtil.i("開啟通知失敗!");
                                }

                            } else {
                                LogUtil.i("獲取特徵失敗!");
                            }

                        } else {
                            //獲取特定服務失敗
                            LogUtil.i("獲取服務失敗!");
                        }
                    }
                });
            }

        }

修改onCharacteristicChanged中的程式碼

//外設特徵值改變回撥
        @Override
        public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
            super.onCharacteristicChanged(gatt, characteristic);
            final byte[] value = characteristic.getValue();
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    // value為裝置傳送的資料,根據資料協議進行解析
                    LogUtil.i("原始資料:" + new String(characteristic.getValue()));
                    LogUtil.i("裝置傳送資料:" + DigitalTrans.byte2hex(value)); // 這是一個byte轉16進位制的工具類,後面會給完整的程式碼,所以現在不用糾結
                }
            });
        }

再次連線裝置,連線成功後我們來進行測試
在這裡插入圖片描述
資料接收成功。需要注意的地方是,接收到的資料是byte型別的,使用的時候需要自己進行轉化。
至此接收資料完成。

下面我們來看看傳送資料怎麼完成

修改程式碼:

  /**
     * 傳送資料
     * 將輸入的16進位制轉化為byte傳送
     */
    private void sendMsg(String msg) {
        if (null == mGattCharacteristic || null == mBluetoothGatt) {
            ToastUtil.show(MainActivity.this, "請先連線藍芽裝置");
            return;
        }

        mGattCharacteristic.setValue(NumUtil.hexString2Bytes(msg));
        mBluetoothGatt.writeCharacteristic(mGattCharacteristic);
    }

在這裡插入圖片描述
關閉藍芽:

/**
     * 關閉BLE藍芽連線
     */
    public void closeBLE() {
        if (mBluetoothGatt == null) {
            return;
        }
        mBluetoothGatt.disconnect();
        mBluetoothGatt.close();
        mBluetoothGatt = null;
        ToastUtil.show(this, "藍芽已斷開");
    }
至此藍芽的接收和傳送已全部完成。

問題

藍芽傳送資料大於20個的問題:
在這裡插入圖片描述
這明明是40個資料才拆分了啊,你這是不是欺負老實人嗎?
在這裡插入圖片描述
請聽我狡辯:
從XCOM串列埠工具中我勾選了16進位制傳送,所以兩個才是一個16進位制的數值。不知道16進位制的請自行百度,這裡不再贅述。在實際的開發中我們用到的也會是16進位制根據規定的協議進行溝通。

可以看到資料被拆分了,如果資料大於20個需要進行拼包操作。
如果有時間的話我以後會發拼包的功能實現。

作者能力有限,如果有什麼問題,請留言進行溝通。

程式碼和工具類奉上:
XCOM串列埠除錯工具:連結:https://pan.baidu.com/s/1i-9W31CjXd-551mqphi_hg
提取碼:95vv
程式碼:

相關文章