简介:

博主小白一枚,这个app只是大三时候和一位学长一起做了这个用于测量吊车倾角的app,硬件上是有两个姿态传感器,将姿态传感器的数据通过总机接收汇总之后,通过总机和手机之间的蓝牙连接,将数据上传到手机上,在手机上实时的绘制出当前的吊钩倾角状态。现在做一下总结也算是对这次经历的反思和改进,第一次做一个实际的项目,所以比较混乱,欢迎各位看后提出改进意见。

界面很简单,只有一个设置界面和一个进行数据显示的界面

设置界面主要是查找设备,进行初始化,进行系统清除,控制语音播报,显示配对设备和搜寻到的设备

视图界面主要是将接收到的数据进行图形化显示。上边的坐标视图显示的是吊车吊钩在平面坐标系下的投影(Y轴正方向是吊车司机面向的方向,X轴正方向是吊车司机自然抬起右手的方向),下边的视图是吊车吊钩与铅锤线的夹角,最下边的数据面板显示的是当前的数据实时情况,包括两台姿态传感器分别X,Y轴的角度,和铅垂线的角度,以及设备的电量

工作流程:

首先理一下粗略的工作流程:


这是整个系统的工作流程,其实逻辑并不复杂。

  1. 首先是进入APP搜索所有的蓝牙设备,并选择下位总机的蓝牙模块进行连接。这里有两种状况:

    1. 首次连接,之前并没有连接,先要输入PIN码进行连接
    2. 之前已经配对完成,直接连接
  2. 连接成功之后开始接收数据 ,这时候需要先进行数据的校正(也就是把当前接受到的数据作为初始值校零),并将用于校零的数据保存下来(用于app意外退出,再次进入时恢复初识状态)。

  3. 完成初始化校正之后就开始了数据的传输和实时显示,由于是测量吊车吊钩倾角,所以使用的是SurfaceView来实时模拟吊钩运动状态的,同时在屏幕最下边有当前已经校准过的精确数据。

遇到的问题:

  1. 首先是手机蓝牙和下位机蓝牙模块的连接,因为下位机蓝牙模块的配对PIN码是固定写好的,无法动态的输入,而手机每次产生PIN码需要手动输入连接,所以这是遇到的第一个问题。
  2. 其次是自定义通信格式的问题。在连接上之后就和学长开始测试接收和发送的数据是否正确。在商定通讯协议时候发生了数据总是发送/接受错误的问题,下位机发送一个无符号的16位整数,在手机端接受的时候就变成了另一个数。
  3. 然后遇到了数据更新太快,以至于界面来不及显示的问题。在确定了数据的通讯 格式之后测试数据的收发,因为下位机的数据传输速度很快,最开始的时候只是简单的将数据封装之后通过handler发送出去,然后UI线程进行处理。但是因为数据量特别大,所以会发生数据串扰问题,当一个数据正在往屏幕上显示的时候,下一组数据已经覆盖了上一组数据,导致有时候会有数据串扰。
  4. 第四个遇到就是如何将接受到的数据转化为屏幕上要显示的图像问题。
  5. 第五点,也是做这个项目感受最深的一点,就是开发中遇到的各种设计上的细节问题。比如说运行过程中的误退出问题,以及误退出之后如何保证再次进入app时数据能和之前保持一致(因为设备已经开始运行,无法再次校正初始数据),如何保证保存的运行数据的安全性问题。这些细节的考虑是我做这个app最大的收获。

遇到的问题详解:

1. 首先是下位机和手机蓝牙的连接。因为下位机总机的蓝牙模块中用于配对的PIN码是固定的,而手机蓝牙连接的时候每次都是动态的输入PIN码,所以两者怎么连接是一个问题。

解决办法:
  1. 首先尝试的方式通过反射机制,在每次配对请求的时候进行拦截,然后写入下位机蓝牙的PIN码。这种方式确实可以进行连接,但是却比较不稳定,有时候连接的容易断开或者是程序挂掉(应该是我比较水的原因。。。 ~~(>_<)~~)
  2. 第二种方式是在尚未配对的时候请求配对,手机会自动的弹出一个输入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 {@Overridepublic 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 {@Overridepublic 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() {@Overridepublic 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];//bufferint i = 0;//标记在数组中存放的位置int flag = 0;//用于标记当前读取的状态_0-未找到包头_1-找到包头Log.e("running", "Thread已启动");while (MyAPP.connectFlag) {//用于标记是否发送数据boolean isPostData = true;int count = 0;//getDataif (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();//关闭各个输入流和socketIOUtils.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开发总结相关推荐

  1. Flutter App开发蓝牙协议

    Flutter App开发蓝牙协议 Summary BLE低功耗蓝牙,是我们常说的蓝牙4.0, 该技术有极低的运行待机功耗,本文记录使用Flutter开发安卓App的过程,使用蓝牙模块的配置和一些细节 ...

  2. 阿里宣布Atlas正式开源:带你重返App开发的田园时代

    继Weex之后,阿里在移动技术领域又有开源大动作. 3月13日,手机淘宝安卓客户端容器化框架Atlas正式宣布开源( https://github.com/alibaba/atlas ).Atlas由 ...

  3. 适合前端工作者的iPhone Web App开发

    iPhone有着丰富的软件资源,到目前为止,仅在appStore上架的软件就达十多万个,而相比之下,有着10年历史的WM系统却不过只有大约2万个应用程序. 随着ipad和iphone 4的发布,iph ...

  4. 2021爱智先行者—(2)零基础APP开发实例

    [本文正在参与"2021爱智先行者-征文大赛"活动],活动链接:https://bbs.csdn.net/topics/602601454 欢迎关注 『Python小白的项目实战』 ...

  5. jquery实现app开发闹钟功能_一款让你真正摆脱懒觉的“闹钟APP软件”

    对生活在繁忙城市的白领工作者们来说,朝九晚五是平常的事情.相信很多的上班族们在工作日里最纠结的事情就是早上起床了,虽然上班族们几乎都设置了起床闹铃的习惯,但是这些普通的闹钟真的能让我们准时起床吗?今天 ...

  6. 一款app 开发在线工具:app inventor

    一:App Inventor简介 app inventor是由Google公司开发的一款在线开放的Android编程工具软件,通过图形化积木式的拖放组件完成app开发,2012年1月移交麻省理工学院M ...

  7. 蓝牙运动手环app开发方案

    所谓智能蓝牙手环app软件开发,  就是内置蓝牙操作系统.通过连接网络来实现多种功能的手环产品,蓝牙手环一般能同步手机中的电话.短信.邮件.照片.音乐等相关数据.其实早在1982年,日本精工就通过其收 ...

  8. 共享储物柜app开发方案

    智能储物柜应用背景 在公众场所提供储物柜已经成为当前不可或缺的一种工具,在商场.超市,?储物柜的使用随处可见,现有技术中储物柜用户存取物品一般都需要物理凭证,如条码.IC卡.磁卡.条码卡等.其中条码打 ...

  9. 安卓app开发工具_四川智慧社区安卓手机app开发多少钱

    四川智慧社区安卓手机app开发多少钱 注册登录应用公园后,有两种APP制作模式: 1.主题模式: 应用公园平台提供了上百个配置好的APP模板,可以直接使用,把图片文字替换就可以直接使用.如下图所示: ...

最新文章

  1. sort -k选项详解
  2. Redis-Predis 扩展
  3. python scrapy框架爬虫_Scrapy爬虫框架教程(一)-- Scrapy入门
  4. R-apply()函数
  5. c++矩阵连乘的动态规划算法并输出_算法交流: 7215 简单的整数划分问题 【2.7基本算法之算法效率】...
  6. C 语言实例 - 判断闰年
  7. 网络口协商_以太网端口协商原理
  8. iPhone 13 Pro手机壳曝光 网友:更丑了
  9. 操作 Wave 文件(3): 接触 mmio 系列函数
  10. saltstack实战2--远程执行之模块(Modules)
  11. vue底部跳转_Vue中底部tabBar切换及跳转
  12. linux CP命令覆盖不提示方法
  13. 原 SpringFramework核心技术五:Spring AOP API
  14. Mac OS使用技巧之四:修改打开不同格式视频的默认播放器
  15. Flex Builder教程
  16. 通过mac地址查找ip
  17. redis实现分布式锁的几种方式
  18. 使用sikuli测试web网页实例
  19. 类型数组HTML5 中的新数组
  20. 磁盘黑色未分配区域恢复成绿色逻辑分区

热门文章

  1. 2、mysql基本操作中
  2. 在未提供官方驱动的Windows平板上安装Win10且完美驱动的解决方案
  3. python文本编辑器怎么运行_python程序编辑和运行的几种方法
  4. 计算机二级c语言预测,计算机二级C语言考前预测上机试题及解析
  5. linux怎么共享打印机驱动程序,为samba共享的打印机添加Windows驱动
  6. 云宏Ceph分布式存储高性能设计
  7. Autcad 2020,2019 一键安装64位破解版
  8. 手把手教你实现一个抽奖系统(Java版)
  9. C#图像处理:在图片上写字,文字位置居中问题
  10. 网络工程——软科中国大学专业排名