Android 蓝牙开发

近期的项目涉及到蓝牙通讯,于是就整理了一下蓝牙的通讯机制的知识点。
蓝牙通讯主要是配对和连接两个过程。

配对和连接是两个不同的概念,请不要混为一谈,配对上的设备不代表已经连接。

首先我们需要权限

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

代表本地蓝牙适配器(蓝牙无线电)。BluetoothAdapter是所有蓝牙交互的入口。使用这个你可以发现其他蓝牙设备,查询已配对的设备列表,使用一个已知的MAC地址来实例化一个BluetoothDevice

//通常我们使用该方法获得蓝牙的本地属性。
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (mBluetoothAdapter == null) {
        // 代表设备不支持蓝牙
    }
adapter部分方法
BluetoothDevice

代表一个远程蓝牙设备,使用这个来请求一个与远程设备的BluetoothSocket连接,或者查询关于设备名称、地址、类和连接状态等设备信息。

//通过mac地址来获得远程蓝牙设备,通常我们也使用查找设备的广播来获得远程蓝牙设备,稍后会介绍
mBluetoothDevice = mBluetoothAdapter.getRemoteDevice(macAddress);
//该类的方法与adapter类似
开启蓝牙

(以下的mBluetoothAdapter表示本地蓝牙设备,mBluetoothDevice表示远程蓝牙设备)
方法一:

     //利用系统设置开启蓝牙
     Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
     discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 3600);
     startActivity(discoverableIntent);
    
    if (!mBluetoothAdapter.isEnabled()) {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }

方法二

mBluetoothAdapter.enable()
//需要权限
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
蓝牙搜寻广播
Action 静态注册
<intent-filter>
<action android:name="android.bluetooth.device.action.FOUND" />
</intent-filter>
动态注册
    // 定义一个广播 for ACTION_FOUND
    private final 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);
                // 保存设备的mac地址与蓝牙名称
                mArrayAdapter.add(device.getName() + "\n" + device.getAddress());
            }
        }
    };
    // 动态注册代码
    IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
    registerReceiver(mReceiver, filter);
    //不要忘了在onDestroy注销广播哟  
扫描代码
mBluetoothAdapter.startDiscovery();//开始扫描
//蓝牙扫描是非常耗费资源与时间的,当我们扫描到需要操作的设备的时候,我们需要停止扫描来获得更好的连接效率。
mBluetoothAdapter.cancelDiscovery();//取消扫描

设置蓝牙可被搜索

Intent discoverableIntent = newIntent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);//后面的参数是可被发现的时间,最大支持3600秒
startActivity(discoverableIntent);

蓝牙配对

如果只是单纯的配对,我们可以利用反射:
(这里将会介绍两张配对方式,反射是第一种,第二种是通过连接来配对,这里的配对都不需要输入pin码的)

 try {  
    //检查是否处于未配对状态
     if (mBluetoothDevice.getBondState() == BluetoothDevice.BOND_NONE) {  
        Method creMethod = BluetoothDevice.class.getMethod("createBond");  
        Log.e("TAG", "开始配对");  
        creMethod.invoke(mBluetoothDevice);  
     } 
 } catch (Exception e) {  
    // TODO: handle exception  
    //DisplayMessage("无法配对!");  
    e.printStackTrace();  
} 
取消配对
 static public boolean removeBond(Class btClass, BluetoothDevice btDevice)  
            throws Exception  
    {  
        Method removeBondMethod = btClass.getMethod("removeBond");  
        Boolean returnValue = (Boolean) removeBondMethod.invoke(btDevice);  
        return returnValue.booleanValue();  
    }  
连接与配对

是的蓝牙的连接类似TCP的socket连接,但是不同的是蓝牙的BluetoothSock不是new出来的,而是通过一个静态方法根据UUID生成的,而不是端口号。同一时间一个通道只允许一个socket连接通讯(多线程或许能解决这个问题),但是大多蓝牙情景都是一对一的连接。
为了在两台设备上创建一个连接,你必须实现服务器端和客户端两头的机制,因为一个设备必须打开一个服务器socket,而另一个设备初始化创建(使用服务器设备的MAC地址来初始化一个连接)。当他们在相同的RFCOMM通道上有一个已连接的 BluetoothSocket 时,服务器和客户被认为是互相连接了。
**注意:如果两台设备之前没有配对过,那么Android框架将会自动显示一个请求配对的通知或对话框。因此,当尝试连接设备时,你的应用不需要考虑设备是否配对过。你的RFCOMM连接尝试将会阻塞,知道用户成功配对,或者用户拒绝失败时,或者配对失败,或者超时
**

BluetoothSocket

代表一个蓝牙socket的接口(和TCP Socket类似)。这是一个连接点,它允许一个应用与其他蓝牙设备通过InputStreamOutputStream交换数据。

BluetoothServerSocket secure = null;    
if(sdk>=10){
secure = adapter.listenUsingRfcommWithServiceRecord(app_name, SECURE_UUID);
}else{
secure = adapter.listenUsingRfcommWithServiceRecord(app_name, SECURE_UUID);
}
//UUID可以参考网上内容自己生成
UUID

一个全局唯一的标识符(UUID)是一个标准的128-bit格式的string ID,它被用于唯一标识信息。一个UUID的关键点是它非常大以至于你可以随机选择而不会发生崩溃。在这种情况下,它被用于唯一地指定你的应用中的蓝牙服务。为了得到一个UUID以在你的应用中使用,你可以使用网络上的任何一种随机UUID产生器,然后使用fromString(String)初始化一个UUID

BluetoothServerSocket

代表一个开放的服务器socket,它监听接受的请求(与TCP ServerSocket类似)。为了连接两台Android设备,一个设备必须使用这个类开启一个服务器socket。当一个远程蓝牙设备开始一个和该设备的连接请求,BluetoothServerSocket将会返回一个已连接的BluetoothSocket,接受该连接。


BluetoothSocket socket;
if (sdk < 10) {
    socket = device.createRfcommSocketToServiceRecord(BlueToothControl.SECURE_UUID);
} else {//sdk >= 10
    socket = device.createInsecureRfcommSocketToServiceRecord(BlueToothControl.INSECURE_UUID);
}
//UUID可以参考网上内容自己生成

下面我会举一个栗子,并且说明是如何进行通讯的

服务端

private class AcceptThread extends Thread {
    private final BluetoothServerSocket mmServerSocket;
 
    public AcceptThread() {
        // 使用一个临时对象来标志mmServerSocket,
        // 因为mmServerSocket是final类型
        BluetoothServerSocket tmp = null;
        try {
            // MY_UUID 是应用的 UUID string, 同样也在客户端使用
            tmp = mBluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
        } catch (IOException e) { }
        mmServerSocket = tmp;
    }
 
    public void run() {
        BluetoothSocket socket = null;
        // 保持监听直到建立连接或者发生异常
        while (true) {
            try {
                socket = mmServerSocket.accept();
            } catch (IOException e) {
                break;
            }
            // 如果接收到连接
            if (socket != null) {
                // 用一个线程去做一些连接的管理工作
                manageConnectedSocket(socket);
                mmServerSocket.close();
                break;
            }
        }
    }
//该方法用于中断监听并且断开连接
public void cancel() {
        try {
            mmServerSocket.close();
        } catch (IOException e) { }
    }
}
  • listenUsingRfcommWithServiceRecord(String, UUID)得到一个BluetoothServerSocket。这个String是你的服务的标志名称,系统将会把它写入设备中的一个新的服务发现协议(SDP)数据库条目中(名字是任意的,并且可以只是你应用的名字)。UUID同样被包含在SDP条目中,并且将会成为和客户端设备连接协议的基础。也就是说,当客户端尝试连接这个设备时,它将会携带一个UUID用于唯一指定它想要连接的服务器。这些UUIDs必须匹配以便该连接可以被接受(在下一步中)。 通过调用accept()开始监听连接请求。
  • 通过调用accept()开始监听连接请求。这一个阻塞调用。在一个连接被接受或一个异常出现时,它将会返回。只有当一个远程设备使用一个UUID发送了一个连接请求,并且该UUID和正在监听的服务器socket注册的UUID相匹配时,一个连接才会被接受。成功后,accept() 将会返回一个已连接的 BluetoothSocket
  • 调用close(),除非你想要接受更多的连接。这将释放服务器socket和它所有的资源,但是不会关闭 accept()返回的已连接的 BluetoothSocket。不同于TCP/IPRFCOMM仅仅允许每一个通道上在某一时刻只有一个已连接的客户端,因此在大多数情况下在接受一个已连接的socket后,在BluetoothServerSocket上调用 close()是非常必要的。
  • accept() 不应该再主活动UI线程上执行,因为它是一个阻塞调用,并且将会阻止任何与应用的交互行为。它通常在你的应用管理的一个新的线程中使用一个BluetoothServerSocketBluetoothSocket 来完成所有工作。为了中止一个阻塞调用,例如accept(),从你的其他线程里在BluetoothServerSocket (或 BluetoothSocket) 上调用close() ,然后阻塞调用就会立即返回。注意在 BluetoothServerSocketBluetoothSocket 上所有的方法都是线程安全的。

客户端

private class ConnectThread extends Thread {
    private final BluetoothSocket mmSocket;
    private final BluetoothDevice mmDevice;
 
    public ConnectThread(BluetoothDevice device) {
        //使用临时变量来标志mmSocket,
        // 因为 mmSocket 是 final
        BluetoothSocket tmp = null;
        mmDevice = device;
 
        // 通过得到的BluetoothDevice 获取 BluetoothSocket来连接
        try {
            // MY_UUID 是应用的UUID string, 同样也用于服务端
            tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
        } catch (IOException e) { }
        mmSocket = tmp;
    }
 
    public void run() {
        // 你应该在连接前总是这样做,而不需要考虑是否真的有在执行查询任务(但是如果你想要检查,调用 isDiscovering())
        //检查会很大程度影响效率
        mBluetoothAdapter.cancelDiscovery();
 
        try {
            // 通过socket.connect()来连接. 同时也会阻塞线程
            // 直到连接成功或者抛出异常
            mmSocket.connect();
        } catch (IOException connectException) {
            // 无法连接,关闭socket并退出
            try {
                mmSocket.close();
            } catch (IOException closeException) { }
            return;
        }
 
        //做一些管理socket的工作
        manageConnectedSocket(mmSocket);
    }
 
  
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

为了和一个远程设备(一个持有服务器socket的设备)初始化一个连接,你必须首先得到一个BluetoothDevice 对象来表示这个远程设备。(上面的课程Finding Devices讲述了如何得到一个 BluetoothDevice )。然后你必须使用BluetoothDevice来得到一个 BluetoothSocket ,然后初始化该连接。
下面是基本的过程:

  • 使用 BluetoothDevice,通过调用createRfcommSocketToServiceRecord(UUID)来得到一个 BluetoothSocket 。T这将初始化一个BluetoothSocket,它连接到该BluetoothDevice。这里传递的UUID必须和服务器设备开启它的 BluetoothServerSocket时使用的UUID相匹配。
  • 通过调用connect()初始化一个连接。执行这个调用时,系统将会在远程设备上执行一个SDP查找工作,来匹配UUID。如果查找成功,并且远程设备接受了连接,它将会在连接过程中分享RFCOMM通道,而 connect()将会返回。这个方法是阻塞的。如果,处于任何原因,该连接失败了或者connect()超时了(大约12秒以后),那么它将会抛出一个异常。
    因为connect()是一个阻塞调用,这个连接过程应该总是在一个单独的线程中执行。
注意:你应该总是确保在你调用connect()时设备没有执行设备查找工作。如果正在查找设备,那么连接尝试将会很大程度的减缓,并且很有可能会失败。

**当你使用完你的 BluetoothSocket后,总是调用close()来清除资源。这样做将会立即关闭已连接的socket,然后清除所有的内部资源
**

读写流

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;
 
        // 获得socket的流信息
        try {
            tmpIn = socket.getInputStream();
            tmpOut = socket.getOutputStream();
        } catch (IOException e) { }
 
        mmInStream = tmpIn;
        mmOutStream = tmpOut;
    }
 
    public void run() {
        byte[] buffer = new byte[1024];  //缓冲字符数组
 
        // 保持通讯
        while (true) {
            try {
                // Read from the InputStream
                bytes = mmInStream.read(buffer);
                // 发送包含的信息给UI
                mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
                        .sendToTarget();
            } catch (IOException e) {
                break;
            }
        }
    }
 
    /* 调用该方法去发送信息 */
    public void write(byte[] bytes) {
        try {
            mmOutStream.write(bytes);
        } catch (IOException e) { }
    }
 
    /* 调用该方法关闭连接*/
    public void cancel() {
        try {
            mmSocket.close();
        } catch (IOException e) { }
    }
}

**当然,实现的细节需要考虑。首先并且最重要的是,你应该为所有输入和输出的数据流使用一个专属的线程。这是十分重要的,因为read(byte[])write(byte[])方法都是阻塞调用。 read(byte[])将会发生阻塞知道送数据流中读取到了一些东西。write(byte[])不经常发生阻塞,但是当远程设备没有足够迅速地调用read(byte[])而中间缓冲区已经负载时可以阻塞。因此,你的线程中的主要循环应该是专门从InputStream中读取数据的。一个单独的公共方法可以被用于初始化向OutputStream中写入数据。
**

如文中有错误,欢迎指出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容