无线测量APP开发总结
Posted 兜里有糖心里不慌
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了无线测量APP开发总结相关的知识,希望对你有一定的参考价值。
简介:
博主小白一枚,这个app只是大三时候和一位学长一起做了这个用于测量吊车倾角的app,硬件上是有两个姿态传感器,将姿态传感器的数据通过总机接收汇总之后,通过总机和手机之间的蓝牙连接,将数据上传到手机上,在手机上实时的绘制出当前的吊钩倾角状态。现在做一下总结也算是对这次经历的反思和改进,第一次做一个实际的项目,所以比较混乱,欢迎各位看后提出改进意见。
界面很简单,只有一个设置界面和一个进行数据显示的界面
设置界面主要是查找设备,进行初始化,进行系统清除,控制语音播报,显示配对设备和搜寻到的设备
视图界面主要是将接收到的数据进行图形化显示。上边的坐标视图显示的是吊车吊钩在平面坐标系下的投影(Y轴正方向是吊车司机面向的方向,X轴正方向是吊车司机自然抬起右手的方向),下边的视图是吊车吊钩与铅锤线的夹角,最下边的数据面板显示的是当前的数据实时情况,包括两台姿态传感器分别X,Y轴的角度,和铅垂线的角度,以及设备的电量
工作流程:
首先理一下粗略的工作流程:
这是整个系统的工作流程,其实逻辑并不复杂。
首先是进入APP搜索所有的蓝牙设备,并选择下位总机的蓝牙模块进行连接。这里有两种状况:
- 首次连接,之前并没有连接,先要输入PIN码进行连接
- 之前已经配对完成,直接连接
连接成功之后开始接收数据 ,这时候需要先进行数据的校正(也就是把当前接受到的数据作为初始值校零),并将用于校零的数据保存下来(用于app意外退出,再次进入时恢复初识状态)。
完成初始化校正之后就开始了数据的传输和实时显示,由于是测量吊车吊钩倾角,所以使用的是SurfaceView来实时模拟吊钩运动状态的,同时在屏幕最下边有当前已经校准过的精确数据。
遇到的问题:
- 首先是手机蓝牙和下位机蓝牙模块的连接,因为下位机蓝牙模块的配对PIN码是固定写好的,无法动态的输入,而手机每次产生PIN码需要手动输入连接,所以这是遇到的第一个问题。
- 其次是自定义通信格式的问题。在连接上之后就和学长开始测试接收和发送的数据是否正确。在商定通讯协议时候发生了数据总是发送/接受错误的问题,下位机发送一个无符号的16位整数,在手机端接受的时候就变成了另一个数。
- 然后遇到了数据更新太快,以至于界面来不及显示的问题。在确定了数据的通讯 格式之后测试数据的收发,因为下位机的数据传输速度很快,最开始的时候只是简单的将数据封装之后通过handler发送出去,然后UI线程进行处理。但是因为数据量特别大,所以会发生数据串扰问题,当一个数据正在往屏幕上显示的时候,下一组数据已经覆盖了上一组数据,导致有时候会有数据串扰。
- 第四个遇到就是如何将接受到的数据转化为屏幕上要显示的图像问题。
- 第五点,也是做这个项目感受最深的一点,就是开发中遇到的各种设计上的细节问题。比如说运行过程中的误退出问题,以及误退出之后如何保证再次进入app时数据能和之前保持一致(因为设备已经开始运行,无法再次校正初始数据),如何保证保存的运行数据的安全性问题。这些细节的考虑是我做这个app最大的收获。
遇到的问题详解:
1. 首先是下位机和手机蓝牙的连接。因为下位机总机的蓝牙模块中用于配对的PIN码是固定的,而手机蓝牙连接的时候每次都是动态的输入PIN码,所以两者怎么连接是一个问题。
解决办法:
- 首先尝试的方式通过反射机制,在每次配对请求的时候进行拦截,然后写入下位机蓝牙的PIN码。这种方式确实可以进行连接,但是却比较不稳定,有时候连接的容易断开或者是程序挂掉(应该是我比较水的原因。。。
~~(>_<)~~) - 第二种方式是在尚未配对的时候请求配对,手机会自动的弹出一个输入PIN码的输入框,输入下位机的蓝牙PIN码,完成配对,但是需要每次都输入PIN码,而且PIN码很容易被泄露,从而伪造运行数据(由于测量的是吊车吊钩倾角,所以所有的数据都会会存储记录,便于追责)。
第一种方式:
首先定义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", "跳出循环");
}
}
2. 其次是自定义通信格式的问题。
因为之前学长在发送数据的时候是十六进制和ASCII一起使用,所以导致接受到的数据很难进行解析,后来修改了一个通讯协议。即所有的数字,字符都用ASCII来表示,这样就没有了数据解析的困扰。
3. 然后遇到了数据更新太快,数据发生错乱的问题。
因为数据接受的太快,所以会发生数据的串扰问题,后来想到可以使用类似信号量的方式,当接受完一组数据,并在进行显示的时候,设置信号量为false,即不再接受数据;当绘制完成时,修改型号量为true,即开始接收数据,这样数据的收发就处于合理的速度中。
4. 然后解决接收到的数据如何转化在屏幕上显示,以何种方式呈现出来。
为了显示的更加直观,我们考虑使用两种视图配合来体现吊钩当前的状态,一种是吊钩在水平面上的投影,另一种是吊钩和铅垂线方向的夹角。通过这两个视图的配合使用,可以很清晰的知道吊钩是朝那个象限偏移,便于及时调整。最主解决的问题就是坐标转换的问题,因为传感器传来的数据是其自身基于自身坐标轴进行旋转的角度,而我们需要先把这个旋转的角度转化为传感器当前以大地为参考系下的平面坐标,把这个平面坐标又要转换为以吊车司机为原点的坐标,最后要把这个坐标转换为手机屏幕上显示的坐标,这样中间一共经历了三次的坐标变换。
5. 最后一点,也是做这个项目感受最深的一点,就是工程思想在实际项目中的应用。
因为这是个实际使用的项目,虽然在技术的实现上没有很大的困难,但是更多的是在设计细节上的考虑。比如要考虑到使用人员的误退出操作,以及误操作之后再次进入app如何解决,同时也要在app中放置关于应用的使用说明,便于进行查看,以及当设备连接上之后就不能再进行初始化等操作。还有就是语音播报的频率问题,以及当设备倾角大于某一个值时要进行语音报警提示,这些都是在不断的做的过程中慢慢发现的一些细节方面的需求。
以上是关于无线测量APP开发总结的主要内容,如果未能解决你的问题,请参考以下文章
GPS拓展无线同步模块GSYN1000系列在广域同步测量的应用方案