功能设计
开箱检测与智能监测
开箱提醒与开箱记录
每一次打开箱子,都会被记录,用户可以通过配套的APP知道自己的箱子什么时候被打开或关闭过,同时也可以得知当前箱子的开闭状态。另外可以通过简单设置,当箱子被打开时,配套的APP发送通知给用户。
手机端弹框界面:
环境较暗自动补光
当箱子处于打开状态,如果周围环境较暗,箱子的内置照明灯会亮起并照亮箱子内部。即使用户不得不在户外低照明的情况下开箱子,也不用一手提着手电一手找物品。另外,用户也可以手动打开照明灯进行照明。
开箱自动拍照记录开箱人与取放物品
开启箱子后,摄像头会自动开启并录像,拍摄的画面最终可以在APP中查看。通过把摄像头放置在合适的位置,使得正常开箱后能捕获到人脸。这样,用户不仅可以知道开箱时间,还能知道开箱人的脸部信息,以及取出了什么物品。
手机端界面:
旅行箱状态检测与分析
温湿度检测
系统能够把箱子的温湿度数据实时发送到用户APP上。如果用户在箱子中放置了如甜品,特殊药品等对温度湿度敏感的物品,则希望旅行箱可以提供相应信息,以便在条件不合适的时候采取其他办法。
用户可以自主设置温湿度阈值,app也会提供推荐阈值。当箱子内环境超出阈值时可以通过app提醒用户。在现实生活中,部分用户可能会把正常充电的设备放置在密封的旅行箱内导致旅行箱温度异常升高。APP针对这种情况设置异常阈值。提醒用户检查箱内安全。
手机端界面与警告弹框:
手机端界面 |
警告弹框 |
|
|
箱子放置形状检测
系统能够把箱子的放置形状数据实时送到用户APP。正常情况下,放置形状几乎是不变的,但如果有人有意或者无意的摆动我们的箱子,APP会作出相应提醒。比如有人把我们的箱子撞翻了,或者有人挪动我们的箱子,用户都可以知道。考虑到有些物品是不能剧烈抖动的,用户还可以实时查看箱子的放置状态检测箱子是否发生倒立、侧翻、抖动等。
手机端界面:
位置与安全
定位服务与导航
用户可以通过APP了解用户当前位置和箱子的实时位置。即使意外丢失或者被盗,也能提高找回的可能性,定位误差小于50m。
在APP上,亦可根据导航提供的较为快捷的路线,较短时间内寻找到箱子。
手机端界面:
箱子远离提醒
用户可以通过APP开启报警功能。当箱子离开用户可视范围时,箱子通过蜂鸣器警报,APP也会提醒用户。
手机端弹框界面:(远离时显示异常,再次靠近时显示正常)
声音寻箱
用户可以在手机上打开箱子的蜂鸣器。利用声音,用户可以快速定位箱子的位置。应用场景有:机场托运取行李,大巴行李仓取箱子,一时间找不到行李箱等。
消息显示
用户可以通过APP向箱子发送文字信息。在箱子意外落下时,可以把联系方式等推送到箱子外的显示屏上。如果有人发现,能够联系到主人。
手机端设置界面:(便于用户异步控制箱子)
APP端实现
项目中负责的部分是Android端的APP开发,开发的主要逻辑是从OneNet云平台获取数据在手机端进行处理和展示,或是从手机端的APP进行命令的下达,通过OneNet平台的命令下发到旅行箱上的万耦开发板,以此实现对于开发板一端的控制。
OnetNET平台交互
向OneNet查询设备信息
以查询设备信息为例,代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
public String getDevice(String deviceId,String apiKey) { String result=""; BufferedReader in =null; try { String url = "http://api.heclouds.com/devices/"+deviceId; URL realUrl = new URL(url); HttpURLConnection connection=(HttpURLConnection)realUrl.openConnection(); connection.setRequestMethod("GET"); connection.setRequestProperty("Authorization",apiKey); connection.connect(); in = new BufferedReader(new InputStreamReader( connection.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { System.out.println("查询设备信息出现异常! "); } finally { try { if (in != null) { in.close(); } } catch (Exception e2) { System.out.println("IO close error !"); } } return result; }
|
查找设备数据点
这部分是APP端做开发板端数据展示的基础,例如要更新APP首页的温度等信息时要调用到refresh_tempreture(),而该方法中对于数据点的查询用到的就是下面的这个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
public String getDataPoints(String deviceId,String apiKey,String datastreamId,String start,int limit) { String result=""; BufferedReader in =null; try { String url = "http://api.heclouds.com/devices/"+deviceId+"/datapoints?datastream_id="+datastreamId+"&limit="+limit; URL realUrl = new URL(url); HttpURLConnection connection=(HttpURLConnection)realUrl.openConnection(); connection.setRequestMethod("GET"); connection.setRequestProperty("Authorization",apiKey); connection.connect(); in = new BufferedReader(new InputStreamReader( connection.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line; } } catch (Exception e) { e.printStackTrace(); System.out.println("获取数据流出现异常! "); } finally { try { if (in != null) { in.close(); } } catch (Exception e2) { System.out.println("IO close error !"); } } return result; }
|
命令下达
命令下达的部分主要是对于开发板的灯、蜂鸣器、显示屏上文字的展示等部分通过命令下达以进行控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
|
public String postCommand(String deviceId,String apiKey,int timeout,String body,int flag,int en) { String result=""; BufferedReader in =null; DataOutputStream out=null; try { String url = "http://api.heclouds.com/v1/synccmds?device_id="+deviceId+"&timeout="+timeout; URL realUrl = new URL(url); HttpURLConnection connection=(HttpURLConnection)realUrl.openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Authorization",apiKey); connection.setDoInput(true); connection.setDoOutput(true); connection.connect(); out=new DataOutputStream(connection.getOutputStream()); switch (flag){ case 1: byte[] data=body.getBytes(); out.writeByte(1); out.writeByte(1); out.writeByte(1); out.write(data); out.writeByte(0); break; case 2: out.writeByte(2); if(en==1) out.writeByte(1); else out.writeByte(0); out.writeByte(0); break; case 3: out.writeByte(3); out.writeByte(0); out.writeByte(0); break; case 4: out.writeByte(11); if(en==1) out.writeByte(1); else out.writeByte(0); out.writeByte(0); break; case 5: out.writeByte(5); if(en==1) out.writeByte(1); else out.writeByte(0); out.writeByte(0); break; case 6: out.writeByte(6); if(en==1) out.writeByte(1); else out.writeByte(0); out.writeByte(0); break; case 7: out.writeByte(7); if(en==1) out.writeByte(1); else out.writeByte(0); out.writeByte(0); break; case 8: out.writeByte(8); if(en==1) out.writeByte(1); else out.writeByte(0); out.writeByte(0); break; case 10: out.writeByte(10); if(en==1) out.writeByte(1); else out.writeByte(0); out.writeByte(0); break; default: throw new IllegalStateException("Unexpected value: " + flag); }
in = new BufferedReader(new InputStreamReader( connection.getInputStream())); String line; while ((line = in.readLine()) != null) { result += line;} System.out.println(result); } catch (Exception e) { e.printStackTrace(); System.out.println("命令下发异常! "); }
finally { try { if (in != null) { in.close(); } } catch (Exception e2) { e2.printStackTrace(); System.out.println("IO close error !"); } } System.out.println(result); return result; }
|
文件获取
在APP端中文件获取方法唯一的作用就是获取开箱时候的图片。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
|
public byte[] getFile(String apiKey,int box_id,int zhen) { BufferedReader in =null; byte []datas=null; try { String url = "http://api.heclouds.com"+"/bindata"+"/"+index_front+box_id+"-"+zhen+".jpg"; URL realUrl = new URL(url); HttpURLConnection connection=(HttpURLConnection)realUrl.openConnection(); connection.setRequestMethod("GET"); connection.setRequestProperty("Authorization",apiKey); connection.connect(); InputStream is = connection.getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] flush = new byte[1024]; int len = -1; while ((len = is.read(flush)) != -1) { baos.write(flush, 0, len); } baos.flush(); datas = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); System.out.println("获取文件出现异常! "); } finally { try { if (in != null) { in.close(); } } catch (Exception e2) { System.out.println("IO close error !"); } } return datas; }
|
数据更新
以APP首页的温度、湿度、光强,以及箱子的开闭,开发板的开关情况为例,这些都是在APP端的首页进行了信息展示,对于这些信息的数据更新在Android端是不能直接在Activity里生成一个Thread来进行实时更新的,那样会闪退,所以需要对于一个页面建立Handler类,在该类里面进行数据更新。
线程中的run()方法
该部分的逻辑是不断地发送Message给Handler进行处理,case为0时是对于开发板的六轴的数据进行获取,在case为1~3时是对于温度、湿度和光强进行更新,在case为4时则是对于箱子的开关状态进行监控。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| @Override public void run() { while (true){ Message message0 = new Message(); Message message1 = new Message(); Message message2 = new Message(); Message message3 = new Message(); Message message4 = new Message(); for(int i=0;i<=4;i++){ switch (i){ case 0: message0.what = 0; handler.sendMessage(message0); break; case 1: if(temperature_en.en==1) { message1.what=1; handler.sendMessage(message1); } break; case 2: if(guang_en.en==1) { message2.what=2; handler.sendMessage(message2); } break; case 3: if(shidu_en.en==1) { message3.what=3; handler.sendMessage(message3); } break; case 4: message4.what = 4; handler.sendMessage(message4); break; default: throw new IllegalStateException("Unexpected value: " + i); } } try { Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } } }
|
MyHandler中的处理
这里主要注意到的是case为4时对于开闭状态的更改并不是直接进行更改,因为在实际测试中发现从开发板到OneNET平台的红外检测数据会有波动,这可能是红外接收和发射的装置没有完全对准好导致的结果,这会让APP端的状态更改也发生波动,所以在APP端进行了处理,在数据稳定不变几秒之后才进行箱子开闭状态的更新,防止手机端的频繁弹窗提醒。
其他的部分就和上述差不多,case为0时是对于开发板的六轴的数据进行获取,在case为1~3时是对于温度、湿度和光强进行更新。
handleMessage方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
| @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); switch(msg.what){ case 0: String str = api.getDevice(deviceId,apiKey); boolean temponline = judgeOnline(str); if (temponline != online && !unstable_online) { utime_online_1 = System.currentTimeMillis(); unstable_online = true; } if (unstable_online) utime_online_2 = System.currentTimeMillis(); if (unstable_online && temponline == online) unstable_online = false; if (temponline != online && unstable_online && utime_online_2-utime_online_1 >= 1000) { onlineDialog(temponline); onlineNotification(temponline); String tempstr1="设备: <font color='#FF0000'><small>正常</small></font>"; String tempstr2="设备: <font color='#FF0000'><small>异常</small></font>"; if (temponline == true) t4.setText(Html.fromHtml(tempstr1)); else t4.setText(Html.fromHtml(tempstr2)); online = temponline; unstable_online = false; }
api.refresh_acce(deviceId,apiKey); acce_x = -api.acce_x; acce_y = api.acce_y; acce_z = api.acce_z; new_degree = (float)(Math.atan2((double)acce_x, (double)acce_z) * 180 / Math.PI); if(acce_y>700 && acce_y<1200) new_flag = 1; else if(acce_y>300 && acce_y<=700) new_flag = 2; else if(acce_y>-1200 && acce_y<-800) new_flag = 3; else if(acce_y>=-800 && acce_y<-400) new_flag = 4; else new_flag = 0; if(unstable_location) { utime_location_2 = System.currentTimeMillis(); if(utime_location_2 - utime_location_1 >=3000){ unstable_location = false; } } else { boolean same = true; if(new_flag != old_flag) same = false; if(!same){ AlertDialog dialog = new AlertDialog.Builder(ma) .setTitle("箱子状态") .setMessage("箱子状态发生改变,请前往查看") .setIcon(R.drawable.box_2) .create(); dialog.show(); } utime_location_1 = System.currentTimeMillis(); old_degree = new_degree; old_flag = new_flag; unstable_location = true; } break; case 1: api.refresh_temperature(deviceId,apiKey); t1.setText("温度: "+api.temperature_zheng+"."+api.temperature_xiao); showDialog(1,api.temperature_zheng,tem_max,tem_min); break; case 2: api.refresh_guang(deviceId,apiKey); t2.setText("光强: "+api.guang_zheng+"."+api.guang_xiao); break; case 3: api.refresh_shidu(deviceId,apiKey); t3.setText("湿度: "+api.shi_du_zheng+"."+api.shi_du_xiao); showDialog(3,api.shi_du_zheng,shidu_max,shidu_min); break; case 4: api.refresh_isopen(deviceId,apiKey); int tempisopen = api.is_open; if (tempisopen != is_open && !unstable_open) { utime_open_1 = System.currentTimeMillis(); unstable_open = true; } if (unstable_open) utime_open_2 = System.currentTimeMillis(); if (unstable_open && tempisopen == is_open) { unstable_open = false; } if (tempisopen != is_open && unstable_open && utime_open_2-utime_open_1 >= 1000) { openDialog(tempisopen); openNotification(tempisopen); String tempstr1="箱子: <font color='#FF0000'><small>打开</small></font>"; String tempstr2="箱子: <font color='#FF0000'><small>关闭</small></font>"; if (tempisopen == 1) t5.setText(Html.fromHtml(tempstr1)); else t5.setText(Html.fromHtml(tempstr2)); is_open = tempisopen; unstable_open = false; } break; default: throw new IllegalStateException("Unexpected value: " + msg.what); } if (temperature_en.en == 0) t1.setText("温度:"); if (guang_en.en == 0) t2.setText("光强:"); if (shidu_en.en == 0) t3.setText("湿度:"); }
|
开箱视频
开箱时树莓派的摄像头会对开箱者拍下一定数量的照片,而这些照片会上传到OneNET云平台之后,由手机端的文件读取方法读取到APP端进行展示,展示的方法就是每隔几毫秒用drawImage方法来展示图片以达到开箱“视频”的效果。
线程中的run()方法
这里就比较简单了,因为只涉及到一个操作也就是从OneNET平台读数据,所以只要每隔25秒就发送一次Message就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Override public void run() { while(true){ try { Message message = new Message(); message.what = 5; myHandler_camera.sendMessage(message); Thread.sleep(25); }catch (InterruptedException e){ e.printStackTrace(); } } }
|
MyHandler_camera中的处理方法
这里同样简单,因为对于照片的处理是在MyView_camera中进行
1 2 3 4 5 6 7 8 9 10 11
| @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); if (msg.what == 5) { if(box_id.state == 0) myView_camera.invalidate(); } }
|
MyView_camera中的处理
首先是onDraw()方法:
可以看到里面用到了getFile方法来从OneNET云平台获取图片,而获取到的文件事实上是以byte数组形式来让手机端APP接收的,所以需要用一个方法byte2image方法来转换成Bitmap类型(这个类型和电脑java里的BufferedImage很像)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| protected void onDraw(Canvas canvas) { Bitmap image = null; Paint p = new Paint(); image = byte2image(api_edp.getFile(Device.apiKey_Camera, box_id.value, box_id.en)); if (box_id.en >= box_id.sum) { box_id.state = 1; return; } else if (image == null) { box_id.en++; return; } imageView.setImageBitmap(image); box_id.en++; box_id.state = 0; }
|
byte数组转bitmap:
1 2 3 4 5 6 7 8 9 10
| public Bitmap byte2image(byte[] dataImage) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = 2; if (dataImage != null){ Bitmap bitmap = BitmapFactory.decodeByteArray(dataImage,0,dataImage.length,options); return bitmap; } else return null; }
|
命令下达
命令下达在APP端有对于开发板的显示屏信息的更新、蜂鸣器控制、灯光控制等功能,下面进行部分代码的展示。
与前端的部件相连
1 2 3 4 5 6 7 8 9 10 11 12 13
| eText=findViewById(R.id.edittext); b1 = findViewById(R.id.btn1); b3 = findViewById(R.id.btn3); s2 = findViewById(R.id.sw2); s2.setChecked(Data.fengmingqi); s4 = findViewById(R.id.sw4); s4.setChecked(Data.zhaomingdeng); s5 = findViewById(R.id.sw5); s5.setChecked(Data.wendujiance); s6 = findViewById(R.id.sw6); s6.setChecked(Data.shidujiance); s7 = findViewById(R.id.sw7); s7.setChecked(Data.guangqiangjiance); s8 = findViewById(R.id.sw8); s8.setChecked(Data.liuzhoujiance); s10 = findViewById(R.id.sw10); s10.setChecked(Data.wifi_isopen); s11 = findViewById(R.id.sw11); s11.setChecked(Data.lowpower_isopen); s9 =findViewById(R.id.sw9); s9.setChecked(Data.lanya);
|
postCommand方法的调用
这里以更新显示屏上的文字为例:
首先前面eText已经和前端部件edittext进行了绑定,因此在设置界面输入了相关文字(由于开发板只能显示英文所以输入的是英文),而b1和部件btn1进行了绑定,在点击btn1时监听器调用onClick方法,调用了postCommand方法,将eText.getText.toString()传入该方法,从而完成显示屏端的信息更新。
1 2 3 4 5 6 7
| b1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { api.postCommand(deviceId,apiKey,30,eText.getText().toString(),1,1); } });
|
位置更新
位置更新部分使用了高德地图的API来完成相关功能:
线程中的run()方法
线程部分代码依然简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Override public void run() { while(true){ try { Message message=new Message(); message.what=4; handler.sendMessage(message); Thread.sleep(10000); }catch (InterruptedException e){ e.printStackTrace(); } } }
|
MyHandler_location
这里要注意的是,标记点的更新首先需要将之前的标记点marker进行remove操作来移除,否则会出现标记点的重复,首先调用到refresh_location方法将接收到的开发板坐标进行更新,之后将数据转换成double类型,LatLng类是记录坐标点的类,将该类用到高德地图API中AMap的addMarker方法,将刚刚的数据作为参数传入,即可更新开发板在地图上的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); int num = msg.what; if(msg.what==4){ System.out.println("i 'm flag"); if(marker!=null) marker.remove(); api.refresh_location(deviceId,apiKey); latLng = new LatLng(get_location_double(api.wz,api.wx),get_location_double(api.jz,api.jx)); marker = amap.addMarker(new MarkerOptions().position(latLng).title("旅行箱").snippet("旅行箱")); System.out.println("经度为:"+get_location_double(api.jz,api.jx)); System.out.println("纬度为:"+get_location_double(api.wz,api.wx)); } }
|
MainActivity
这部分直接放代码,主要是这个页面的一些初始化操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); supportRequestWindowFeature(Window.FEATURE_NO_TITLE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { MainActivity.setStatusBarColor(this,0xFF6BA821); } setContentView(R.layout.activity_main3); mapview = (MapView) findViewById(R.id.map); mapview.onCreate(savedInstanceState); if(aMap==null) aMap=mapview.getMap(); myhandler_location=new Myhandler_location(api,aMap); refreshThread_location=new RefreshThread_location(myhandler_location); if(ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED){ ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.ACCESS_FINE_LOCATION},200); }else{ Toast.makeText(this,"已开启定位权限",Toast.LENGTH_LONG).show(); } myLocationStyle = new MyLocationStyle(); myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_LOCATION_ROTATE); myLocationStyle.interval(2000); myLocationStyle.myLocationType(MyLocationStyle.LOCATION_TYPE_MAP_ROTATE); aMap.setMyLocationStyle(myLocationStyle); aMap.getUiSettings().setMyLocationButtonEnabled(true); aMap.setMyLocationEnabled(true); refreshThread_location.start(); }
|