【车联网原型系统|五】前后端分离
物联网原型系统导航
【车联网原型系统|一】项目介绍+需求分析+概要设计 https://blog.csdn.net/weixin_46291251/article/details/125807297
【车联网原型系统|二】数据库+应用层协议设计 https://blog.csdn.net/weixin_46291251/article/details/125808107
【车联网原型系统|三】树莓派设计+模拟基站程序 https://blog.csdn.net/weixin_46291251/article/details/125808229
【车联网原型系统|四】adhoc组网+frp内网穿透 https://blog.csdn.net/weixin_46291251/article/details/125808621
【车联网原型系统|五】前后端分离 https://blog.csdn.net/weixin_46291251/article/details/125808674
【车联网原型系统|六】效果展示 https://blog.csdn.net/weixin_46291251/article/details/125808845
【源码下载】 https://download.csdn.net/download/weixin_46291251/86227197
文章目录
【车联网原型系统|五】前后端分离
前端
![img](https://imgbed.cheney.cc/picgo/wps1.jpg)
如图,前端共包含四个模块:
-
控制小车模块用于将控制小车移动的启停、控制传感器工作的启停、控制小车的移动等操作的信息参数发送给后端;
-
查询数据模块用于向后端查找满足指定条件的数据;
-
生成轨迹模块用于根据控制信息生成预计轨迹模块和根据实际信息生成实际轨迹模块;
-
展示数据模块用于展示传感器监视的所有信息。
侧面导航栏
导航栏界面用于给用户提供页面导航功能。通过使用el-menu标签实现,每一个标签都是一个el-menu-item。主要代码如下:
1.<el-menu-item index="/carMove">
2. <template slot="title">
3. <i class="el-icon-view"></i>
4. <span slot="title">小车运动</span>
5. </template>
6.</el-menu-item>
7.<el-submenu index="">
8. <template slot="title">
9. <i class="el-icon-menu"></i>
10. <span slot="title">传感器数据</span>
11. </template>
12. <el-menu-item-group>
13. <el-menu-item index="temperature">温度</el-menu-item>
14. <el-menu-item index="humidity">湿度</el-menu-item>
15. <el-menu-item index="illumination">光照</el-menu-item>
16. </el-menu-item-group>
17.</el-submenu>
效果:
![image-20220715160110036](https://imgbed.cheney.cc/picgo/image-20220715160110036.png)
头部导航栏
头部导航栏用于定位页面。通过el-breadcrumb标签实现。主要代码如下:
1.< el-breadcrumb separator = "/" style = "display: inline-block; margin-left: 10px" >
2. < el-breadcrumb-item :to = "'/'" >首页</ el-breadcrumb-item >
3. < el-breadcrumb-item > {{ currentPathName }} </ el-breadcrumb-item >
4.</ el-breadcrumb >
currentPathName为当前所在页面的名称。效果如下图:
![image-20220715160249801](https://imgbed.cheney.cc/picgo/image-20220715160249801.png)
![image-20220715160309223](https://imgbed.cheney.cc/picgo/image-20220715160309223.png)
运动界面
在前端页面中,可以通过选择器选择控制的小车。以小车1为例,当选择控制小车1时,在前端页面中按键WASD可以控制小车的上、下、左、右运动。当小车运动时,前端页面会实时显示小车的轨迹。
因此,前端程序中需要保存用户当前选择控制的小车,当用户控制小车运动时,调用相应的接口,发送运动元组。例如,当前进时,运动元组为[True,False,False,False]。此外,前端还需要定时调用相应的接口,获取小车的最近运动轨迹;在本系统中,我们设置为3秒调用一次接口,实时获取小车1和小车2的最新运动数据,运动数据为具体的坐标值。
其中,控制小车运动的中使用了el-button标签,绘制小车轨迹使用了canvas标签,用于作图。
效果图如下:
![image-20220715160348226](https://imgbed.cheney.cc/picgo/image-20220715160348226.png)
传感器界面
传感器数据也需实时显示,因此,也需要定时调用接口获取数据。在本系统中,我们设置为4秒调用一次接口,实时获取小车1和小车2的最新传感器,传感器数据包括数值和提供数据的小车的IP地址。
其中,使用了el-table标签进行数据的具体显示,使用了el-pagination标签完成了分页查询,默认情况下,分页查询显示第一页,每页显示10条数据。
![image-20220715160406292](https://imgbed.cheney.cc/picgo/image-20220715160406292.png)
后端
![img](https://imgbed.cheney.cc/picgo/wps2.jpg)
如图,后端共包含三个模块:
-
数据库模块用于对数据库进行增、删、改、查操作;
-
前端接口模块用于提供所有前端需要的接口;
-
小车通信模块用于与小车进行通信,交换数据报文。
我们采用Django框架完成后端系统的设计。在基于Django的后端程序启动时,需要同时开启一个线程,启动小车基站。
数据库模块
后端需要操作所有数据表,包括增、删、改、查等基础功能,因此,我们使用Django中自带的库,大大简化了人工编写sql语句的过程。
本系统产生的所有数据都存储在云端的数据库中.
在本系统中,主要需要对两个数据表进行操作,分别是小车运动轨迹表和传感器数据表。首先,在Django的models层定义了这两个数据表,代码如下:
class datas(models.Model):
id = models.IntegerField('id', primary_key=True)
data = models.FloatField('data', null=False)
time = models.CharField('time', null=False , max_length=255)
pi = models.CharField('pi', null=False, max_length=255)
class Meta:
db_table = 'datas'
class trace(models.Model):
id = models.IntegerField('id', primary_key=True)
x = models.IntegerField('x', null=False)
y = models.IntegerField('y', null=False)
pi = models.CharField('pi', null=False, max_length=255)
class Meta:
db_table = 'trace'
需要对这两个数据表进行具体操作的条件有以下 个:
1、小车运动时向基站发送实时运动数据;
2、小车向基站实时发送传感器监测到的数据;
3、前端定时调用接口获取小车的实时运动数据;
4、前端定时调用接口获取小车传感器实时监测的数据。
1、2为对数据表的增操作,3、4为对数据表的查操作,主要代码如下:
t = trace(x = int(x), y = int(y), pi = sock[0])
t.save()
data = datas(data=float(data_list[0]),time = stime.strftime("%Y-%m-%d %X"),pi=sock[0])
data.save()
data_list = datas.objects.all()
paginator = Paginator(data_list, pageSize)
s = 'select * from trace where pi = \'' + carIP + '\'' + 'and id > ' + max_id
trace_map = {'id':'id','x':'x','y':'y','pi':'pi'}
trace_all = trace.objects.raw(s,translations=trace_map)
前端接口模块
前端代码通过url和参数的方式访问后端接口。在本系统中,共有4个接口供前端访问。这些接口如下:
path('car_move',app.views.CarView.car_move),
path('car_sensor',app.views.CarView.car_sensor),
path('car_max_id',app.views.CarView.trace_max_id),
path('trace_record',app.views.CarView.trace_record),
car_move
该接口用于控制小车的运动。前端调用该接口时,需要发送两个参数,运动元组和选择控制的小车的IP地址,即为direction和carIP。当后端接受到前端的调用时,先搜索此时基站是否有该IP地址的连接,若没有,则不做任何操作,若有,则向该小车发送运动元组。代码如下:
def car_move(request):
response = {}
if request.method == 'GET':
direction =request.GET.get("direction")# 获取运动元组
carIP =request.GET.get("carIP")# 获取控制的小车的ip
direction = direction.split(',')
for _ in range(4):
direction[_] = strtobool(direction[_])
if carIP in carserver.client_connect_pool.keys():# 该小车的ip已经连接基站
content = {}
content["direction"] = direction
package = carserver.__generate_package__(content,2,0,carIP)
# print(package)
carserver.__sending_package__(package,carIP)
return JsonResponse(response)
car_sensor
该接口向前端提供了分页查询传感器数据的功能。分页查询使用了Django库提供的Paginator类,只需提供简单的参数即可,不需要编写复杂的sql语句。当前端调用该接口时,需要发送两个参数,页面序号和每页数据个数,即pageNum和pageSize,默认情况下,pageNum为1,pageSize为10。当后端接受到前端的调用时,使用Paginator类对参数进行搜索,并返回搜索的结果。代码如下:
def car_sensor(request):
response = {}
if request.method == 'GET':
# carIP =request.GET.get("car")# 获取需要获取数据的小车的ip
pageNum = request.GET.get('pageNum')
pageSize = int(request.GET.get('pageSize'))
book_list = datas.objects.all()
paginator = Paginator(book_list, pageSize)
response['total'] = paginator.count
try:
books = paginator.page(pageNum)
except PageNotAnInteger:
books = paginator.page(1)
except EmptyPage:
books = paginator.page(paginator.num_pages)
response['list'] = json.loads(serializers.serialize("json", books))
# book_list = datas.objects.all().extra(tables=['datas'],where=['id > 52'])
# response['list'] = json.loads(serializers.serialize("json", book_list))
return JsonResponse(response)
return JsonResponse(response)
car_max_id
该接口用于获取某小车目前最新运动数据的id,用于辅助实时获取小车运动轨迹的实现。当前端调用该接口时,需要发送一个参数,小车IP,即carIP。当后端接受到前端的调用时,通过条件查询,找到对应该小车IP的轨迹数据的最大id。代码如下:
def trace_max_id(request):
response = {}
if request.method == 'GET':
carIP =request.GET.get("carIP")
s = 'select max(id) from trace where pi=\'' + carIP + '\''
id_map = {'max(id)':'id'}
max_id = trace.objects.raw(s,translations=id_map)
response['max_id'] = max_id[0].id
return JsonResponse(response)
trace_record
该接口用于获取小车运动轨迹数据。当前端调用该接口时,需要发送两个参数,小车IP和最大id,即carIP和max_id。当后端接受到前端的调用时,通过条件查询,找到对应该小车IP的轨迹数据且轨迹数据的id应当大于最大id。代码如下:
def trace_record(request):
response = {}
if request.method == 'GET':
carIP =request.GET.get("carIP")
max_id = request.GET.get("max_id")
s = 'select * from trace where pi = \'' + carIP + '\'' + 'and id > ' + max_id
# print(s)
trace_map = {'id':'id','x':'x','y':'y','pi':'pi'}
trace_all = trace.objects.raw(s,translations=trace_map)
response['trace_list'] = []
for _ in range(len(trace_all)):
t = {}
t['id'] = trace_all[_].id
t['x'] = trace_all[_].x
t['y'] = trace_all[_].y
t['pi'] = trace_all[_].pi
# print(trace_all[_].id)
# print(type(trace_all[_].id))
response['trace_list'].append(t)
return JsonResponse(response)
通信模块
通信模块用于后端与树莓派之间的通信。通信采用socket的方式,数据报文的格式如3.4节所述。当后端服务器启动时,socket监听也同步开启。当小车在基站范围内时,保持与基站的连接,并收发报文。
当基站接收到小车发送的数据时,首先需要判断该数据为小车运动数据还是传感器监测的环境数据。若为环境数据,则直接存入数据库中;若为小车的运动数据,则需要对这些数据进行处理、计算。
小车的运动数据为一个列表,列表中保存了3秒内小车的所有运动数据,可能为空。当该列表不为空时,需要遍历所有的运动数据进行处理。运动数据包括运动元组、运动发送时间、运动速度。运动数据中的运动元组有两种可能性,一是此时开始发送运行,二是此时某运动停止。因为运动的开始和停止总是成对出现的,因此不会出现连续收到两个运动的运动元组获停止的运动元组。因此在程序中,需要定义一个结构来保存上次的运动元组、上次运动发生的时间和运动角度。
当收到运动的运动元组时,代表此时开始运动,此时只需保存下运动状态即可;当收到停止的运动元组时,代表此时运动停止,需要计算上次运动的坐标或角度。当上次运动为前后方向时,需要计算此次运动的相对坐标,两次收到运动元组的时间差乘以20即为运动长度,再分别乘以sin角度与cos角度即可获得坐标值;当上次运动为左右方向时,需要计算此次运动的角度变化值,并修改程序中的角度变量。经多次测量,车转一周的时间约为3.3秒,因此,当左/右转动的时间为1秒时,大约转动109.1°,若为左(右)转,则在原有基础上减(增)相应度数。根据这个公式,就可以计算出每次左/右转对小车前进角度的影响,提高了轨迹坐标计算的准确度。