目录
蓝牙连接分为经典蓝牙和低功耗蓝牙,此篇为经典蓝牙。Android Studio 官方指导文件对于两种蓝牙连接都有详细的讲解。地址为:
蓝牙概览 | Android 开发者 | Android Developers (google.cn)
一.具体实现
1.向系统获取蓝牙,并判断设备是否支持蓝牙功能。通过 mBluetoothAdapter 对象实现蓝牙的相关功能。
//对象声明
private BluetoothAdapter mBluetoothAdapter;
//蓝牙获取
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
//判断 mBluetoothAdapter 是否获取成功
if (mBluetoothAdapter == null) {
Log.e(TAG, "onCreate: 此设备不支持蓝牙");
}
2.判断设备蓝牙是否打开,没有则请求打开蓝牙。
//判断蓝牙是否打开 true为打开
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
3.查询已配对的设备
此功能会获取所有之前设备配对过且没有删除的设备信息。此功能可以快速查看需要连接的设备是否已经处于了已检测到状态,从而可以直接实现连接。
在此方法中可以看到,我是通过动态添加按钮的方式将已连接过的设备信息进行了输出,并对按钮添加了监听事件,从而实现点击选择指定设备进行连接。
private void checkpairedDevices() {
Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
LinearLayout layout2=(LinearLayout) findViewById(R.id.myLinearLayout2);
if (pairedDevices.size() > 0) {
// There are paired devices. Get the name and address of each paired device.
for (BluetoothDevice device : pairedDevices) {
String deviceName = device.getName();
String deviceHardwareAddress = device.getAddress(); // MAC address
//将连接过的设备信息输出
Log.i("已连接过的设备:", deviceName+"---"+deviceHardwareAddress);
//以下是根据自己的需要对设备信息进行的处理
Button button=new Button(this);
button.setText(deviceName+"_: "+deviceHardwareAddress);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
//点击进行连接
connectThread=new ConnectThread(device);
connectThread.run();
}
});
layout2.addView(button);
}
}
}
4.蓝牙扫描
需要注意,蓝牙扫描之前必须开启GPS才能成功扫描,且蓝牙扫描需要注册广播,应用必须针对
ACTION_FOUND
Intent 注册一个 BroadcastReceiver,以便接收每台发现的设备的相关信息。
蓝牙扫描实现:
private void Bluetoothscan() {
//发现设备,实现扫描功能
boolean f = mBluetoothAdapter.startDiscovery();
if (f == true) {
Log.i(TAG, "成功进行扫描以发现设备");
} else {
Log.i(TAG, "扫描失败");
}
}
申请打开GPS,申请打开GPS的方法有很多,此处列举我使用过的两种方法:
方法1:
protected void onCreate(Bundle savedInstanceState) {
//打开GPS的权限申请
List<String> permissionList = new ArrayList<>();
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
if (!permissionList.isEmpty()) {
String[] permissions = permissionList.toArray(new String[permissionList.size()]);
ActivityCompat.requestPermissions(MainActivity.this, permissions, 1);
}
......
}
方法2:
public void GPS() {
LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
boolean isEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
if (!isEnabled) {
androidx.appcompat.app.AlertDialog.Builder builder = new androidx.appcompat.app.AlertDialog.Builder(this);
builder.setTitle("蓝牙扫描需要开启GPS,是否进入设置开启?").setNegativeButton("取消", null);
builder.setPositiveButton("确认",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
Intent myIntent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(myIntent);
}
});
builder.show();
}
}
注册广播接收蓝牙扫描到的其他蓝牙设备的信息,并对数据实现自己想要的处理:
//注册广播
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(receiver, filter);
创建注册的广播:
我在使用官方给出的广播方法时,发现同一设备会被多次扫描到而被多次输出。因此,在此广播中我加入了一个数组对扫描信息进行了判断,从而是实现扫描到的设备不多次输出展示。
//创建广播
private final BroadcastReceiver receiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
boolean f=true;
String action = intent.getAction();
// Discovery has found a device. Get the BluetoothDevice
BluetoothDevice device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
//判断扫描到的设备是否已经被输出过
for(int i=0;i<number;i++){
if(scanresult[i].equals(device.getAddress())) {
f=false; break;}
}
if (BluetoothDevice.ACTION_FOUND.equals(action)&&f) {
//进入此if语句中则代表扫描到了一个新的蓝牙设备,以下是对此设备信息的处理
//将扫描到的蓝牙mac地址存入字符串组,保证唯一输出显示
scanresult[number]=device.getAddress();
number++;
// object and its info from the Intent.
String deviceName = device.getName();
String deviceHardwareAddress = device.getAddress(); // MAC address
//以动态添加按钮的方式输出扫描到的蓝牙设备信息
LinearLayout layout2=(LinearLayout) findViewById(R.id.myLinearLayout2);
Button button=new Button(context);
button.setText(deviceName+"_: "+deviceHardwareAddress);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
//这里写入子线程需要做的工作
Thread conThread = new Thread(new Runnable() {
@Override
public void run() {
//点击进行连接
connectThread=new ConnectThread(device);
connectThread.run();
}
});
conThread.start(); //启动线程
}
});
layout2.addView(button);
}
}
};
我对此处广播的理解:此处的广播就相当于一个监听,我们对这个监听函数的监听方法进行了实现并对其进行注册,使其处于监听工作状态,因为我们的蓝牙扫描并没有实现对扫描数据的获取和处理,因此,此处的广播就配合扫描动作,接收蓝牙扫描获取到的数据,并实现对数据的处理。
5.设备连接
蓝牙连接有两种方式:1.作为服务器连接 ,2.作为客户端连接。作为服务器则自己端发送数据,其他设备接收,作为客户端则相反。
1.作为服务器连接:
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 = bluetoothAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) {
Log.e(TAG, "Socket's listen() method failed", e);
}
mmServerSocket = tmp;
}
public void run() {
BluetoothSocket socket = null;
// Keep listening until exception occurs or a socket is returned.
while (true) {
try {
socket = mmServerSocket.accept();
} catch (IOException e) {
Log.e(TAG, "Socket's accept() method failed", e);
break;
}
if (socket != null) {
// A connection was accepted. Perform work associated with
// the connection in a separate thread.
manageMyConnectedSocket(socket);
mmServerSocket.close();
break;
}
}
}
// Closes the connect socket and causes the thread to finish.
public void cancel() {
try {
mmServerSocket.close();
} catch (IOException e) {
Log.e(TAG, "Could not close the connect socket", e);
}
}
}
2.作为客户端连接:
private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
// Use a temporary object that is later assigned to mmSocket
// because mmSocket is final.
BluetoothSocket tmp = null;
mmDevice = device;
try {
// Get a BluetoothSocket to connect with the given BluetoothDevice.
// MY_UUID is the app's UUID string, also used in the server code.
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
Log.e(TAG, "Socket's create() method failed", e);
}
mmSocket = tmp;
}
public void run() {
// Cancel discovery because it otherwise slows down the connection.
bluetoothAdapter.cancelDiscovery();
try {
// Connect to the remote device through the socket. This call blocks
// until it succeeds or throws an exception.
mmSocket.connect();
} catch (IOException connectException) {
// Unable to connect; close the socket and return.
try {
mmSocket.close();
} catch (IOException closeException) {
Log.e(TAG, "Could not close the client socket", closeException);
}
return;
}
// The connection attempt succeeded. Perform work associated with
// the connection in a separate thread.
manageMyConnectedSocket(mmSocket);
}
// Closes the client socket and causes the thread to finish.
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "Could not close the client socket", e);
}
}
}
6.管理连接
连接成功后,通过以下方法实现对蓝牙通信的信息数据进行处理:
public class MyBluetoothService {
private static final String TAG = "MY_APP_DEBUG_TAG";
private Handler handler; // handler that gets info from Bluetooth service
// Defines several constants used when transmitting messages between the
// service and the UI.
private interface MessageConstants {
public static final int MESSAGE_READ = 0;
public static final int MESSAGE_WRITE = 1;
public static final int MESSAGE_TOAST = 2;
// ... (Add other message types here as needed.)
}
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;
private byte[] mmBuffer; // mmBuffer store for the stream
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();
} catch (IOException e) {
Log.e(TAG, "Error occurred when creating input stream", e);
}
try {
tmpOut = socket.getOutputStream();
} catch (IOException e) {
Log.e(TAG, "Error occurred when creating output stream", e);
}
mmInStream = tmpIn;
mmOutStream = tmpOut;
}
public void run() {
mmBuffer = new byte[1024];
int numBytes; // bytes returned from read()
// Keep listening to the InputStream until an exception occurs.
while (true) {
try {
// Read from the InputStream.
numBytes = mmInStream.read(mmBuffer);
// Send the obtained bytes to the UI activity.
Message readMsg = handler.obtainMessage(
MessageConstants.MESSAGE_READ, numBytes, -1,
mmBuffer);
readMsg.sendToTarget();
} catch (IOException e) {
Log.d(TAG, "Input stream was disconnected", e);
break;
}
}
}
// Call this from the main activity to send data to the remote device.
public void write(byte[] bytes) {
try {
mmOutStream.write(bytes);
// Share the sent message with the UI activity.
Message writtenMsg = handler.obtainMessage(
MessageConstants.MESSAGE_WRITE, -1, -1, mmBuffer);
writtenMsg.sendToTarget();
} catch (IOException e) {
Log.e(TAG, "Error occurred when sending data", e);
// Send a failure message back to the activity.
Message writeErrorMsg =
handler.obtainMessage(MessageConstants.MESSAGE_TOAST);
Bundle bundle = new Bundle();
bundle.putString("toast",
"Couldn't send data to the other device");
writeErrorMsg.setData(bundle);
handler.sendMessage(writeErrorMsg);
}
}
// Call this method from the main activity to shut down the connection.
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) {
Log.e(TAG, "Could not close the connect socket", e);
}
}
}
}
设备连接,管理连接总结:
以上第5,6点主要实现的是对指定蓝牙设备的连接和实现两设备之间的通信,以下提供我使用的方法,此方法总和了连接和连接管理:
这里是作为客户端进行蓝牙连接,实现对其他蓝牙设备的数据进行接收,代码中有详细注释。
//此方法参数为需要连接的蓝牙设备的MAC地址。
public void connectBT2(String address){
if (btSocket != null) {return;}
if(address.isEmpty()){ return;}
if (mBluetoothAdapter == null) {
return;
}
//利用MAC地址生成device对象,用于btSocket创建
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
try {
btSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) {
}
try {
//蓝牙连接,连接的成功失败会有对应提示:
btSocket.connect();
//runOnUiThread :不能在主线程以外操作UI,在子线程中对UI进行修改需要调用的方法。
runOnUiThread(new Runnable() {
@Override
public void run() {
connect.setText(""+"设备已连接");//此处为我app中的提示信息,可删除
}
});
} catch (IOException e) {
btSocket=null;
Log.e("", "connectBT2: "+"连接失败 1" +e);
return;
}
try {
if(btSocket!=null) {
inStream = btSocket.getInputStream();
outStream = btSocket.getOutputStream();
lastBTHB = System.currentTimeMillis();
}
} catch (IOException e) {
try {
btSocket.close();
} catch (IOException e2) { }
btSocket=null;
inStream=null;//数据输入流,用于接收数据
outStream=null;//数据输出流,用于发送数据
return;
}
//以下开始对蓝牙通信传输的数据进行处理
readBuffer = new byte[1024];
btReadThread = new Thread(new Runnable() {
@Override
public void run() {
int bytes = 0;
while (true&&!readThreadstop) {
try {
try{
Thread.sleep(1);
}catch(InterruptedException ie){ }
if(inStream.available()>0){
Log.e(TAG, "run: 有数据传入" );
byte[] temp = new byte[1024];
// Read from the InputStream
if( (bytes = inStream.read(temp)) > 0 )
{
Log.e(TAG, "run:接收到的数据 "+bytes );
byte[] temp1 = new byte[bytes];
for(int i =0;i<bytes;i++)
{
temp1[i]=temp[i];
}
final String recstr=new String(temp1);
runOnUiThread(new Runnable() {
@Override
public void run() {
//此处实现app需要对接收到的数据进行的处理
}
}); } } }
catch (IOException e) {} } }
});
readThreadstop =false;
btReadThread.start();
}
如果需要实现对其他蓝牙设备进行数据发送可以调用下面这个方法:
数据发送:
//参数为需要发送的数据
public void write(String send) {
byte[] bytes = send.getBytes();
new Thread(new Runnable() {
@Override
public void run() {
try {
outStream.write(bytes);
} catch (IOException e) {
Log.e("TAG", "Error occurred when sending data", e);
}
}
}).start();
}
至此,蓝牙的基本功能都实现了。但在蓝牙功能的实现中有很多需要注意的细节,这些在Android Studio 的指导文档里都有详细讲解。
二.我遇到的问题和解决
1.蓝牙扫描发现不了设备。
GPS没有打开。
2.蓝牙扫描需要注册广播。
没有理解广播之前,我无法理解扫描和广播的工作逻辑,因此建议有困难的可以专门去看看Android Studio中的广播。
3.线程问题。
1.在蓝牙的扫描,连接,数据的接收,发送对是否建立新的线程都有要求,详细可以看官方文档的说明。
2.在子线程中对UI进行更新需要使用 runOnUiThread 线程。
4.在蓝牙连接时有时会出现 read failed, socket might closed or timeout, read ret: -1 的报错,从而连接失败。
目前我在网上找到了两个方法:
1.修改自己的 UUID
2.设置蓝牙连接时的端口号
我使用的方法:
3.在连接时,将MAC地址作为参数,利用MAC地址生成device对象,用于btSocket创建。
方法总结:第一个方法在我这完全没用,第二个方法解决了第一次出现这种报错。之后在第二个方法上又出现了这种报错,然后使用第三个方法可以实现连接不报错,但是在多次的使用中还是会有几率出现报错。因此目前我也没有100%的解决这个 read ret: -1 de 报错。如果有大神知道这个报错的根本问题,希望得到指点,多谢!