Android蓝牙开发记录

478 查看

原文见:http://wyh.life/article/2014/12/06/android-bluetooth

概述

一个简单的 Android-Ardunio 蓝牙通信 Demo,代码位于 Github Repo.

需求分析

  • 建立蓝牙连接,根据已定义的协议、同 Arduino 下位机通信

  • 解析应答数据,显示湿度或任何通信过程中异常信息

  • 提供UI来呈现指令按钮、配对设备列表、通信日志等

通信协议

指令 请求 应答
获取湿度/湿度阈值 0x57 0x55 0x01 0xFF 0x53 0x53 0x01 <当前值> <高> <低> 0xFF
设定阈值 0x57 0x55 0x02 <高> <低> 0xFF 0x53 0x53 0x02 0xFF
启动浇花 0x57 0x55 0x03 0xFF 0x53 0x53 0x03 0xFF
停止浇花 0x57 0x55 0x04 0xFF 0x53 0x53 0x04 0xFF

湿度值两位数据表示,校验位暂定0xFF

程序实现

主界面 BTConnActivity 处理用户交互,_BTService_ 负责建立蓝牙 Socket、并在单独线程管理蓝牙连接。

由于 Demo 实现得简单,因此仅将 Android 蓝牙开发的部分内容整理归档下。


Android 蓝牙应用开发

1. 权限声明

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

2. 蓝牙开发常用的类

2.1 BluetoothAdapter

代表本地的蓝牙适配器设备,BluetoothAdapter 类能让用户执行基本的蓝牙任务。例如:初始化设备的搜索、查询可匹配的设备集、使用一个已知的MAC地址来初始化一个 BluetoothDevice 类,创建一个 BluetoothServerSocket 类以监听其他设备对本机的连接请求等。

2.2 BluetoothDevice

代表远端蓝牙设备,可通过 BluetoothAdapter.getRemoteDevice(String) 创建一个表示已知MAC地址的设备(通过 BluetoothAdapter 来完成对设备的查找),或通过 Bluetooth.getBondedDevices() 返回的已匹配的设备集中得到设备对象。

2.3 BluetoothServerSocket

蓝牙端口监听接口和TCP端口类似:Socket 和 ServerSocket 类。在服务器端,使用 BluetoothServerSocket 类来创建一个监听服务端口。当一个连接被 BluetoothServerSocket 所接受,它会返回一个新的 BluetoothSocket 来管理该连接。在客户端,使用一个单独的 BluetoothSocket 类去初始化一个外接连接和管理该连接。

最常使用的蓝牙端口是 RFCOMM,它是被 Android API 支持的类型。RFCOMM 是一个面向连接,通过蓝牙模块进行的数据流传输方式,它也被称为串行端口规范(Serial Port Profile, SPP)

一些基础概念:

蓝牙 (Bluetooth): 是一种无线技术,用于建立带宽为2.4GHz,波长为10m的私有网络。

通道 (Channel): 是位于基带连接之上的逻辑连接,每个通道以多对一的方式绑定一个单一协议。多个通道可以绑定同一个协议,但是一个通道不可以绑定多个协议。

RFCOMM 协议: 是为了兼容传统的串口应用,同时取代有线的通信方式,蓝牙协议需要提供与有线串口一致的通信接口而开发出的协议。

设备配对 (Pairing of Device): 蓝牙设备可以选择通过验证以提供某种特殊服务,蓝牙验证一般使用PIN码 (最长为16个字符的 ASCII 字符串),用户需要在两个设备中输入相同的PIN码。用户输入PIN码后,两个设备会生成一个连接密匙 (link key),接着连接密钥可以存储在设备或存储器中。连接时两个设备会使用该连接密钥,该过程称为结对 (pairing)。如果任一方丢失了连接密钥,必须重新进行结对。

2.4 BluetoothSocket

代表一个蓝牙套接字的接口,是应用程序通过输入输出流与其他蓝牙设备通信的连接点。

3. 蓝牙应用开发示例

3.1 获取 BluetoothAdapter 对象
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter == null) {
    // Device does not support Bluetooth
}
3.2 启用蓝牙
if (!mBluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

系统会弹窗提升用户是否要允许开启蓝牙。如果开启成功,onActivityResult() 回调中收到 RESULT_OK

3.3 查询已配对设备集

已绑定/匹配的蓝牙设备,不同于已连接状态(设备间存在一个 RFCOMM 通道并能够互相传递数据)。建立连接首先要求建立配对,如果是第一次连接的话,配对请求会显示给用户;配对成功后会保存设备名称及MAC地址。

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
if (pairedDevices.size() > 0 ) {
    // Loop through paired devices
    for (BluetoothDevice device : pairedDevices) {
        // Add the name and address to an array adapter to show in a ListView
        mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
    }
}
3.4 发现设备

调用 startDiscovery() 方法,注意这是一个异步方法,整个扫描过程大概12s,应用程序需要注册一个 BroadcastReceiver 来接收扫描到的信息,对于每一个设备,系统都会广播 ACTION_FOUND 动作。

// Create a BroadcastReceiver for ACTION_FOUND
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        // When discovery finds a device
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            // Get the BluetoothDevice object from the Intent
            BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            // Add the name and address to an array adapter to show in a ListView
            mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
        }
    }
}

// Register the BroadcastReceiver
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter);    // Don't forget to unregister during onDestory

注意,扫描是一个很耗费资源的过程,一旦找到需要的设备后,在发起请求之前,确保你的程序调用 cancelDiscovery() 停止扫描。

3.5 开启可见

ACTION_REQUEST_DISCOVERABLE 动作封装在Intent中并调用 startActivityForResult(Intent, int) 方法就可以了,可以通过 EXTRA_DISCOVERABLE_DURATION 字段改变可见时长(缺省120s)

Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);
3.6 作为服务端连接

持有一个打开的 BluetoothServerSocket 以监听外来连接请求,当监听到以后提供一个连接上的 BluetoothSocket 给客户端、并可以销毁 BluetoothServerSocket (除非还想监听更多的连接请求,一般都是单点传输)

详细步骤:

  • 通过 listenUsingRfcommWithServiceRecord(String, UUID) 方法来获取 BluetoothServerSocket 对象,系统会在设备上建立SDP数据库;UUID 必须双方匹配才能建立连接

  • 调用 accept() 方法来监听可能到来的连接请求,当监听到以后、返回一个连接上的 BluetoothSocket

  • 在监听到一个连接后,调用 close() 方法关闭监听程序。(因accept是一种阻塞调用,单独开线程管理)

蓝牙服务发现协议(Service Discovery Protocol, SDP):让客户端应用发现存在的服务端应用所提供的服务、以及这些服务的属性。SDP 只提供侦测 service 的机制,不提供使用服务的方法。每个蓝牙设备都需要一个 SDP Service,只做客户端的蓝牙设备除外。

SDP Server 维护的服务条目包含在 Service Record中,由一个32位数唯一区分。

from Android SDK Docs:

If you are connecting to a Bluetooth serial board then try using the well-known SPP UUID 00001101-0000-1000-8000-00805F9B34FB. However if you are connecting to an Android peer then please generate your own unique UUID.

private class AcceptThread extends Thread {
    private final BluetoothServerSocket mmServerSocket;

    public AcceptThread() {
        // Use a temporary object that is later assigned to mmServerSocket,
        // because mmServerSocket is final
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID is the app's UUID string, also used by the client code
            tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) {
            // IGNORE
        }
        mmServerSocket = tmp;
    }

    public voi run() {
        BluetoothSocket socket = null;
        // Keep listening until exception occurs or a socket is returned
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                break;
            }

            // If a connection was accepted
            if (socket != null) {
                // Do work to manage the connection (in a separate thread)
                manageConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }

    /** Will cancel the listening socket, and cause the thread to finish */
    public void cancel () {
        try {
            mmServerSocket.close();
        } catch (IOException e) {
            // IGNORE
        }
    }
}
3.7 作为客户端连接

为了初始化一个与远端设备的连接,需要先获取代表该设备的一个 BluetoothDevice 对象,再通过 createRfcommSocketToServiceRecord(UUID) 获取 BluetoothSocket,并调用 connect() 方法初始化连接。如果成功建立连接,将在通信过程中共享 RFCOMM 信道,且 connect() 方法返回。

private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;

    public ConnectThread(BluetoothDevice device) {
        BluetoothSockcet tmp = null;
        mmDevice = device;

        // Get a BluetoothSocket to connect with the given BluetoothDevice
        try {
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) {
            // IGNORE
        }
        mmSocket = tmp;
    }

    public void run() {
        // Cancel discovery because it will slow down the connection
        mBluetoothAdapter.cancelDiscovery();

        try {
            // Connect the device through the socket. This will block
            // until it succeeds or throws an exception
            mmSocket.Connect();
        } catch (IOException connectException) {
            // Unable to connect; close the socket and get out
            try {
                mmSocket.close();
            } catch (IOException closeException) {
                // IGNORE
            }
            return;
        }

        // Do work to manage the connection (in a separate thread)
        manageConnectedSocket(mmSocket);
    }

    /** Will cancel an in-progress connection, and close the socket */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) {
            // IGNORE
        }
    }
}
3.8 管理连接(主要涉及数据的传输)

当成功建立连接后,两端都持有BluetoothSocket,以流的方式通信。由于读写操作都是阻塞调用,需要开一个线程来管理。

private class ConnectedThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final InputStream mmInStream;
    private final OutputStream mmOutStream;

    public ConnectedThread(BluetoothSocket socket) {
        mmSocket = socket;
        InputStream tmpIn = null;
        OutputStream tmpOut = null;

        // Get the input and output streams, using temp objects because
        // member streams are final
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) {
            // IGNORE
        }

        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }

    public void run() {
        byte[] buffer = new byte[1024]; // buffer store for the stream
        int bytes; // bytes returned from read()

        // Keep listening to the InputStream until an exception occurs
        while (true) {
            try {
                // Read from the InputStream
                bytes = mmInStream.read(buffer);
                // Send  the obtained bytes to the UI activity
                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer).sendToTarget();
            } catch (IOException e) {
                // IGNORE
                break;
            }
        }
    }

    /** Call this from the main activity to send data to the remote device */
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) {
            // IGNORE
        }
    }

    /** Call this from the main activity to shutdown the connection */
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) {
            // IGNORE
        }
    }
}