需要实现一个车辆模拟器,在地图上按照一条线路来驾驶,模拟器需要通过MQTT与服务器进行通讯,发送车辆当前的GPS坐标,速度等信息。服务器根据车辆的位置,以及当前地图区域上的网络状况,提前预测车辆将要到达区域的网络状况,并采取相应的行动(例如调整网络参数等)并发送消息通知模拟器。
模拟器的实现
模拟器的界面是基于百度地图来实现的一个前端页面,这里需要实现几个功能:
1. 与服务器的通讯
前端页面与一个MQTT服务器通信,实时上报车辆的信息到一个主题,并且订阅另外一个主题来获取服务器的通知消息。
MQTT服务器采用的是ActiveMQ的artemis,具体的设置可以参见我以前的博客https://blog.csdn.net/gzroy/article/details/104013738, 为简便起见,这里就不用设置SSL和颁发证书了。
2. 模拟车辆在地图上行驶
采用百度的开放地图平台来搭建这个模拟器。通过DrivingRoute函数来设置一条行驶路线,获取路线的坐标点,然后定义一个小车图片的Marker,按照坐标点来进行位置更新,在更新位置的同时,计算当前行驶方向的方位角(即当前坐标与上一个坐标之间的连线与正北方向的夹角),调整小车图片的旋转角度,使得车头始终对准行驶方向。在行驶过程中,每次更新坐标时都往MQTT服务器发送位置消息。此外在行驶路线上还增加了几个Marker,表示不同位置的网络质量的状态。
代码如下:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<style type="text/css">
body, html,#allmap {width: 100%;height: 100%;overflow: hidden;margin:0;font-family:"微软雅黑";}
</style>
<script type="text/javascript" src="http://api.map.baidu.com/api?v=3.0&ak=XXXX"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>
<title>车辆模拟器</title>
</head>
<body>
<div id="allmap"></div>
<script type="text/javascript">
client = new Paho.MQTT.Client("localhost", 1883, "clientId");
client.connect({userName: "abc", password: "123456", onSuccess:onConnect, useSSL:false});
function onConnect() {
// Once a connection has been made, make a subscription and send a message.
console.log("onConnect");
client.subscribe("World");
message = new Paho.MQTT.Message("Hello");
message.destinationName = "World";
client.send(message);
}
// 百度地图API功能
var map = new BMap.Map("allmap");
map.centerAndZoom(new BMap.Point(116.404, 39.915), 15);
map.enableScrollWheelZoom(true);
var myP1 = new BMap.Point(116.381084,39.913178); //起点
var myP2 = new BMap.Point(116.404305,39.90635); //终点
var myIcon = new BMap.Icon("car.png", new BMap.Size(70, 37), { //小车图片
imageSize: new BMap.Size(70, 37),
imageOffset: new BMap.Size(0, 0) //图片的偏移量。为了是图片底部中心对准坐标点。
});
var driving2 = new BMap.DrivingRoute(map, {renderOptions:{map: map, autoViewport: true}}); //驾车实例
driving2.search(myP1, myP2); //显示一条公交线路
var toRadian = Math.PI/180;
var toDegree = 180/Math.PI;
var xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange=state_Change;
xmlhttp.open("GET",'http://localhost/measurements.txt',true);
xmlhttp.send(null);
function state_Change(){
if (xmlhttp.readyState==4){// 4 = "loaded"
if (xmlhttp.status==200){
measurements_txt = xmlhttp.responseText;
console.log(measurements_txt)
records = measurements_txt.split("\n");
for(i=0;i<records.length;i++){
record = records[i].split(",");
var marker = new BMap.Marker(new BMap.Point(parseFloat(record[0]), parseFloat(record[1])));
marker.setTitle(record[2]);
map.addOverlay(marker);
}
}
else{
alert("Problem retrieving measurement data");
}
}
}
window.run = function (){
var driving = new BMap.DrivingRoute(map); //驾车实例
driving.search(myP1, myP2);
driving.setSearchCompleteCallback(function(){
var pts = driving.getResults().getPlan(0).getRoute(0).getPath(); //通过驾车实例,获得一系列点的数组
var paths = pts.length; //获得有几个点
for (i=0;i<paths;i++){
console.log(pts[i])
}
var radian_conv = Math.PI/180;
var carMk = new BMap.Marker(pts[0],{icon:myIcon});
map.addOverlay(carMk);
i=0;
function calculateRotate(lon1, lat1, lon2, lat2){
lon1 = lon1*toRadian;
lat1 = lat1*toRadian;
lon2 = lon2*toRadian;
lat2 = lat2*toRadian;
deltaFI = Math.log(Math.tan(lat2/2+Math.PI/4)/Math.tan(lat1/2+Math.PI/4));
deltaLON = Math.abs(lon1-lon2)%180;
theta = Math.atan2(deltaFI, deltaLON)*toDegree;
return -theta;
}
function resetMkPoint(i){
carMk.setPosition(pts[i]);
if(i>0 && i<paths){
rotate = calculateRotate(pts[i-1].lng, pts[i-1].lat, pts[i].lng, pts[i].lat);
carMk.setRotation(rotate);
message = new Paho.MQTT.Message(pts[i].lng.toString()+","+pts[i].lat.toString());
message.destinationName = "vehicle/location/vin123";
client.send(message);
}
if(i < paths){
setTimeout(function(){
i++;
resetMkPoint(i);
},500);
}
}
setTimeout(function(){
resetMkPoint(5);
},500)
});
}
setTimeout(function(){
run();
},500);
</script>
</body>
</html>
服务器端的实现
服务器订阅车辆上报的MQTT消息,获取车辆的位置,判断车辆行进方向上离哪个测量点最近,根据那个测量点的网络状况来预测车辆的网络变化,从而及早进行网络参数的调整。这里用到了Google的S2库来进行地理坐标的距离和范围的计算,具体使用S2的方法可以见我以前的博客:https://blog.csdn.net/gzroy/article/details/104054507
这里我只做一个简单的示例,即在车辆驾驶途中,服务器判断车辆将接近哪个测量点,并打印出来。代码如下:
import paho.mqtt.client as mqtt
from cacheout import Cache
import re
import time
import pywraps2 as s2
import math
re_location = r'.*\/location\/(.*)'
radius = 500 #500 meters
radius_of_earth = 40076020/2
degree_to_distance = (radius_of_earth)/180
radius_to_angle = s2.S1Angle.Degrees(radius/radius_of_earth*180)
geofence_cell_level = 12
geofence_cell_level_bits = geofence_cell_level*2+3
remaining_bits = 64-geofence_cell_level_bits
cell_level_bits = int(math.pow(2, geofence_cell_level_bits+1)-1)<<remaining_bits
measurement_point_cache = Cache(maxsize=512, ttl=120)
measurement_infor_cache = Cache(maxsize=512, ttl=120)
measurement_cellid_cache = Cache(maxsize=512, ttl=120)
vehicle_location_cache = Cache(maxsize=1024, ttl=1)
region_cover = s2.S2RegionCoverer()
region_cover.set_fixed_level(geofence_cell_level)
measurements = []
with open('measurements.txt', 'r') as f:
lines = f.readlines()
for l in lines:
longitude, latitude, infor = l.split(',')
point = s2.S2LatLng_FromDegrees(float(latitude), float(longitude)).ToPoint()
cellid = s2.S2CellId(point).id()
key = hash(l)
measurement_point_cache.set(key, point)
measurement_infor_cache.set(key, infor)
measurement_cellid_cache.set(cellid, key)
def predict_network_quality(vin):
location = vehicle_location_cache.get(vin)
current_point = s2.S2LatLng_FromDegrees(location[0][0], location[0][1]).ToPoint()
last_point = s2.S2LatLng_FromDegrees(location[1][0], location[1][1]).ToPoint()
geofence = s2.S2Cap(current_point, radius_to_angle)
cells = region_cover.GetCovering(geofence)
keys = measurement_cellid_cache.keys()
selected_keys = []
for cell in cells:
cellid = cell.id()
cell_begin = cellid & cell_level_bits
cell_end = cell_begin + int(math.pow(2, remaining_bits))
for k in keys:
if k>cell_begin and k<cell_end:
selected_keys.append(k)
selected_measuments_keys = measurement_cellid_cache.get_many(selected_keys).values()
min_distance = radius
nearest_point_key = None
distance = s2.S1Angle(current_point, last_point).degrees()*degree_to_distance
for k in selected_measuments_keys:
point = measurement_point_cache.get(k)
d1 = s2.S1Angle(point, current_point).degrees()*degree_to_distance
d2 = s2.S1Angle(point, last_point).degrees()*degree_to_distance
angle = math.acos((d1**2+distance**2-d2**2)/(2*d1*distance))
if angle>math.pi/2 and angle<math.pi:
if d1<min_distance:
nearest_point_key = k
min_distance = d1
if nearest_point_key is not None:
network_infor = measurement_infor_cache.get(nearest_point_key)
print('find nearest point:'+network_infor)
return True
else:
return False
def on_connect(client, userdata, flags, rc):
print("Connected with result code "+str(rc))
def on_message(client, userdata, msg):
location_topic = re.search(re_location, msg.topic)
if location_topic:
vin = location_topic.group(1)
longitude, latitude = str(msg.payload, 'utf-8').split(',')
longitude = float(longitude)
latitude = float(latitude)
if vehicle_location_cache.has(vin):
existing_location = vehicle_location_cache.get(vin)
vehicle_location_cache.set(vin, [(latitude, longitude), existing_location[0]])
predict_network_quality(vin)
else:
vehicle_location_cache.set(vin, [(latitude, longitude)])
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.username_pw_set('abc', '123456')
client.connect("localhost", 1883, 60)
client.subscribe('vehicle/#', 0)
client.loop_start()
start = time.time()
while True:
time.sleep(1)
end = time.time()
if int(end - start) > 60:
client.loop_stop()
break
client.disconnect()