1. 程式人生 > >Android-經典藍芽(BT)-建立長連線傳輸短訊息和檔案

Android-經典藍芽(BT)-建立長連線傳輸短訊息和檔案

一.藍芽模組簡介

模組分類
協議對比

從藍芽4.0開始包含兩個藍芽晶片模組:傳統/經典藍芽模組(Classic Bluetooth,簡稱BT)和低功耗藍芽(Bluetooth Low Energy,簡稱BLE)  
經典藍芽是在之前的藍芽1.0,1.2,2.0+EDR,2.1+EDR,3.0+EDR等基礎上發展和完善起來的, 而低功耗藍芽是Nokia的Wibree標準上發展起來的,是完全不同兩個標準。
1.經典藍芽模組(BT)
泛指藍芽4.0以下的模組,一般用於資料量比較大的傳輸,如:語音、音樂、較高資料量傳輸等。
經典藍芽模組可再細分為:傳統藍芽模組和高速藍芽模組。
傳統藍芽模組在2004年推出,主要代表是支援藍芽2.1協議的模組,在智慧手機爆發的時期得到廣泛支援。
高速藍芽模組在2009年推出,速率提高到約24Mbps,是傳統藍芽模組的八倍。 
傳統藍芽有3個功率級別,Class1,Class2,Class3,分別支援100m,10m,1m的傳輸距離

2.低功耗藍芽模組(BLE)
泛指藍芽4.0或更高的模組,藍芽低功耗技術是低成本、短距離、可互操作的魯棒性無線技術,工作在免許可的2.4GHz ISM射頻頻段。
因為BLE技術採用非常快速的連線方式,因此平時可以處於“非連線”狀態(節省能源),
此時鏈路兩端相互間只是知曉對方,只有在必要時才開啟鏈路,然後在儘可能短的時間內關閉鏈路(每次最多傳輸20位元組)。
低功耗藍芽無功率級別,一般傳送功率在7dBm,一般在空曠距離,達到20m應該是沒有問題

Android手機藍芽4.x都是雙模藍芽(既有經典藍芽也有低功耗藍芽),而某些藍芽裝置為了省電是單模(只支援低功耗藍芽)

開發者選經典藍芽,還是BLE?(參考: http://baijiahao.baidu.com/s?id=1594727739470471520&wfr=spider&for=pc )
經典藍芽:   
    1.傳聲音
    如藍芽耳機、藍芽音箱。藍芽設計的時候就是為了傳聲音的,所以是近距離的音訊傳輸的不二選擇。
    現在也有基於WIFI的音訊傳輸方案,例如Airplay等,但是WIFI功耗比藍芽大很多,裝置無法做到便攜。
    因此固定的音響有WIFI的,移動的如耳機、便攜音箱清一色都是基於經典藍芽協議的。

    2.傳大量資料
    例如某些工控場景,使用Android或Linux主控,外掛藍芽遙控裝置的,
    可以使用經典藍芽裡的SPP協議,當作一個無線串列埠使用。速度比BLE傳輸快多了。
    這裡要注意的是,iPhone沒有開放

BLE藍芽:
    耗電低,資料量小,如遙控類(滑鼠、鍵盤),感測裝置(心跳帶、血壓計、溫度感測器、共享單車鎖、智慧鎖、防丟器、室內定位)
    是目前手機和智慧硬體通訊的價效比最高的手段,直線距離約50米,一節5號電池能用一年,傳輸模組成本10塊錢,遠比WIFI、4G等大資料量的通訊協議更實用。
    雖然藍芽距離近了點,但勝在直連手機,價格超便宜。以室內定位為例,商場每家門店掛個藍芽beacon,
    就可以對手機做到精度10米級的室內定位,一個beacon的價格也就幾十塊錢而已

雙模藍芽:
    如智慧電視遙控器、降噪耳機等。很多智慧電視配的遙控器帶有語音識別,需要用經典藍芽才能傳輸聲音。
    而如果做複雜的按鍵,例如原本鍵盤表上沒有的功能,經典藍芽的HID按鍵協議就不行了,得用BLE做私有協議。
    包括很多降噪耳機上通過APP來調節降噪效果,也是通過BLE來實現的私有通訊協議。

二.Android 經典藍芽(Classic Bluetooth)的API簡介

本文介紹經典藍芽,經典藍芽適用於電池使用強度較大的操作,例如Android之間流式傳輸和通訊等(音訊/檔案等大資料)。 
從Android 4.3(API 18)才有API支援低功耗藍芽(BLE),BLE相關API下篇再介紹。
經典藍芽API如下:
android.bluetooth
.BluetoothA2dp 音訊分發配置檔案,高質量音訊通過藍芽連線和流式傳輸
.BluetoothAdapter 本地藍芽介面卡,是所有藍芽互動的入口,發現裝置,查詢配對裝置,建立BluetoothServerSocket偵聽其他裝置
.BluetoothAssignedNumbers
.BluetoothClass 描述藍芽裝置的一般特徵和功能,這是一組只讀屬性,裝置型別提示
.BluetoothDevice 遠端藍芽裝置,與某個遠端裝置建立連線,查詢裝置資訊,名稱,地址,類和配對狀態
.BluetoothHeadset 提供藍芽耳機支援,以便與手機配合使用,藍芽耳機和擴音配置檔案
.BluetoothHealth  控制藍芽服務的健康裝置配置檔案代理
.BluetoothHealthAppConfiguration 第三方藍芽健康應用註冊的應用配置,以便與遠端藍芽健康裝置通訊
.BluetoothHealthCallback 實現 BluetoothHealth 回撥的抽象類
.BluetoothManager 
.BluetoothProfile 藍芽配置檔案,藍芽通訊的無線介面規範
.BluetoothServerSocket 服務端監聽,連線RFCOMM通道(類似TCP ServerSocket)
.BluetoothSocket 建立RFCOMM通道,藍芽Socket介面(類似TCP Socket),通過InputStream和OutputStream與其他裝置傳輸資料

Android經典藍芽的開發步驟如下:
    1.掃描其他藍芽裝置
    2.查詢本地藍芽介面卡的配對藍芽裝置
    3.建立 RFCOMM 通道 (SPP協議)
    4.通過服務發現連線到其他裝置
    5.與其他裝置進行雙向資料傳輸
    6.管理多個連線

RFCOMM是藍芽簡單傳輸協議, 在兩個藍芽裝置間的一條物理鏈上提供多個模擬串列埠進行傳輸資料, 可同時保持高達60路的通訊連線。
SPP(Serial Port Profile)是通過藍芽裝置之間的串列埠進行資料傳輸協議,spp協議處於RFCOMM上層,
如果能使用RFCOMM傳輸資料,就不需要使用SPP(省去一些流程,速度更快),但還是推薦用SPP,相容性有保證

三.經典藍芽-客戶端和服務端建立長連線,傳輸短訊息/檔案

1.藍芽許可權和設定藍芽

(1).在manifest中新增許可權  
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> 
<!--建立藍芽連線和傳輸許可權-->
<uses-permission android:name="android.permission.BLUETOOTH" /> 
<!--掃描藍芽裝置或修改藍芽設定許可權-->
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />   
<!--Android 6.0及後續版本掃描藍芽,需要定位許可權(進入GPS設定,可以看到藍芽定位)-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

(2).在Activity中設定藍芽
// 檢查藍芽開關
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null) {
    Util.toast(this, "本機沒有找到藍芽硬體或驅動!");
    finish();
    return;
} else {
    if (!adapter.isEnabled()) {
        //直接開啟藍芽
        adapter.enable();
        //跳轉到設定介面
        //startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE), 112);
    }
}

// Android 6.0動態請求許可權
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    String[] permissions = {Manifest.permission.WRITE_EXTERNAL_STORAGE
            , Manifest.permission.READ_EXTERNAL_STORAGE
            , Manifest.permission.ACCESS_COARSE_LOCATION};
    for (String str : permissions) {
        if (checkSelfPermission(str) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(permissions, 111);
            break;
        }
    }
}

2.客戶端-掃描經典藍芽裝置(不包含BLE藍芽裝置)

// 1.獲取已配對裝置
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
List<BluetoothDevice> devices = new ArrayList<>();
devices.addAll(pairedDevices);

// 2.獲取未配對裝置
BroadcastReceiver mReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();         
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {//獲取未配對裝置
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            devices.add(device);
        }
    }
};
registerReceiver(mReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
BluetoothAdapter.getDefaultAdapter().startDiscovery(); //開始掃描裝置

3.客戶端-建立連線

通用唯一識別符號(UUID)是用於唯一標識資訊的字串ID的128位標準化格式,UUID足夠龐大,不會發生衝突。 
藍芽MAC相當於TCP的IP,藍芽UUID相當於TCP的埠,用於標識服務端藍芽程序,
所以客戶端和服務端兩個UUID必須一致!

/**
 * 客戶端,與服務端建立長連線
 */
public class BtClient extends BtBase {
    public BtClient(Listener listener) {
        super(listener);
    }

    /**
     * 與遠端裝置建立長連線
     * @param dev 遠端裝置
     */
    public void connect(BluetoothDevice dev) {
        close();
        try {
        // final BluetoothSocket socket = dev.createRfcommSocketToServiceRecord(SPP_UUID); //加密傳輸,Android強制執行配對,彈窗顯示配對碼
            final BluetoothSocket socket = dev.createInsecureRfcommSocketToServiceRecord(SPP_UUID); //明文傳輸(不安全),無需配對
            // 開啟子執行緒
            Util.EXECUTOR.execute(new Runnable() {
                @Override
                public void run() {
                    loopRead(socket); //迴圈讀取
                }
            });
        } catch (Throwable e) {
            close();
        }
    }
}

4.服務端-監聽連線

/**
 * 服務端監聽和連線執行緒,只連線一個裝置
 */
public class BtServer extends BtBase {
    private static final String TAG = BtServer.class.getSimpleName();
    private BluetoothServerSocket mSSocket;

    public BtServer(Listener listener) {
        super(listener);
        listen();
    }

    /**
     * 監聽客戶端發起的連線
     */
    public void listen() {
        try {
            BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
//            mSSocket = adapter.listenUsingRfcommWithServiceRecord(TAG, SPP_UUID); //加密傳輸,Android強制執行配對,彈窗顯示配對碼
            mSSocket = adapter.listenUsingInsecureRfcommWithServiceRecord(TAG, SPP_UUID); //明文傳輸(不安全),無需配對
            // 開啟子執行緒
            Util.EXECUTOR.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        BluetoothSocket socket = mSSocket.accept(); // 監聽連線
                        mSSocket.close(); // 關閉監聽,只連線一個裝置
                        loopRead(socket); // 迴圈讀取
                    } catch (Throwable e) {
                        close();
                    }
                }
            });
        } catch (Throwable e) {
            close();
        }
    }

    @Override
    public void close() {
        super.close();
        try {
            mSSocket.close();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

5.客戶端和服務端的基類,用於管理socket長連線,讀寫短訊息/檔案

public class BtBase {
    static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); //自定義
    private static final String FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/bluetooth/";
    private static final int FLAG_MSG = 0;  //訊息標記
    private static final int FLAG_FILE = 1; //檔案標記

    private BluetoothSocket mSocket;
    private DataOutputStream mOut;
    private Listener mListener;
    private boolean isRead;
    private boolean isSending;

    BtBase(Listener listener) {
        mListener = listener;
    }

    /**
     * 迴圈讀取對方資料(若沒有資料,則阻塞等待)
     */
    void loopRead(BluetoothSocket socket) {
        mSocket = socket;
        try {
            if (!mSocket.isConnected())
                mSocket.connect();
            notifyUI(Listener.CONNECTED, mSocket.getRemoteDevice());
            mOut = new DataOutputStream(mSocket.getOutputStream());
            DataInputStream in = new DataInputStream(mSocket.getInputStream());
            isRead = true;
            while (isRead) { //死迴圈讀取
                switch (in.readInt()) {
                    case FLAG_MSG: //讀取短訊息
                        String msg = in.readUTF();
                        notifyUI(Listener.MSG, "接收短訊息:" + msg);
                        break;
                    case FLAG_FILE: //讀取檔案
                        Util.mkdirs(FILE_PATH);
                        String fileName = in.readUTF(); //檔名
                        long fileLen = in.readLong(); //檔案長度
                        notifyUI(Listener.MSG, "正在接收檔案(" + fileName + ")····················");
                        // 讀取檔案內容
                        long len = 0;
                        int r;
                        byte[] b = new byte[4 * 1024];
                        FileOutputStream out = new FileOutputStream(FILE_PATH + fileName);
                        while ((r = in.read(b)) != -1) {
                            out.write(b, 0, r);
                            len += r;
                            if (len >= fileLen)
                                break;
                        }
                        notifyUI(Listener.MSG, "檔案接收完成(存放在:" + FILE_PATH + ")");
                        break;
                }
            }
        } catch (Throwable e) {
            close();
        }
    }

    /**
     * 傳送短訊息
     */
    public void sendMsg(String msg) {
        if (isSending || TextUtils.isEmpty(msg))
            return;
        isSending = true;
        try {
            mOut.writeInt(FLAG_MSG); //訊息標記
            mOut.writeUTF(msg);
        } catch (Throwable e) {
            close();
        }
        notifyUI(Listener.MSG, "傳送短訊息:" + msg);
        isSending = false;
    }

    /**
     * 傳送檔案
     */
    public void sendFile(final String filePath) {
        if (isSending || TextUtils.isEmpty(filePath))
            return;
        isSending = true;
        Util.EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    notifyUI(Listener.MSG, "正在傳送檔案(" + filePath + ")····················");
                    FileInputStream in = new FileInputStream(filePath);
                    File file = new File(filePath);
                    mOut.writeInt(FLAG_FILE); //檔案標記
                    mOut.writeUTF(file.getName()); //檔名
                    mOut.writeLong(file.length()); //檔案長度
                    int r;
                    byte[] b = new byte[4 * 1024];
                    while ((r = in.read(b)) != -1) {
                        mOut.write(b, 0, r);
                    }
                    notifyUI(Listener.MSG, "檔案傳送完成.");
                } catch (Throwable e) {
                    close();
                }
                isSending = false;
            }
        });
    }

    /**
     * 關閉Socket連線
     */
    public void close() {
        try {
            isRead = false;
            mSocket.close();
            notifyUI(Listener.DISCONNECTED, null);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

    /**
     * 當前裝置與指定裝置是否連線
     */
    public boolean isConnected(BluetoothDevice dev) {
        boolean connected = (mSocket != null && mSocket.isConnected());
        if (dev == null)
            return connected;
        return connected && mSocket.getRemoteDevice().equals(dev);
    }

    // ============================================通知UI===========================================================
    private void notifyUI(final int state, final Object obj) {
        MainAPP.runUi(new Runnable() {
            @Override
            public void run() {
                try {
                    mListener.socketNotify(state, obj);
                } catch (Throwable e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public interface Listener {
        int DISCONNECTED = 0;
        int CONNECTED = 1;
        int MSG = 2;

        void socketNotify(int state, Object obj);
    }
}