标签:mis imp 图形化显示 enable unp hid discover short 无法
博主小白一枚,这个app只是大三时候和一位学长一起做了这个用于测量吊车倾角的app,硬件上是有两个姿态传感器,将姿态传感器的数据通过总机接收汇总之后,通过总机和手机之间的蓝牙连接,将数据上传到手机上,在手机上实时的绘制出当前的吊钩倾角状态。现在做一下总结也算是对这次经历的反思和改进,第一次做一个实际的项目,所以比较混乱,欢迎各位看后提出改进意见。
界面很简单,只有一个设置界面和一个进行数据显示的界面
设置界面主要是查找设备,进行初始化,进行系统清除,控制语音播报,显示配对设备和搜寻到的设备
视图界面主要是将接收到的数据进行图形化显示。上边的坐标视图显示的是吊车吊钩在平面坐标系下的投影(Y轴正方向是吊车司机面向的方向,X轴正方向是吊车司机自然抬起右手的方向),下边的视图是吊车吊钩与铅锤线的夹角,最下边的数据面板显示的是当前的数据实时情况,包括两台姿态传感器分别X,Y轴的角度,和铅垂线的角度,以及设备的电量
首先理一下粗略的工作流程:
这是整个系统的工作流程,其实逻辑并不复杂。
首先是进入APP搜索所有的蓝牙设备,并选择下位总机的蓝牙模块进行连接。这里有两种状况:
连接成功之后开始接收数据 ,这时候需要先进行数据的校正(也就是把当前接受到的数据作为初始值校零),并将用于校零的数据保存下来(用于app意外退出,再次进入时恢复初识状态)。
完成初始化校正之后就开始了数据的传输和实时显示,由于是测量吊车吊钩倾角,所以使用的是SurfaceView来实时模拟吊钩运动状态的,同时在屏幕最下边有当前已经校准过的精确数据。
第一种方式:
首先定义IntentFilter ,并注册广播监听器,用于获取蓝牙的状态
//定义IntentFilter对象并注册广播
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);//状态的改变
filter.addAction(BluetoothDevice.ACTION_FOUND);//发现新设备
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);//开始扫描
filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);//扫描结束
filter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST);//配对请求
filter.addAction(Constant.BLUETOOTH_STATE_CHANGED);
registerReceiver(receiver, filter);
这是我们需要监听的系统广播,同时也需要我们自己实现一个BroadcastReceiver,用于处理监听到的广播。
自定义的BroadcastReceiver ,并重写onReceive方法
BroadcastReceiver receiver = 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);
//若未配对过,则将设备名和地址保存在unpairedDevicesList当中,并更新适配器
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
//判断是否已经包含此设备,若不包含,则加入未配对列表
if (!unpairedDevicesList.contains(device.getName() + "|"+ device.getAddress())) {
//更新设备列表和设备名称列表
unpairedBluetoothList.add(device);
unpairedDevicesList.add(device.getName() + "|"+ device.getAddress() + "\n");
//更新适配器
unpairedArrayAdapter.notifyDataSetChanged();
}
}
} else if (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {//蓝牙搜寻结束
mProgressDlg.dismiss();
Toast.makeText(Main.this, "搜寻完毕", Toast.LENGTH_SHORT).show();
} else if (BluetoothAdapter.ACTION_DISCOVERY_STARTED.equals(action)) {//蓝牙搜寻开始
mProgressDlg.show();
}
else if (Constant.BLUETOOTH_STATE_CHANGED.equals(action)) {//蓝牙的连接状态发生改变
Log.e("state", "监听到状态的改变" + action);
// TODO: 2016/1/17 蓝牙的自动重连
IOUtils.closeIO(clientSocket, handler);
}
}
};
接下来就是需要处理搜寻到的设备,这里分为两种,一种是已经配对过的,另一种是尚未配对的,我们首先处理尚未配对设备的点击事件:
实现一个未配对列表item点击事件的监听器:
/**
* 用于处理未配对列表的点击事件
*/
private class UnPairedListener implements AdapterView.OnItemClickListener {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
//获取未配对设备列表中被点击的设备
BluetoothDevice device = unpairedBluetoothList.get(position);
/**
*在这里进行设备PIN码的输入,通过ClsUtils中的pair方法进行自动配对
**/
boolean flag = ClsUtils.pair(device.getAddress(), MyAPP.getPinKey());
if (flag) {
try {
//如果成功--->将设备信息从未配对列表清除--->将设备添加到已配对列表
unpairedBluetoothList.remove(device);
unpairedDevicesList.remove(position);
pairedDevicesList.add(device.getName() + "|" + device.getAddress());
unpairedArrayAdapter.clear();
unpairedArrayAdapter.notifyDataSetChanged();
pairedArrayAdapter.notifyDataSetChanged();
} catch (Exception e) {
e.printStackTrace();
}
Toast.makeText(Main.this, "配对成功,请链接", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(Main.this, "配对失败", Toast.LENGTH_SHORT).show();
}
}
}
这里最主要的就是通过ClsUtils类的pari方法将固定的PIN码告知手机的蓝牙适配器。
/**
* 用于配对的函数
*
* @param strAddr
* @param strPsw
* @return
*/
public static boolean pair(String strAddr, String strPsw) {
boolean result = false;
//获取本机的蓝牙适配器
BluetoothAdapter bluetoothAdapter = BluetoothAdapter
.getDefaultAdapter();
bluetoothAdapter.cancelDiscovery();
if (!bluetoothAdapter.isEnabled()) {
bluetoothAdapter.enable();
}
// 检查蓝牙地址是否有效
if (!BluetoothAdapter.checkBluetoothAddress(strAddr)) {
Log.d("mylog", "devAdd un effient!");
}
Log.w("mylog", strAddr);
//根据传入的蓝牙地址获取蓝牙设备
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(strAddr);
//检测蓝牙的连接状态--未连接
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
try {
Log.d("mylog", "NOT BOND_BONDED");
ClsUtils.setPin(device.getClass(), device, strPsw); // 手机和蓝牙采集器配对
ClsUtils.createBond(device.getClass(), device);//创建连接
} catch (Exception e) {
Log.d("mylog", "setPiN failed!");
e.printStackTrace();
}
} else {
Log.d("mylog", "HAS BOND_BONDED");
try {
ClsUtils.createBond(device.getClass(), device);
ClsUtils.setPin(device.getClass(), device, strPsw); // 手机和蓝牙采集器配对
ClsUtils.createBond(device.getClass(), device);
} catch (Exception e) {
Log.d("mylog", "setPiN failed!");
e.printStackTrace();
}
}
if (device != null) {
result = true;
}
return result;
}
/**
* 与设备配对
*/
static public boolean createBond(Class btClass, BluetoothDevice btDevice)
throws Exception {
Method createBondMethod = btClass.getMethod("createBond");
Boolean returnValue = (Boolean) createBondMethod.invoke(btDevice);
return returnValue.booleanValue();
}
以上就是未配对列表的点击事件,通过ClsUtils中的pari方法把一个固定的PIN码进行配对。如果这一步完成之后,则只剩下领完一种,就是已经配对,但是尚未连接的设备。接下来我们处理已经配对,但是尚未连接的设备。
实现一个尚未连接设备列表的item点击Listener:
/**
* 用于处理已配对列表的点击事件
*/
private class PairedListener implements AdapterView.OnItemClickListener {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
String s = pairedArrayAdapter.getItem(position);
final String address = s.substring(s.indexOf("|") + 1).trim();
if (bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.cancelDiscovery();
}
//另一个线程进行连接
connect(address);
}
}
这里调用了connect方法进行连接,
/**
* 请求连接
*/
public void connect(final String address) {
//发送连接的请求
handler.obtainMessage(Constant.CONNECT_REQUEST).sendToTarget();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
boolean isConnected = requestConnect(address);
//将连接结束的结果返回显示
handler.obtainMessage(Constant.CONNECT_FINISHED, isConnected).sendToTarget();
}
});
thread.start();//新开一个线程进行连接操作
}
这里又把蓝牙的地址传给了requestConnect()这个函数,在这里实现真正的蓝牙连接。
/**
* 通过address来获取设备的socket和IO
*
* @param address
* @return
*/
public boolean requestConnect(String address) {
boolean flag = false;
if (!BluetoothAdapter.checkBluetoothAddress(address)) {
Toast.makeText(Main.this, "蓝牙地址无效", Toast.LENGTH_SHORT).show();
} else {
//通过address来获取到远程的蓝牙设备
BluetoothDevice btd = bluetoothAdapter.getRemoteDevice(address);
//判断是否获取到设备
if (btd == null) {
Log.e("设备状况", "设备为空");
} else {
Log.e("设备状况", "设备不为空");
}
//判断socket是否为空
if (clientSocket == null) {
try {
clientSocket = btd.createRfcommSocketToServiceRecord(MyAPP.MY_UUID);
Log.e(Constant.LOGTAG, "\tsocket是否为空" + (clientSocket == null));
} catch (IOException e) {
e.printStackTrace();
Log.i("clientSocket", "NULL");
handler.obtainMessage(Constant.CONNECT_FINISHED, false);
}
}
//若socket未连接,进行连接
if (!clientSocket.isConnected()) {
Log.w("判断socket是否连接", "\t" + clientSocket.isConnected());
try {
clientSocket.connect();
Log.w("判断socket是否连接", "\t" + clientSocket.isConnected());
} catch (IOException e) {
e.printStackTrace();
//发送错误报告
handler.obtainMessage(Constant.SOCKET_LOST);
flag = false;
btd = null;
clientSocket = null;
Log.e("socket shifou ", (clientSocket == null) + "" + "\tflag\t" + flag);
}
}
if ((clientSocket != null) && (clientSocket.isConnected())) {
try {
//连接成功之后就新开启一个线程用于接收传来的数据。
acceptThread = new AcceptThread(this,clientSocket.getInputStream(),handler,bluetoothAdapter);
acceptThread.start();
flag = true;
} catch (Exception e) {
e.printStackTrace();
}
}
}
MyAPP.saveLastDevice(address);
return flag;
}
新线程中就是根据获得的IO流进行读取即可。因为通讯协议是自定的,所以要完成数据包的封装和校验,当确定是有效的数据包时才会发送给用于显示数据的View进行显示,否则就丢弃这次的数据。
/**
* 处理消息的内部类,服务器线程
*/
public class AcceptThread extends Thread {
private InputStream mInputStream;
private Handler mHandler;
private BluetoothAdapter mBluetoothAdapter;
private Context mContext;
private BluetoothServerSocket mServerSocket;
class Constant{
public static final String LOGTAG = "AcceptThread";
}
public AcceptThread(Context context,InputStream inputStream, final Handler handler,BluetoothAdapter bluetoothAdapter) {
Log.e("create thread", "");
this.mHandler = handler;
this.mInputStream = inputStream;
this.mBluetoothAdapter = bluetoothAdapter;
this.mContext = context;
}
//重写的run方法
public void run() {
Log.e("run", "");
try {
mServerSocket = mBluetoothAdapter
.listenUsingRfcommWithServiceRecord(Main.Constant.NAME, MyAPP.MY_UUID);
if (mServerSocket != null) {
Log.w(Constant.LOGTAG, "serverSocket建立");
MyAPP.connectFlag = true;
} else {
Log.w(Constant.LOGTAG, "serverSocket未建立");
}
} catch (Exception e) {
e.printStackTrace();
}
int[] numData = new int[27];//buffer
int i = 0;//标记在数组中存放的位置
int flag = 0;//用于标记当前读取的状态_0-未找到包头_1-找到包头
Log.e("running", "Thread已启动");
while (MyAPP.connectFlag) {
//用于标记是否发送数据
boolean isPostData = true;
int count = 0;
//getData
if (MyAPP.readFlag) {
try {
/*获取原始数据*/
count = mInputStream.read();
Log.e("读到的数据", String.format("%c", count));
//标记判断
switch (flag) {
//找到包头
case 0:
if (count == DataConst.PACKAGE_BEGIN) {
numData[i] = count;
flag = 1;
i++;
break;
} else {
break;
}
//开始组装数据包
case 1:
if (i < DataConst.PACKAGE_LENGTH) {
numData[i] = count;
i++;
}
if (i == DataConst.PACKAGE_LENGTH) {
if ((numData[25] == DataConst.PACKAGE_END) && (numData[0] == DataConst.PACKAGE_BEGIN)) {
/*数据包的进一步校验------检查数据内的符号是否正确*/
for (int index = 1; index < DataConst.PACKAGE_LENGTH - 2; index++) {
//5,11,17位应该为数据的符号位+或-
if (index == 5 || index == 11 || index == 17) {
if ((numData[index] != DataConst.ADD_SYMBLE) && (numData[index] != DataConst.SUB_SYMBLE)) {
isPostData = false;
mHandler.obtainMessage(Main.Constant.DATA_ERROR);
Log.i("isPostData---1", isPostData + "" + "num");
}
//9,15,17应该为数据的小数点位.
}
if (index == 9 || index == 15 || index == 21) {
if (numData[index] != DataConst.DOT_SYMBLE) {
isPostData = false;
mHandler.obtainMessage(Main.Constant.DATA_ERROR);
Log.i("isPostData---2", isPostData + "");
}
// 剩下的为数据的数字位0-9之间
}
if (((index != 5) && (index != 11) && (index != 17) && (index != 9) && (index != 15) && (index != 21))) {
if ((numData[index] < 48) || (numData[index] > 57)) {
isPostData = false;
mHandler.obtainMessage(Main.Constant.DATA_ERROR);
Log.i("isPostData---3", isPostData + "" + "现在的下标" + index);
}
}
}
//根据检查结果判断是否进行数据的发送
if (isPostData) {
Message msg = new Message();
msg.what = Main.Constant.DATA_CHANGE;
msg.obj = numData;
MyAPP.readFlag = false;
mHandler.sendMessage(msg);
}
}
flag = 0;
i = 0;
}
break;
}
} catch (IOException e) {
Log.e("IOException", "socketrunning");
MyAPP.connectFlag = false;
e.printStackTrace();
//关闭各个输入流和socket
IOUtils.closeIO(mInputStream, mHandler);
IOUtils.closeIO(mServerSocket, mHandler);
if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
mContext.sendBroadcast(new Intent(Main.Constant.BLUETOOTH_STATE_CHANGED));
Log.e("socket错误", "has colse all stream and socket");
}
}
}
Log.i("socket lost", "跳出循环");
}
}
因为之前学长在发送数据的时候是十六进制和ASCII一起使用,所以导致接受到的数据很难进行解析,后来修改了一个通讯协议。即所有的数字,字符都用ASCII来表示,这样就没有了数据解析的困扰。
因为数据接受的太快,所以会发生数据的串扰问题,后来想到可以使用类似信号量的方式,当接受完一组数据,并在进行显示的时候,设置信号量为false,即不再接受数据;当绘制完成时,修改型号量为true,即开始接收数据,这样数据的收发就处于合理的速度中。
为了显示的更加直观,我们考虑使用两种视图配合来体现吊钩当前的状态,一种是吊钩在水平面上的投影,另一种是吊钩和铅垂线方向的夹角。通过这两个视图的配合使用,可以很清晰的知道吊钩是朝那个象限偏移,便于及时调整。最主解决的问题就是坐标转换的问题,因为传感器传来的数据是其自身基于自身坐标轴进行旋转的角度,而我们需要先把这个旋转的角度转化为传感器当前以大地为参考系下的平面坐标,把这个平面坐标又要转换为以吊车司机为原点的坐标,最后要把这个坐标转换为手机屏幕上显示的坐标,这样中间一共经历了三次的坐标变换。
因为这是个实际使用的项目,虽然在技术的实现上没有很大的困难,但是更多的是在设计细节上的考虑。比如要考虑到使用人员的误退出操作,以及误操作之后再次进入app如何解决,同时也要在app中放置关于应用的使用说明,便于进行查看,以及当设备连接上之后就不能再进行初始化等操作。还有就是语音播报的频率问题,以及当设备倾角大于某一个值时要进行语音报警提示,这些都是在不断的做的过程中慢慢发现的一些细节方面的需求。
标签:mis imp 图形化显示 enable unp hid discover short 无法
原文地址:http://blog.csdn.net/wqc_csdn/article/details/52915187