Android蓝牙 - 经典蓝牙BT

一、蓝牙发展历程

蓝牙(Bluetooth):是一种无线技术标准,可实现设备间短距离数据交换。 蓝牙可以以一定的周期发送广播,手机端接收到广播后,解析广播包,可做设备识别、配对,事件通知以及指令控制等。低精度定位根据设备的信号强度,可以估算出大概方位和距离。

蓝牙发展至今经历了多个版本的更新,其中,将1.x~3.0之间的版本称之为经典蓝牙,4.x开始的蓝牙称之为低功耗蓝牙,也就是蓝牙ble。

根据应用、协议类型等,可以对蓝牙进行以下分类:


image.png

二、经典蓝牙API

1、BluetoothAdapter:代表了移动设备的本地的蓝牙适配器, 通过该蓝牙适配器可以对蓝牙进行基本操作, 例如 : 启动设备发现,获取已配对设备,通过mac蓝牙地址获取蓝牙设备等。

  • enable():打开蓝牙,需要 BLUETOOTH_ADMIN权限。
  • disable():关闭蓝牙,需要 BLUETOOTH_ADMIN权限。
  • checkBluetoothAddress(String address):验证蓝牙设备MAC地址是否有效。
  • getAddress():获取本地蓝牙适配器的硬件地址(MAC地址)
  • getBondedDevices():获取与本机蓝牙所有绑定的远程蓝牙信息。
  • getName():获取本地蓝牙适配器的蓝牙名称。
  • setName(String name):设置本地蓝牙适配器的蓝牙名称。
  • isEnabled():判断当前蓝牙适配器是否打开。
  • isDiscovering():判断蓝牙适配器是否正在处于扫描过程中。
  • startDiscovery():开始扫描周边蓝牙设备。
  • cancelDiscovery():取消蓝牙搜索操作。
  • getScanMode():获取本地蓝牙适配器的当前蓝牙扫描模式。

蓝牙扫描模式:
SCAN_MODE_NONE: 该设备不能扫描以及被扫描。
SCAN_MODE_CONNECTABLE:该设备可以扫描其他蓝牙设备。
SCAN_MODE_CONNECTABLE_DISCOVERABLE:该设备既可以扫描其他设备,也可以被其他设备扫描发现。

  • getState():获取本地蓝牙适配器的当前状态。

蓝牙适配器状态:
STATE_OFF:表示本地蓝牙适配器已关闭。
STATE_TURNING_ON:表示本地蓝牙适配器正在打开。
STATE_ON:表示本地蓝牙适配器已开启,并可供使用。
STATE_TURNING_OFF:表示本地蓝牙适配器正在关闭。

  • getRemoteDevice(String address):获取给定蓝牙硬件地址的BluetoothDevice对象。

如果address中的MAC无效无效,将抛出IllegalArgumentException异常。

  • listenUsingInsecureRfcommWithServiceRecord(String name, UUID uuid):创建不安全的蓝牙服务套接字。
  • listenUsingRfcommWithServiceRecord(String name, UUID uuid):创建一个正在监听的安全的带有服务记录的无线射频通信(RFCOMM)蓝牙端口。

2、BluetoothDevice:代表了一个远程的蓝牙设备, 通过这个类可以查询远程设备的物理地址, 名称, 连接状态等信息。这个类实际上只是一个蓝牙硬件地址的简单包装,这个类的对象是不可变的。对这个类的操作, 会执行在远程蓝牙设备的硬件上。

  • getName():获取远程蓝牙设备的蓝牙名称。
  • getAddress():获取远程蓝牙设备的硬件地址。
  • createBond():开始与远程蓝牙设备的绑定过程。
  • getBondState():获取远程蓝牙设备的绑定状态。

蓝牙绑定状态:
BOND_NONE:远程设备未绑定。
BOND_BONDING:正在与远程设备进行绑定。
BOND_BONDED:远程设备已绑定。

  • createInsecureRfcommSocketToServiceRecord(UUID uuid):创建不安全的蓝牙套接字。
  • createRfcommSocketToServiceRecord(UUID uuid):创建安全的蓝牙套接字。

3、BluetoothServerSocket:侦听蓝牙服务套接字。使用BluetoothServerSocket可以创建一个监听服务端口, 使用accept方法阻塞, 当该方法监测到连接的时候, 就会返回一个BluetoothSocket对象来管理这个连接。BluetoothServerSocket是线程安全的,close方法始终会立即中止正在进行的操作并关闭蓝牙服务套接字。需要BLUETOOTH权限。

  • accept():阻塞直到建立连接,成功连接时连接的BluetoothSocket对象。
  • accept(int timeout):阻塞直到建立连接或超时,成功连接时连接的BluetoothSocket对象。
  • close():关闭该监听服务端口,并释放所有关联的资源。

4、BluetoothSocket:蓝牙套接口。在服务器端,使用BluetoothServerSocket创建侦听服务器套接字。当连接被BluetoothServerSocket接受时,它将返回一个新的BluetoothSocket来管理连接。 在客户端,使用单个BluetoothSocket来启动连接并管理连接。BluetoothSocket是线程安全的,close方法始终会立即中止正在进行的操作并关闭套接字。需要BLUETOOTH权限。

  • connect():尝试连接到远程蓝牙服务器。
  • isConnected():获取此套接字的连接状态,即是否与远程蓝牙服务连接。
  • getRemoteDevice():获取此套接字连接的远程蓝牙设备。
  • getInputStream():获取与此套接字关联的输入流。

即使套接字尚未连接,输入流也会返回,但对该流的操作将抛出IOException异常,直到关联的套接字连接。

  • getOutputStream():获取与此套接字关联的输出流。

即使套接字尚未连接,输出流也会返回,但对该流的操作将抛出IOException异常,直到关联的套接字连接。

  • close():关闭此流并释放与其关联的所有系统资源。如果流已经关闭,则调用此方法不起作用。

三、经典蓝牙开发

1、在工程清单文件AndroidManifest.xml中添加权限:

<!--如果使用了BLUETOOTH_ADMIN权限,那么必须使用BLUETOOTH权限-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

<!--android6.0后需要搜索周边蓝牙设备,需要添加以下两个权限-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

<!--要求设备硬件必须支持蓝牙-->
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>

2、获取本地蓝牙适配器
BluetoothAdapter有两种方式获取,方式二要求Android4.3以上才可以用,建议使用方式一,比较通用。

//第一种方式
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

//第二种方式
BluetoothManager manager = (BluetoothManager) Context.getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter mBluetoothAdapter = manager.getAdapter();

3、打开蓝牙
方式一:通过Intent来向用户弹框请求打开蓝牙,可以重写onActivityResult来监听打开蓝牙的请求结果.

public void openBluetooth(){
    if (mBluetoothAdapter==null) {
        //自定义方法,用来往TextView上添加提示信息
        showTip("当前设备不支持蓝牙功能!");
        return;
    }

    if (mBluetoothAdapter.isEnabled()) {
        showTip("蓝牙已打开");
        return;
    }

    Intent intent=new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(intent,GlobalDef.REQ_CODE_OPEN_BT);
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode==GlobalDef.REQ_CODE_OPEN_BT) {
        if (resultCode == Activity.RESULT_OK) {
            showTip("蓝牙打开成功");
        } else {
            showTip("蓝牙打开失败");
        }
    }
}

方式二:通过enable方法静默打开蓝牙,无需用户同意(部分Android系统使用该方法依然会弹框提示,向用户请求打开蓝牙)

mBluetoothAdapter.enable();

4、关闭蓝牙
关闭蓝牙,无需用户同意(部分Android系统使用该方法依然会弹框提示)

mBluetoothAdapter.disable();

5、允许蓝牙可被发现
如果开发的外围设备如音箱,需要被中心设备发现扫描到,需要该功能。有两种实现方式:
方式一:通过Intent方式向用户请求允许蓝牙被搜索。如果蓝牙没有开启,用户点击确定后,会首先开启蓝牙,继而设置蓝牙能被扫描。

Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
//设置蓝牙可见性的时间,默认持续时间为120秒,每个请求的最长持续时间上限为300秒
intent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 120);
startActivity(intent);

方式二:通过反射的方式来设置蓝牙可见性,且不会出现弹框,如果蓝牙没有开启,通过此方式并不会直接打开蓝牙。

/**
 * 设置蓝牙可见
 * @param adapter
 * @param timeout 超时为0时,永久可见
 */
public static void setDiscoverableTimeout(BluetoothAdapter adapter, int timeout) {
    BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
    try {
        Method setDiscoverableTimeout = BluetoothAdapter.class.getMethod("setDiscoverableTimeout", int.class);
        setDiscoverableTimeout.setAccessible(true);
        Method setScanMode = BluetoothAdapter.class.getMethod("setScanMode", int.class, int.class);
        setScanMode.setAccessible(true);

        setDiscoverableTimeout.invoke(adapter, timeout);
        setScanMode.invoke(adapter, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, timeout);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

6、注册蓝牙广播

mBluetoothBroadcastReceiver = new BluetoothBroadcastReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_FOUND);//扫描到设备会通过该广播获取到
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);//设备的绑定状态
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);//蓝牙状态
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);//发起扫描
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//结束扫描
mContext.registerReceiver(mBluetoothBroadcastReceiver,filter);

广播类:

class BluetoothBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        Log.d(TAG,"Action received is "+action);
        //蓝牙搜索
        if (BluetoothDevice.ACTION_FOUND.equals(action)) {
            BluetoothDevice scanDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            if(scanDevice == null || scanDevice.getName() == null){
                return;
            }

            int btType = scanDevice.getType();
            if(btType == BluetoothDevice.DEVICE_TYPE_LE || btType==BluetoothDevice.DEVICE_TYPE_UNKNOWN){
                return;
            }

            Log.d(TAG, "bt name=" + scanDevice.getName() + " address=" + scanDevice.getAddress());
            //将搜索到的蓝牙设备加入列表
            deviceList.add(scanDevice);
            short rssi = intent.getExtras().getShort(BluetoothDevice.EXTRA_RSSI);
            rssiList.add(rssi);
            listAdapter.notifyDataSetChanged();//通知ListView适配器更新
        }
        //蓝牙配对
        else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
            BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
            if(mCurDevice!=null && btDevice.getAddress().equals(mCurDevice.getAddress())){
                int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
                if (state == BluetoothDevice.BOND_NONE) {                    
                    Log.i(TAG,"已取消与设备" + btDevice.getName() + "的配对");         
                } else if (state == BluetoothDevice.BOND_BONDED) {
                    Log.i(TAG,"与设备" + btDevice.getName() + "配对成功");                   
                }
            }
        }  else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
            int blueState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 0);
            switch (blueState) {
                case BluetoothAdapter.STATE_TURNING_ON:
                    Log.i(TAG,"STATE_TURNING_ON 手机蓝牙正在开启");
                    break;
                case BluetoothAdapter.STATE_ON:
                    Log.i(TAG,"STATE_ON 手机蓝牙开启");                  
                    break;
                case BluetoothAdapter.STATE_TURNING_OFF:
                    Log.i(TAG,"STATE_TURNING_OFF 手机蓝牙正在关闭");
                    break;
                case BluetoothAdapter.STATE_OFF:
                    Log.i(TAG,"STATE_OFF 手机蓝牙关闭");
                    break;
            }
        }
    }
}

7、发起扫描

if (mBluetoothAdapter.isDiscovering()) {
    mBluetoothAdapter.cancelDiscovery();
}
//搜索到的蓝牙设备通过广播接收
mBluetoothAdapter.startDiscovery();

8、与扫描到的设备进行配对

private void pair(BluetoothDevice device) {
    //在配对之前,停止搜索
    if (mBluetoothAdapter.isDiscovering()) {
        mBluetoothAdapter.cancelDiscovery();
    }
    if (device.getBondState() != BluetoothDevice.BOND_BONDED) {//没配对才配对
        try {
            Method createBondMethod = BluetoothDevice.class.getMethod("createBond");
            Boolean returnValue = (Boolean) createBondMethod.invoke(device);
            if (returnValue){
                Log.d(TAG, "配对成功...");
            }
         } catch (NoSuchMethodException e) {
                e.printStackTrace();
         } catch (IllegalAccessException e) {
                e.printStackTrace();
         } catch (InvocationTargetException e) {
                e.printStackTrace();
         }
    }
}


public void unpair(BluetoothDevice device){
    Log.d(TAG, "attemp to cancel bond:" + device.getName());
    try {
        Method removeBondMethod = device.getClass().getMethod("removeBond");
        Boolean returnValue = (Boolean) removeBondMethod.invoke(device);
        if (returnValue){
            Log.d(TAG, "解配对成功...");
        }
    } catch (Exception e) {           
        e.printStackTrace();
        Log.e(TAG, "attemp to cancel bond fail!");
    }
}

9、蓝牙连接
经典蓝牙连接相当于socket连接,是个非常耗时的操作,所以应该放到子线程中去完成。
9.1 新建ConnectBlueTask

public class ConnectBlueTask extends AsyncTask<BluetoothDevice, Integer, BluetoothSocket> {
    private static final String TAG = ConnectBlueTask.class.getName();
    private BluetoothDevice bluetoothDevice;
    private ConnectBlueCallBack callBack;
 
    public ConnectBlueTask(ConnectBlueCallBack callBack){
        this.callBack = callBack;
    }
 
    @Override
    protected BluetoothSocket doInBackground(BluetoothDevice... bluetoothDevices) {
        bluetoothDevice = bluetoothDevices[0];
        BluetoothSocket socket = null;
        try{
            Log.d(TAG,"开始连接socket,uuid:" + ClassicsBluetooth.UUID);
            socket = bluetoothDevice.createRfcommSocketToServiceRecord(UUID.fromString(ClassicsBluetooth.UUID));
            if (socket != null && !socket.isConnected()){
                socket.connect();
            }
        }catch (IOException e){
            Log.e(TAG,"socket连接失败");
            try {
                socket.close();
            } catch (IOException e1) {
                e1.printStackTrace();
                Log.e(TAG,"socket关闭失败");
            }
        }
        return socket;
    }
 
    @Override
    protected void onPreExecute() {
        Log.d(TAG,"开始连接");
        if (callBack != null) callBack.onStartConnect();
    }
 
    @Override
    protected void onPostExecute(BluetoothSocket bluetoothSocket) {
        if (bluetoothSocket != null && bluetoothSocket.isConnected()){
            Log.d(TAG,"连接成功");
            if (callBack != null) callBack.onConnectSuccess(bluetoothDevice, bluetoothSocket);
        }else {
            Log.d(TAG,"连接失败");
            if (callBack != null) callBack.onConnectFail(bluetoothDevice, "连接失败");
        }
    }
}

9.2 启动连接线程

/**
 * 连接 (在配对之后调用)
 * @param device
 */
public void connect(BluetoothDevice device, ConnectBlueCallBack callBack){
    if (device == null){
        Log.d(TAG, "bond device null");
        return;
    }
    if (!isBlueEnable()){
        Log.e(TAG, "Bluetooth not enable!");
        return;
    }
    //连接之前把扫描关闭
    if (mBluetoothAdapter.isDiscovering()){
        mBluetoothAdapter.cancelDiscovery();
    }
    new ConnectBlueTask(callBack).execute(device);
}

9.3 判断是否连接成功

/**
 * 蓝牙是否连接
 * @return
 */
public boolean isConnectBlue(){
    return mBluetoothSocket != null && mBluetoothSocket.isConnected();
}

9.4 断开连接

/**
 * 断开连接
 * @return
 */
public boolean cancelConnect(){
    if (mBluetoothSocket != null && mBluetoothSocket.isConnected()){
        try {
            mBluetoothSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }
    mBluetoothSocket = null;
    return true;
}

9.5 MAC地址连接

/**
 * 输入mac地址进行自动配对
 * 前提是系统保存了该地址的对象
 * @param address
 * @param callBack
 */
public void connectMAC(String address, ConnectBlueCallBack callBack) {
    if (!isBlueEnable()){
        return ;
    }
    BluetoothDevice btDev = mBluetoothAdapter.getRemoteDevice(address);
    connect(btDev, callBack);
}

10、蓝牙通信
10.1 读取数据线程

public class ReadTask extends AsyncTask<String, Integer, String> {
    private static final String TAG = ReadTask.class.getName();
    private ReadCallBack callBack;
    private BluetoothSocket socket;
 
    public ReadTask(ReadCallBack callBack, BluetoothSocket socket){
        this.callBack = callBack;
        this.socket = socket;
    }
    @Override
    protected String doInBackground(String... strings) {
        BufferedInputStream in = null;
        try {
            StringBuffer sb = new StringBuffer();
            in = new BufferedInputStream(socket.getInputStream());
 
            int length = 0;
            byte[] buf = new byte[1024];
            while ((length = in.read()) != -1) {
                sb.append(new String(buf,0,length));
            }
            return sb.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return "读取失败";
    }
 
    @Override
    protected void onPreExecute() {
        Log.d(TAG,"开始读取数据");
        if (callBack != null) callBack.onStarted();
    }
 
    @Override
    protected void onPostExecute(String s) {
        Log.d(TAG,"完成读取数据");
        if (callBack != null){
            if ("读取失败".equals(s)){
                callBack.onFinished(false, s);
            }else {
                callBack.onFinished(true, s);
            }
        }
    }
}

10.2 写入数据线程

public class WriteTask extends AsyncTask<String, Integer, String>{
    private static final String TAG = WriteTask.class.getName();
    private WriteCallBack callBack;
    private BluetoothSocket socket;
 
    public WriteTask(WriteCallBack callBack, BluetoothSocket socket){
        this.callBack = callBack;
        this.socket = socket;
    }
    @Override
    protected String doInBackground(String... strings) {
        String string = strings[0];
        OutputStream outputStream = null;
        try{
            outputStream = socket.getOutputStream();
 
            outputStream.write(string.getBytes());
        } catch (IOException e) {
            Log.e("error", "ON RESUME: Exception during write.", e);
            return "发送失败";
        }finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return "发送成功";
 
 
    }
 
    @Override
    protected void onPreExecute() {
        if (callBack != null) callBack.onStarted();
    }
 
    @Override
    protected void onPostExecute(String s) {
        if (callBack != null){
            if ("发送成功".equals(s)){
                callBack.onFinished(true, s);
            }else {
                callBack.onFinished(false, s);
            }
 
        }
    }
}

参考文章:
Android经典蓝牙开发全流程
Android蓝牙开发—经典蓝牙详细开发流程

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

推荐阅读更多精彩内容

  • 一、前言: 普通蓝牙设备官方文档 Android 平台包含蓝牙网络堆栈支持,凭借此支持,设备能以无线方式与其他蓝牙...
    因为我的心阅读 8,696评论 1 9
  • 描述 最近公司有个项目,App从后台获取到数据,App连接打印机,将数据在打印机上打印。公司提供的测试设备是蓝牙打...
    So_ProbuING阅读 1,647评论 1 2
  • 声明 普通蓝牙设备官方文档 Android 平台包含蓝牙网络堆栈支持,凭借此支持,设备能以无线方式与其他蓝牙设备交...
    bug喵喵阅读 639评论 0 0
  • 普通蓝牙设备官方文档 Android 平台包含蓝牙网络堆栈支持,凭借此支持,设备能以无线方式与其他蓝牙设备交换数据...
    sydMobile阅读 69,454评论 5 43
  • 前言 最近在做Android蓝牙这部分内容,所以查阅了很多相关资料,在此总结一下。 基本概念 Bluetooth是...
    猫疏阅读 14,530评论 7 113