很久没有写博客了,计划一直都有,但总是被这样或者那样的事情给耽搁了,在此写下文字监督自己:不论长短,每周至少一篇!本文根据自己的实践总结而来,参考前人博客之余,也自己总结和开发了一些功能,在这里给自己备份也分享给大家。不同之处在于:自动打开并搜索蓝牙、修改蓝牙名字、完整接收蓝牙传输数据、修改蓝牙密码、解除蓝牙绑定。
系统由上、下位机两部分构成,旨在实现移动端app通过蓝牙通信,将app发送过来的数据存储在下位机的存储单元,与此同时,app也可以通过指令查询下位机的参数设置。系统框架图如下:
界面主要涉及到按键的监听和Activity的跳转:
①按键监听有四种方法,详情可以参考博客;
②Activity的跳转包括:直接跳startActivity转和带返回跳startActivityForResult转,详情可以参考博客
蓝牙功能 | 函数名称 |
---|---|
(1)开启&关闭蓝牙 | enable()/disable() |
(2)修改蓝牙名字 | changeName() |
(3)搜索&显示蓝牙设备 | DeviceListActivity |
(4)配对&连接蓝牙 | connectToServerSocket(address) |
(5)数据收发 | write/ConnectedThread |
(6)修改蓝牙密码 | changeMima() |
(7)取消蓝牙配对 | removeBond |
本系统中实现的功能如表所示,这里不打算赘述已经开发成熟的功能,可以参考这篇博客,讲得比较清晰,这里只针对自己在开发中添加的功能做一下记录,开发之前需要提醒的有两点:
①不要忘记蓝牙权限的设置:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
②让主程序弹出搜索对话框时,不要忘了intent设置;
".DeviceListActivity"
android:theme="@android:style/Theme.Dialog"
android:label="选取连接设备"
android:configChanges="orientation|keyboardHidden"/>
一般的做法都是监听按键(打开蓝牙按键和搜索设备按键),先打开蓝牙,接着搜索蓝牙。但是,这样做无疑使操作更加复杂,在实际生产和用户使用时并不方便,我要做的效果是程序启动的时候自动打开蓝牙并开始搜索设备,于是乎,出问题了,蓝牙虽然打开了,但是不接着搜索设备了,汗。。。解决办法是,打开蓝牙后延时1秒后开始搜索,这样就行了
//得到BluetoothAdapter对象
BluetoothAdapter mBtAdapter = BluetoothAdapter.getDefaultAdapter();
mBtAdapter.enable();//打开蓝牙
Timer timer=new Timer();
TimerTask task=new TimerTask(){//定时1k=new TimerTask(){//定时1秒后搜索设备
@Override
public void run() {
Intent serverIntent = new Intent(MainActivity.this, DeviceListActivity.class); //跳转程序设置
startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE); //设置返回宏定义
}
};
timer.schedule(task,1000);
这一功能有两种实现方式,一种是通过蓝牙传输将名字传输给下位机,用单片机操作蓝牙AT指令直接修改蓝牙名字,这样,以后任何手机搜索该蓝牙设备时,显示的名字都是你所修改的名字;另一种方式是在软件层面实现蓝牙名字的修改,其实是搜索出蓝牙设备的ID,然后将其对应到你想修改的名字,并将这一映射存在本地,那么,以后这部手机下次搜索到该设备时,就会显示你所修改的名字,而不影响其他手机对该设备的显示。这种方法各有特色,就看你的需求了。这里就讲讲第二种方式的实现:
步骤一:搜索蓝牙设备,获取并修改名字
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// TODO Auto-generated method stub
super.onActivityResult(requestCode, resultCode, data);
switch(requestCode) {
case REQUEST_CONNECT_DEVICE: //连接结果,由DeviceListActivity设置返回
if (resultCode == Activity.RESULT_OK) { //选择蓝牙
String address = data.getExtras().getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS);
device = mBluetoothAdapter.getRemoteDevice(address);// 得到蓝牙设备句柄
connectToServerSocket(address);
setting_thread=true;
ConnectedThread thread=new ConnectedThread();
thread.start();
}
break;
default:break;
}
}
在从DeviceListActivity返回的数据中,其实已经包含蓝牙设备的句柄,我们可以通过device.getName()获取当前连接的蓝牙模块的名字,然后将设备的名字或者其唯一标识符(例如,98:D3:34:90:8A:BC)与新修改的名字做映射保存在本地。
步骤二:存储设置到本地,下次搜索蓝牙时做映射显示
使用SharedPreferences存储数据,SharedPreferences是Android平台上一个轻量级的存储类,主要是保存一些常用的配置比如窗口状态,一般在Activity中 重载窗口状态onSaveInstanceState保存一般使用SharedPreferences完成,它提供了Android平台常规的Long长 整形、Int整形、String字符串型的保存。
//获取SharedPreferences对象
Context ctx = MainActivity.this;
SharedPreferences sp = ctx.getSharedPreferences("SP", MODE_PRIVATE);
//存入数据
Editor editor = sp.edit();
editor.putString("STRING_KEY", "string");
editor.commit();
在开发蓝牙数据接收时,发现一个问题:数据有时候会接收不完整。由于项目中的每一位数据返回都十分重要,所以必须保证数据的完整性,要解决这一问题需要清楚的了解如何从InputStream中读取数据,从输入流中读取数据最常用的方法基本上就是如下 3 个 read() 方法:
① read () 方法,这个方法从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1 。
② read (byte[] b,int off,int len) 方法,将输入流中最多 len 个数据字节读入 byte 数组。尝试读取 len 个字节,但读取的字节也可能小于该值。以整数形式返回实际读取的字节数。
③ read (byte[] b) 方法, 从输入流中读取一定数量的字节,并将其存储在缓冲区数组 b 中。以整数形式返回实际读取的字节数。
对于三种方法的选择,需要看具体项目的需求,如果你能够通过某种方法知道数据的长度,那么建议用第一种方法,每次读取一个字节,保证数据的完整性;如果数据长度不知道,那么就用②或者③,一般③用的比较多,这一方法效率高但是读取数据的长度不确定—-不超过数组b的长度,所以就需要多次读取直到把数据全部读完。
本项目中用的是HC-05蓝牙模块,该模块有接收蓝牙指令模式(即通信模式),和AT指令集模式,一般默认状态为蓝牙通信模式,要想修改蓝牙连接密码,需要将蓝牙模块切换到AT指令集模式,具体过程见下面的流程图:
在真实场景中,要想在程序修改完密码后,之前连接的蓝牙设备都无法与该蓝牙模块建立连接,需要解除该模块与手机的绑定,即取消配对。要想解除所有的配对,当然是需要下位机控制蓝牙模块进入AT指令集模式,解除所有绑定,而与此同时,当前修改密码的手机也需要在软件层面解除绑定(配对),这样交互性更强。
用过Android系统设置(Setting)的人都知道蓝牙搜索之后可以建立配对和解除配对,但是这两项功能的函数没有在SDK中给出,本项目利用Java的反射机制去调用removeBond解除绑定,从而取消本机与该蓝牙模块的配对。具体实现程序如下:当程序接收到下位机传来的修改密码成功的指令后,程序会弹出一个强提示,当用户点击确认按钮之后,程序解除绑定并退出程序。
if(modify_flag){//修改密码成功
AlertDialog.Builder builder = new Builder(MainActivity.this);
builder.setTitle("密码修改成功");
builder.setMessage("点击确定按钮重新启动!");
builder.setCancelable(false);
builder.setPositiveButton("确认", new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {//接触绑定
Method m = device.getClass().getMethod("removeBond", (Class[]) null);
m.invoke(device, (Object[]) null);
} catch (Exception e) {
Log.e("erro", e.getMessage());
}
setting_thread=false;
dialog.dismiss();
mBluetoothAdapter.disable();//关闭蓝牙
finish();
System.exit(0);//销毁程序
}
});
builder.setCancelable(false);
builder.create().show();
}else {
Toast.makeText(MainActivity.this, "set failed!", Toast.LENGTH_SHORT).show();
}