Android藍(lán)牙健康設(shè)備開發(fā):Health Device Profile(HDP)

最近按導(dǎo)師的要求做了個(gè)小項(xiàng)目:用Android手機(jī)跟藍(lán)牙血壓計(jì)通信膛薛。在網(wǎng)上查了很多資料掠剑,發(fā)現(xiàn)有許多文章是講解Android藍(lán)牙開發(fā)入热,但是在中文社區(qū)缺少針對(duì)實(shí)現(xiàn)HDP profile的藍(lán)牙健康設(shè)備的文章潮孽,所以整理了相關(guān)知識(shí)和代碼做一個(gè)總結(jié)缆蝉。做了一點(diǎn)微小的貢獻(xiàn)宇葱,謝謝大家瘦真。
1、相關(guān)概念
HDP:Health Device Profile顧名思義是針對(duì)藍(lán)牙健康設(shè)備(如藍(lán)牙血壓計(jì)黍瞧、藍(lán)牙體重秤)的一個(gè)profile诸尽,由藍(lán)牙技術(shù)聯(lián)盟(Bluetooth SIG)發(fā)布。并不是所有藍(lán)牙設(shè)備都可以采用HDP印颤,只有經(jīng)典藍(lán)牙(BR/EDR)設(shè)備才可以您机。低功耗藍(lán)牙設(shè)備,即藍(lán)牙4.0及以上年局,采用的是GATT等profile际看。

上圖是HDP的協(xié)議棧。在通信時(shí)矢否,藍(lán)牙健康設(shè)備作為source仲闽,Android手機(jī)作為sink。圖中L2CAP僵朗、SDP赖欣、MCAP是藍(lán)牙通信的底層協(xié)議,中間是IEEE11073協(xié)議验庙。11073是IEEE發(fā)布的一個(gè)健康設(shè)備的協(xié)議簇畏鼓,其下包含了許多不同種類的健康設(shè)備協(xié)議,如11073-10407是血壓計(jì)的協(xié)議壶谒。在數(shù)據(jù)傳輸時(shí),手機(jī)與健康設(shè)備根據(jù)此協(xié)議來發(fā)送請(qǐng)求膳沽、解析數(shù)據(jù)等汗菜。最上層的application在本文中自然指的就是Android app。
Android藍(lán)牙模塊:android.bluetooth是藍(lán)牙的開發(fā)包挑社,里面包含了所有藍(lán)牙相關(guān)的類陨界,無論是經(jīng)典藍(lán)牙還是低功耗藍(lán)牙。其中痛阻,本文重點(diǎn)關(guān)注的是BluetoothHealth這個(gè)類菌瘪,還用到了一些藍(lán)牙基礎(chǔ)類BluetoothAdapter BluetoothDevice等,在這里就不展開介紹了阱当。BluetoothHealth是在API14時(shí)引入的俏扩,所以系統(tǒng)版本高于14的Android手機(jī)都可以與采用HDP的健康設(shè)備通信。BluetoothHealth中包括了與HDP設(shè)備建立通信的API弊添,具體請(qǐng)參見官方文檔录淡。在官方文檔中有一個(gè)建立通信的流程:
1、調(diào)用[getProfileProxy(Context, BluetoothProfile.ServiceListener, int)](https://developer.android.com/reference/android/bluetooth/BluetoothAdapter.html#getProfileProxy(android.content.Context, android.bluetooth.BluetoothProfile.ServiceListener, int))來獲取代理對(duì)象的連接油坝。
2嫉戚、創(chuàng)建BluetoothHealthCallback回調(diào)刨裆,調(diào)用[registerSinkAppConfiguration(String, int, BluetoothHealthCallback)](https://developer.android.com/reference/android/bluetooth/BluetoothHealth.html#registerSinkAppConfiguration(java.lang.String, int, android.bluetooth.BluetoothHealthCallback))注冊(cè)一個(gè)sink端的應(yīng)用程序配置。
3彬檀、將手機(jī)與健康設(shè)備配對(duì)帆啃,這一步一般在手機(jī)的“設(shè)置”中完成。
4窍帝、使用[connectChannelToSource(BluetoothDevice, BluetoothHealthAppConfiguration)](https://developer.android.com/reference/android/bluetooth/BluetoothHealth.html#connectChannelToSource(android.bluetooth.BluetoothDevice, android.bluetooth.BluetoothHealthAppConfiguration))來建立一個(gè)與健康設(shè)備的通信channel努潘。有的設(shè)備會(huì)自動(dòng)建立通信,不需要在代碼中調(diào)用這個(gè)方法盯桦。第二步中的回調(diào)會(huì)指示channel的狀態(tài)變化慈俯。
5、用ParcelFileDescriptor來讀取健康設(shè)備傳來的數(shù)據(jù)拥峦,并根據(jù)IEEE 11073來解析數(shù)據(jù)贴膘。
6、通信結(jié)束后略号,關(guān)閉通信channel刑峡,注銷應(yīng)用程序配置。
看完這個(gè)流程是不是一臉懵逼玄柠?不要慌突梦,這里官方文檔實(shí)在太抽象了,必須要配合實(shí)際的代碼才能看懂羽利。
2宫患、demo代碼
項(xiàng)目的目標(biāo)是與經(jīng)典藍(lán)牙血壓計(jì)進(jìn)行通信,獲取血壓計(jì)測(cè)量的數(shù)值和日期这弧。這里血壓計(jì)的型號(hào)是A&D UA-767PBT-C娃闲。項(xiàng)目中的代碼是以github上這個(gè)項(xiàng)目為基礎(chǔ),根據(jù)需求進(jìn)行修改而實(shí)現(xiàn)的匾浪。

項(xiàng)目結(jié)構(gòu)

上圖是項(xiàng)目的結(jié)構(gòu)皇帮,非常簡(jiǎn)單,只有一個(gè)activity和一個(gè)service蛋辈。activity與用戶交互属拾,service綁定在activity中與藍(lán)牙血壓計(jì)建立通信、交換數(shù)據(jù)冷溶。
首先AndroidManifest.xml中注冊(cè)藍(lán)牙權(quán)限:

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

MainActivity

主要作用是顯示血壓計(jì)傳過來的數(shù)據(jù)渐白,以及綁定服務(wù)。
activity的布局如下:

demo.png

上半部分是一個(gè)ArrayList,用于顯示手機(jī)所配對(duì)的藍(lán)牙設(shè)備的名稱和Mac地址逞频。下半部分會(huì)顯示一次血壓測(cè)量的參數(shù):收縮壓礼预、舒張壓、心率虏劲、測(cè)量時(shí)間托酸。

在MainActivity中一些重要私有變量:

private static final int REQUEST_ENABLE_BT = 1;  //用于打開手機(jī)藍(lán)牙
private static final int HEALTH_PROFILE_SOURCE_DATA_TYPE = 0x1007;  //IEEE 11073中規(guī)定的血壓數(shù)據(jù)類型
private BluetoothAdapter mBluetoothAdapter;
private Messenger mHealthService;  //用于與service通信
private boolean mHealthServiceBound; //用于判斷service是否與此activity綁定

打開藍(lán)牙:


       if (!mBluetoothAdapter.isEnabled()) {
            Intent enableIntent = new Intent(
                    BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
        } else {
            initialize();
        }
 

啟動(dòng)服務(wù):

            // Sets up communication with HDPService.
            private ServiceConnection mConnection = new ServiceConnection() {
                public void onServiceConnected(ComponentName name, IBinder service) {
                    mHealthServiceBound = true;
                    Message msg = Message.obtain(null,
                            HDPService.MSG_REG_CLIENT);
                    msg.replyTo = mMessenger;
                    mHealthService = new Messenger(service);
                    try {
                        mHealthService.send(msg);
                        //register blood pressure data type
                        sendMessage(HDPService.MSG_REG_HEALTH_APP,
                                HEALTH_PROFILE_SOURCE_DATA_TYPE);
                    } catch (RemoteException e) {
                        Log.w(TAG, "Unable to register client to service.");
                        e.printStackTrace();
                    }
                }
    
                public void onServiceDisconnected(ComponentName name) {
                    mHealthService = null;
                    mHealthServiceBound = false;
                }
            };
    private void initialize() {
        // Starts health service.
        Intent intent = new Intent(this, HDPService.class);
        //startService(intent);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

用handler和message來實(shí)現(xiàn)activity與service的通信:

    private Handler mIncomingHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
              ......
            }
        }
    };

    private final Messenger mMessenger = new Messenger(mIncomingHandler);

HDPService

這個(gè)service是核心部分褒颈,手機(jī)與藍(lán)牙血壓計(jì)的通信就是在此實(shí)現(xiàn)的。
service中部分重要私有變量:

    private BluetoothHealthAppConfiguration mHealthAppConfig; //BluetoothHealthAppConfiguration
    private BluetoothAdapter mBluetoothAdapter;  //BluetoothAdapter
    private BluetoothHealth mBluetoothHealth;  //代理對(duì)象

在service中按照官方文檔的流程一步一步實(shí)現(xiàn)與藍(lán)牙血壓計(jì)的通信励堡。
1谷丸、獲取代理對(duì)象:

    public void onCreate() {
        super.onCreate();
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            // Bluetooth adapter isn't available.  The client of the service is supposed to
            // verify that it is available and activate before invoking this service.
            stopSelf();
            return;
        }
        if (!mBluetoothAdapter.getProfileProxy(this, mBluetoothServiceListener,
                BluetoothProfile.HEALTH)) {
            Toast.makeText(this, "HDP not available",
                    Toast.LENGTH_LONG).show();
            stopSelf();
            return;
        }
    }

代碼中的mBluetoothServiceListener是一個(gè)回調(diào),如果getProfileProxy方法返回為真应结,就進(jìn)入回調(diào)刨疼。在回調(diào)中獲取了代理對(duì)象屬于BluetoothHealth類的proxy。

private final BluetoothProfile.ServiceListener mBluetoothServiceListener =
            new BluetoothProfile.ServiceListener() {
                public void onServiceConnected(int profile, BluetoothProfile proxy) {
                    if (profile == BluetoothProfile.HEALTH) {
                        mBluetoothHealth = (BluetoothHealth) proxy;
                        Log.d(TAG, "onServiceConnected to profile: " + profile);
                    }
                }

                public void onServiceDisconnected(int profile) {
                    if (profile == BluetoothProfile.HEALTH) {
                        mBluetoothHealth = null;
                    }
                    Log.d(TAG, "onServiceDisconnected");
                }
            };

2鹅龄、注冊(cè)BluetoothHealthAppConfiguration:

mBluetoothHealth.registerSinkAppConfiguration(TAG, dataType, mHealthCallback);
private final BluetoothHealthCallback mHealthCallback = new BluetoothHealthCallback() {
        // Callback to handle application registration and unregistration events.  The service
        // passes the status back to the UI client.
        public void onHealthAppConfigurationStatusChange(BluetoothHealthAppConfiguration config,
                                                         int status) {
            if (status == BluetoothHealth.APP_CONFIG_REGISTRATION_FAILURE) {
                mHealthAppConfig = null;
                sendMessage(STATUS_HEALTH_APP_REG, RESULT_FAIL, null);
                Log.d(TAG, "APP_CONFIG_REGISTRATION_FAILURE");
            } else if (status == BluetoothHealth.APP_CONFIG_REGISTRATION_SUCCESS) {
                mHealthAppConfig = config;
                sendMessage(STATUS_HEALTH_APP_REG, RESULT_OK, null);
                Log.d(TAG, "APP_CONFIG_REGISTRATION_SUCCESS");
            } else if (status == BluetoothHealth.APP_CONFIG_UNREGISTRATION_FAILURE ||
                    status == BluetoothHealth.APP_CONFIG_UNREGISTRATION_SUCCESS) {
                sendMessage(STATUS_HEALTH_APP_UNREG,
                        status == BluetoothHealth.APP_CONFIG_UNREGISTRATION_SUCCESS ?
                                RESULT_OK : RESULT_FAIL, null);
                Log.d(TAG, "UNREGISTRATION");
            }
        }

        // Callback to handle channel connection state changes.
        // Note that the logic of the state machine may need to be modified based on the HDP device.
        // When the HDP device is connected, the received file descriptor is passed to the
        // ReadThread to read the content.
        public void onHealthChannelStateChange(BluetoothHealthAppConfiguration config,
                                               BluetoothDevice device, int prevState, int newState, ParcelFileDescriptor fd,
                                               int channelId) {
//            if (Log.isLoggable(TAG, Log.DEBUG))
            Log.d(TAG, String.format("prevState\t%d ----------> newState\t%d",
                    prevState, newState));
            if (prevState == BluetoothHealth.STATE_CHANNEL_CONNECTING &&
                    newState == BluetoothHealth.STATE_CHANNEL_CONNECTED) {
                if (config.equals(mHealthAppConfig)) {
                    mChannelId = channelId;
                    sendMessage(STATUS_CREATE_CHANNEL, RESULT_OK, null);
                    (new ReadThread(fd)).start();
                } else {
                    sendMessage(STATUS_CREATE_CHANNEL, RESULT_FAIL, null);
                }
            } else if (prevState == BluetoothHealth.STATE_CHANNEL_CONNECTING &&
                    newState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED) {
                sendMessage(STATUS_CREATE_CHANNEL, RESULT_FAIL, null);
            } else if (newState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED) {
                Log.d(TAG, "I'm in State Channel Disconnected.");
                if (config.equals(mHealthAppConfig)) {
                    sendMessage(STATUS_DESTROY_CHANNEL, RESULT_OK, null);

                } else {
                    sendMessage(STATUS_DESTROY_CHANNEL, RESULT_FAIL, null);
                }
            } else if (prevState == BluetoothHealth.STATE_CHANNEL_DISCONNECTED &&
                    newState == BluetoothHealth.STATE_CHANNEL_CONNECTED) {
                if (config.equals(mHealthAppConfig)) {
                    mChannelId = channelId;
                    sendMessage(STATUS_CREATE_CHANNEL, RESULT_OK, null);
                    (new ReadThread(fd)).start();
                } else {
                    sendMessage(STATUS_CREATE_CHANNEL, RESULT_FAIL, null);
                }
            }
        }
    };

mHealthCallback是其中的關(guān)鍵揩慕,有兩個(gè)方法onHealthAppConfigurationStatusChange和onHealthChannelStateChange。
onHealthAppConfigurationStatusChange回調(diào)會(huì)監(jiān)聽AppConfiguration的狀態(tài)扮休,會(huì)在registerSinkAppConfiguration函數(shù)返回后調(diào)用迎卤。
onHealthChannelStateChange回調(diào)會(huì)監(jiān)聽與source端(在這里也就是血壓計(jì))通信通道的狀態(tài)變化,如果收到發(fā)來的數(shù)據(jù)玷坠,會(huì)接收到一個(gè)ParcelFileDescriptor用于讀取蜗搔。

3、將手機(jī)與健康設(shè)備配對(duì)
這一步在手機(jī)的系統(tǒng)設(shè)置中進(jìn)行八堡,不必嚴(yán)格按照這個(gè)流程的順序樟凄,可以事先配對(duì)好。

4兄渺、建立通信通道
這一步不是所有設(shè)備都需要缝龄,部分設(shè)備會(huì)自動(dòng)建立通道與sink端(手機(jī))進(jìn)行通信。我們所用的這個(gè)藍(lán)牙血壓計(jì)就是如此挂谍,會(huì)自動(dòng)建立通道二拐。
如果不能自動(dòng)建立通信,可以主動(dòng)調(diào)用如下代碼:

mBluetoothHealth.connectChannelToSource(mDevice, mHealthAppConfig);

mDevice是將要建立通信的source端凳兵,可以用mBluetoothAdapter獲取與手機(jī)配對(duì)的任一設(shè)備。

5企软、讀取庐扫、解析數(shù)據(jù)
在與藍(lán)牙設(shè)備通信時(shí),我們需要知道一點(diǎn):通信是雙向的仗哨,設(shè)備會(huì)向手機(jī)發(fā)信息形庭,手機(jī)可以也需要向設(shè)備發(fā)信息。在service中建了兩個(gè)線程厌漂,一個(gè)用于讀數(shù)據(jù)萨醒,一個(gè)用于寫數(shù)據(jù),通過這兩個(gè)線程實(shí)現(xiàn)與設(shè)備的數(shù)據(jù)交換苇倡。

首先看讀線程

    private class ReadThread extends Thread {
        private ParcelFileDescriptor mFd;

        public ReadThread(ParcelFileDescriptor fd) {
            super();
            mFd = fd;
            Log.i(TAG, "read thread");
        }


        @Override
        public void run() {
          ...
        }
    }

讀線程的調(diào)用是在回調(diào)方法onHealthChannelStateChange中的富纸,當(dāng)判斷通信通道打開時(shí)囤踩,會(huì)開啟一個(gè)讀線程,并且將回調(diào)中提供的ParcelFileDescriptor傳入讀線程的構(gòu)造方法晓褪,用于讀取數(shù)據(jù)堵漱。
具體的讀取和解析過程我們來看run方法:

FileInputStream fis = new FileInputStream(mFd.getFileDescriptor());
            byte data[] = new byte[300]; //假定有300byte的數(shù)據(jù)
            try {
                while (fis.read(data) > -1) {
                    if (data[0] != (byte) 0x00) {
                        String test = byte2hex(data);
                        Log.i(TAG, "test: " + test);
                        if (data[0] == (byte) 0xE2) {
                            Log.i(TAG, "E2");
                            //data_AR
                            count = 1;
                            (new WriteThread(mFd)).start();
                            try {
                                sleep(100);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            count = 2;
                            (new WriteThread(mFd)).start();
                        } else if (data[0] == (byte) 0xE7) {
                            Log.i(TAG, "E7");
                            if (data[18] == (byte) 0x0d && data[19] == (byte) 0x1d)  //fixed report
                            {
                                count = 3;
                                //set invoke id so get correct response
                                invoke = new byte[]{data[6], data[7]};
                                //write back response
                                (new WriteThread(mFd)).start();
                                //parse data!!

                                int length = data[21];
                                Log.i(TAG, "length is " + length);
                                int number_of_data_packets = data[22 + 5]; //03
                                //packet_start starts from handle 0 byte
                                int packet_start = 30; 
                                final int SYS_DIA_MAP_DATA = 1;
                                final int PULSE_DATA = 2;
                                final int ERROR_CODE_DATA = 3;
                                for (int i = 0; i < number_of_data_packets; i++) {
                                    int obj_handle = data[packet_start + 1]; 
                                    switch (obj_handle) {
                                        case SYS_DIA_MAP_DATA:
                                            int sys = byteToUnsignedInt(data[packet_start + 9]);
                                            int dia = byteToUnsignedInt(data[packet_start + 11]);
                                            int map = byteToUnsignedInt(data[packet_start + 13]);
                                            //create team string... 9+13~9+20
                                            Log.i(TAG, "sys is " + sys);
                                            sendMessage(RECEIVED_SYS, sys, null);
                                            Log.i(TAG, "dia is " + dia);
                                            sendMessage(RECEIVED_DIA, dia, null);
                                            Log.i(TAG, "map is " + map);
                                            break;
                                        case PULSE_DATA:
                                            //parse
                                            int pulse = byteToUnsignedInt(data[packet_start + 5]);
                                            Log.i(TAG, "pulse is " + pulse);
                                            sendMessage(RECEIVED_PUL, pulse, null);
                                            String month = byteToString(data[packet_start + 8]);
                                            String day = byteToString(data[packet_start + 9]);
                                            String year = byteToString(data[packet_start + 6]) + byteToString(data[packet_start + 7]);
                                            String hour = byteToString(data[packet_start + 10]);
                                            String minute = byteToString(data[packet_start + 11]);
                                            String date = year + "." + month + "." + day + " " + hour + ":" + minute;
                                            Log.v(TAG, "the date is " + date);
                                            sendMessage(RECEIVED_DATE, 0, date);
                                            break;
                                        case ERROR_CODE_DATA:
                                            //need more signal
                                            break;
                                    }
                                    packet_start += 4 + data[packet_start + 3];    //4 = ignore beginning four bytes
                                }
                            } else {
                                count = 2;
                            }
                        } else if (data[0] == (byte) 0xE4) {
                            count = 4;
                            (new WriteThread(mFd)).start();
                        }
                        //zero out the data
                        for (int i = 0; i < data.length; i++) {
                            data[i] = (byte) 0x00;
                        }
                    }
                    sendMessage(STATUS_READ_DATA, 0, null);
                }
            } catch (IOException ioe) {
            }
            if (mFd != null) {
                try {
                    mFd.close();
                } catch (IOException e) { /* Do nothing. */ }
            }
            sendMessage(STATUS_READ_DATA_DONE, 0, null);
        }

while循環(huán)中是對(duì)讀取數(shù)據(jù)的解析,這里的解析必須參考IEEE 11073中的內(nèi)容涣仿。這個(gè)demo用到了血壓計(jì)勤庐,因此就要使用IEEE 11073-10407(專門針對(duì)血壓計(jì)的協(xié)議)。使用其他類型的設(shè)備也就要使用相應(yīng)的IEEE 11073-xxxxx協(xié)議好港。
回到demo中的數(shù)據(jù)讀取和解析愉镰,這是一個(gè)根據(jù)IEEE 11073-10407進(jìn)行手機(jī)與藍(lán)牙設(shè)備互相發(fā)送數(shù)據(jù)的過程,下圖是運(yùn)行的log:

log.png

大致解釋一下流程:
按下測(cè)量按鈕后钧汹,藍(lán)牙血壓計(jì)發(fā)來一個(gè)e2開頭的數(shù)據(jù)丈探,這是一個(gè)請(qǐng)求連接的信號(hào)。
收到請(qǐng)求連接后类嗤,手機(jī)用寫線程發(fā)送給血壓計(jì)一個(gè)回復(fù)同意連接,接著再發(fā)送一個(gè)數(shù)據(jù)請(qǐng)求血壓計(jì)的特征辨宠。
血壓計(jì)發(fā)來一個(gè)e7開頭的數(shù)據(jù)遗锣,回復(fù)血壓計(jì)的特征。
測(cè)量完畢后嗤形,血壓計(jì)發(fā)來一個(gè)e7開頭的數(shù)據(jù)精偿,是這次測(cè)量的報(bào)告。
手機(jī)回復(fù)確認(rèn)收到測(cè)量數(shù)據(jù)赋兵。
血壓計(jì)發(fā)來一個(gè)e4開頭的數(shù)據(jù)笔咽,請(qǐng)求斷開連接。
手機(jī)回復(fù)確認(rèn)斷開連接霹期。

想要查看詳細(xì)數(shù)據(jù)請(qǐng)看IEEE 11073-10407叶组!請(qǐng)看IEEE 11073-10407!請(qǐng)看IEEE 11073-10407历造!

接下去是寫線程:

private class WriteThread extends Thread {
        private ParcelFileDescriptor mFd;

        public WriteThread(ParcelFileDescriptor fd) {
            super();
            mFd = fd;
        }

        @Override
        public void run() {
            FileOutputStream fos = new FileOutputStream(mFd.getFileDescriptor());
//          Association Response [0xE300]
            final byte data_AR[] = new byte[]{(byte) 0xE3, (byte) 0x00,
                    (byte) 0x00, (byte) 0x2C,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x50, (byte) 0x79,
                    (byte) 0x00, (byte) 0x26,
                    (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x80, (byte) 0x00,
                    (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x80, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x08,  //bt add for phone, can be automate in the future
                    (byte) 0x3C, (byte) 0x5A, (byte) 0x37, (byte) 0xFF,
                    (byte) 0xFE, (byte) 0x95, (byte) 0xEE, (byte) 0xE3,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00};
//          Presentation APDU [0xE700]
            final byte data_DR[] = new byte[]{(byte) 0xE7, (byte) 0x00,
                    (byte) 0x00, (byte) 0x12,
                    (byte) 0x00, (byte) 0x10,
                    (byte) invoke[0], (byte) invoke[1],
                    (byte) 0x02, (byte) 0x01,
                    (byte) 0x00, (byte) 0x0A,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
                    (byte) 0x0D, (byte) 0x1D,
                    (byte) 0x00, (byte) 0x00};

            final byte get_MDS[] = new byte[]{(byte) 0xE7, (byte) 0x00,
                    (byte) 0x00, (byte) 0x0E,
                    (byte) 0x00, (byte) 0x0C,
                    (byte) 0x00, (byte) 0x24,
                    (byte) 0x01, (byte) 0x03,
                    (byte) 0x00, (byte) 0x06,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00,
                    (byte) 0x00, (byte) 0x00};

            final byte data_RR[] = new byte[]{(byte) 0xE5, (byte) 0x00,
                    (byte) 0x00, (byte) 0x02,
                    (byte) 0x00, (byte) 0x00};

            try {
                Log.i(TAG, String.valueOf(count));
                if (count == 1) {
                    fos.write(data_AR);
                    Log.i(TAG, "Association Responsed!");
                } else if (count == 2) {
                    fos.write(get_MDS);
                    Log.i(TAG, "Get MDS object attributes!");
//                  fos.write(data_ABORT);
                } else if (count == 3) {
                    fos.write(data_DR);
                    Log.i(TAG, "Data Responsed!");
                } else if (count == 4) {
                    fos.write(data_RR);
                    Log.i(TAG, "Data Released!");
                }
            } catch (IOException ioe) {
            }
        }
    }

寫線程的邏輯非常清晰甩十,就是通過同一個(gè)ParcelFileDescriptor向血壓計(jì)發(fā)送數(shù)據(jù)。發(fā)送的數(shù)據(jù)在這里已經(jīng)寫死吭产,一共四組數(shù)據(jù):作用依次是確認(rèn)連接請(qǐng)求侣监;確認(rèn)收到測(cè)量數(shù)據(jù);請(qǐng)求血壓計(jì)的特征臣淤;確認(rèn)斷開連接橄霉。

大功告成,收到數(shù)據(jù)后界面如下:

Screenshot_2016-09-18-09-53-32.png

第一次寫文章邑蒋,有各種不足之處姓蜂,請(qǐng)大家指正按厘。

完整代碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市覆糟,隨后出現(xiàn)的幾起案子刻剥,更是在濱河造成了極大的恐慌,老刑警劉巖滩字,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件造虏,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡麦箍,警方通過查閱死者的電腦和手機(jī)漓藕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挟裂,“玉大人享钞,你說我怎么就攤上這事【魅兀” “怎么了栗竖?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)渠啤。 經(jīng)常有香客問我狐肢,道長(zhǎng),這世上最難降的妖魔是什么沥曹? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任份名,我火速辦了婚禮,結(jié)果婚禮上妓美,老公的妹妹穿的比我還像新娘僵腺。我一直安慰自己,他們只是感情好壶栋,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布辰如。 她就那樣靜靜地躺著,像睡著了一般贵试。 火紅的嫁衣襯著肌膚如雪琉兜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天锡移,我揣著相機(jī)與錄音,去河邊找鬼漆际。 笑死淆珊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的奸汇。 我是一名探鬼主播施符,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼往声,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了戳吝?” 一聲冷哼從身側(cè)響起浩销,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎听哭,沒想到半個(gè)月后慢洋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡陆盘,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年普筹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片隘马。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡太防,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出酸员,到底是詐尸還是另有隱情蜒车,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布幔嗦,位于F島的核電站酿愧,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏崭添。R本人自食惡果不足惜寓娩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望呼渣。 院中可真熱鬧棘伴,春花似錦、人聲如沸屁置。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蓝角。三九已至阱穗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間使鹅,已是汗流浹背揪阶。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留患朱,地道東北人鲁僚。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親冰沙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子侨艾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

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