背景知识
当我们探索未来的交通系统和智能交通解决方案时,车辆到一切(Vehicle-to-Everything, V2X) 通信技术显得尤为重要。V2X是指在车辆与车辆(V2V )、车辆与基础设施(V2I )、车辆与行人(V2P )以及车辆与网络(V2N)之间进行的通信。这种技术能够提高道路安全,优化交通流量,减少拥堵,提升驾驶体验,并为自动驾驶汽车的实现打下基础。
为了准确模拟和分析V2X通信中的复杂交互,需要使用一些专用的仿真工具:
-
OMNeT++ 是一个公开源码的网络仿真框架,提供了广泛的工具集和功能,用于构建复杂的网络和其他分布式系统。OMNeT++的灵活性和模块化使其成为研究和模拟通信网络,特别是V2X通信网络的理想选择。
-
INET 是OMNeT++的一个扩展模型库,专注于互联网协议和网络技术的仿真。INET提供了大量的网络协议模型,如TCP/IP、路由协议等,允许研究者构建和测试各种网络架构和服务。
-
SUMO (Simulation of Urban MObility) 是一个开源的交通仿真软件,用于模拟城市的车辆流动。通过SUMO,研究人员可以创建详细的城市交通场景,包括道路网络、交通信号灯、车辆行为等,来分析不同交通策略和管理措施的效果。
-
VEINS 是一个允许OMNeT++和SUMO之间进行联合仿真的框架,专门用于车辆通信系统的研究。它使得OMNeT++模拟的通信网络和SUMO模拟的移动车辆能够实时交互,从而实现对V2X通信场景的全面仿真。
使用OMNeT++结合VEINS、INET和SUMO进行联合仿真,能够在复杂的城市交通环境中准确模拟V2X通信。这种仿真可以帮助研究者评估V2X技术在实际应用中的表现,如通信延迟、系统可靠性和安全性等。此外,仿真结果还可以指导政策制定者和工程师设计更有效的交通管理策略和智能交通系统,推动智能交通技术的发展,为实现无缝、安全和高效的未来交通网络提供支持。
在这篇博文中,我将尝试:
- 搭建仿真的环境,导入VEINS和INET开源库
- 使用INET库中提供的网络协议和功能模型来模拟通信协议和网络功能
- 使用SUMO创建道路网络和车辆轨迹,并导出到OMNeT++中
- 在OMNeT++中使用VEINS通过socket和SUMO连接,借助VEINS实现的TRACI接口来设置车辆相关的行为和场景
- 在OMNET++中结合VEINS,SUMO和INET运行联合仿真实验,并分析车辆在遇到紧急情况下的通信状况和具体行为
环境搭建
OMNet++ / Veins / INET / SUMO下载
关于这四个软件的下载安装,以下两篇博文是很好的参考:
https://www.cnblogs.com/Xylona/p/17779621.html
(veins5.0+sumo1.2.0+OMNeT++5.5.1)车载自组织网络仿真环境安装配置教程(一步一步)_sumo1.3.0+veins5.2+omnet++5.5.1-CSDN博客
我最终使用的版本:
- OMNeT++ 5.6.2
- Veins 5.2
- SUMO 1.10.0
- INET 4.2.5
环境配置
修改configure.user:
在分别下载好这四个版本的软件后,进入OMNet++文件夹,打开configure.user,修改PREFER_CLANG 的值为no
sumo环境变量配置:
共配置四条:
解压&编译OMNET++:
打开OMNet++中的"mingwenv.cmd",并按下任意键开始解压;
解压完成后分别输入"./configure "和"make"进行编译,make的时间大概要一个小时
打开OMNETT++:
在"mingwenv.cmd"中输入"omnetpp"打开IDE界面
导入VEINS & INET:
点击左上角File -> Import:
导入inet和veins:
注意!对于veins要勾选search for nested projects!!
点击左上角Project -> Build All
运行INET的示例仿真:
点击inet -> examples -> aodv -> omnetpp.ini,然后右键Run as Omnet ++ simulation就可以看到示例仿真:
运行VEINS的示例仿真:
在"mingwenv.cmd"中输入以下指令使veins连接上sumo:
C:/Users/Majiaming/Desktop/WESTERN/9038_wireless_comm/project/project_new/veins-veins-5.2/bin/veins_launchd -vv -c C:/Users/Majiaming/Desktop/WESTERN/9038_wireless_comm/project/project_new/sumo-1.10.0/bin/sumo.exe
点击左侧veins -> examples -> veins -> omnetpp.ini,然后右键Run as Omnet ++ simulation就可以看到示例仿真:
点击Run后:
现在,成功完成了环境搭建,接下来就要开始创建一个属于我的项目。
关于接下来的大体步骤,我参考了以下油管的视频,但是如果仿真软件的版本和我相差不大,建议参考我的版本,因为我按照我下载的这一套VEINS,SUMO,INET,OMNET++版本完全按照视频操作会出现一些问题,所以建议有什么不会再去参考油管的视频:
How to Create a New OMNET++ Project That Works with INET and Veins (youtube.com)
How to Simulate a V2V Network using OMNET++, INET, and Veins - YouTube
How to Add A Custom Sumo Simulation to OMNET++ (youtube.com)
创建工程
- 左上角file -> new -> OMNET++ project创建project:
- 右击新建的project -> Properties -> Project References,链接INET和VEINS和veins_inet:
- 别关掉上面的窗口,按照以下步骤完善make规则:
现在,这个新的project已经成功的refer到了veins 和 inet,但是,问题来了,如何才能将二者结合起来?答案是:veins中提供了一个sub-project,名字是veins_inet,其中提供了大量现成的cc代码和一个简单的示例,这个sub-project提供了一个将二者结合的很好例子,这也是为什么刚刚import veins的时候要同时Import veins_inet这个子项目!
VEINS & INET 的结合
- 将veins_inet的仿真例程迁移到我新创建的project:
① 找到刚刚import的veins_inet子项目, 找到 examples -> veins_inet 拷贝到新项目的simulation下:
出现了大量的红色叉叉,这是因为**.ned文件中的package的名字不正确**
②修改.ned文件中的package name:
cpp
//在所有.ned文件中:(其实就两个)
//将开头的
package XXXXXXX.veins_inet;
//修改为
package _9038_project.simulations.veins_inet;
③可以通过运行当前路径下的omnetpp.ini来测试是否成功:
由于此处包含veins,别忘了在OMNETT++根目录下的mingwenv.cmd中输入以下代码连接SUMO:
cpp
C:/Users/Majiaming/Desktop/WESTERN/9038_wireless_comm/project/project_new/veins-veins-5.2/bin/veins_launchd -vv -c C:/Users/Majiaming/Desktop/WESTERN/9038_wireless_comm/project/project_new/sumo-1.10.0/bin/sumo.exe
可见,成功在自己创建的项目中运行了veins_inet的例程仿真!
现在,已经可以成功的在自己创建的project里运行结合veins_inet例子下的仿真,但是例子中的sumo地图并不是我想要的,下一步是根据自己的地图,实现veins,inet,sumo的联合仿真
VEINS & INET & SUMO 的联合仿真
创建自己的仿真路径
在新建项目的simulations下再创建一个"my_veins_inet"的文件夹,把刚刚"veins_inet"的内容全部复制进来:
(同样,记得修改两个.ned文件中的package名,此处不再展开!)
然后,将square.net.xml ; square.poly.xml ; square.rou.xml; obstacles.xml删除,剩下的留着待会修改
自定义sumo地图导入:
此处我选择了留学所在城市的一家costco附近的地图
网站:https://www.openstreetmap.org/export#map=16/42.9852/-81.2900
① 选定区域后点击左侧的"Export"导出.osm文件
②通过python脚本分别生成.net.xml;.rou.xml;.poly.xml:
请根据文件位置修改文件路径!
python
import numpy as np
import subprocess
#生成map.net.xml
# 设置你的.osm文件路径
osm_file_path = 'costco.osm' #根据https://www.openstreetmap.org 网站导出的
# 设置输出文件路径
output_file_path = 'costco.net.xml'
# 构建netconvert命令
netconvert_command = f'netconvert --osm-files {osm_file_path} -o {output_file_path}'
# 调用命令
subprocess.run(netconvert_command, shell=True)
print(".net.xml成功生成")
#生成map.poly.xml
osm_file_path = 'costco.osm' #根据https://www.openstreetmap.org 网站导出的
type_file_path = 'typemap.xml' #内容根据CSDN收藏
# 设置输出文件路径
output_file_path = 'costco.poly.xml'
# 构建netconvert命令
netconvert_command = f'polyconvert --net-file costco.net.xml --osm-files {osm_file_path} --type-file {type_file_path} -o {output_file_path}'
# 调用命令
subprocess.run(netconvert_command, shell=True)
print(".poly.xml成功生成")
# 生成map.rou.xml
# 设置SUMO环境变量(请根据你的SUMO安装路径进行调整)
sumo_tools_dir = "C:/Users/Majiaming/Desktop/WESTERN/9038_wireless_comm/project/project_new/sumo-1.10.0/tools"
sumo_network_file = "costco.net.xml"
output_route_file = "costco.rou.xml"
# 构建randomTrips.py脚本的完整命令
command = [
"python",
f"{sumo_tools_dir}/randomTrips.py",
"-n", sumo_network_file,
"-e", "100",
"-l",
]
# 使用subprocess运行命令
result = subprocess.run(command, capture_output=True, text=True)
# 检查命令输出(可选)
if result.returncode == 0:
print("执行成功1")
print(result.stdout) # 打印标准输出
else:
print("执行出错1")
print(result.stderr) # 打印错误输出
####################################################
# 构建randomTrips.py脚本的完整命令
command = [
"python",
f"{sumo_tools_dir}/randomTrips.py",
"-n", sumo_network_file,
"-r", output_route_file,
"-e", "50",
"-l",
]
# 使用subprocess运行命令
result = subprocess.run(command, capture_output=True, text=True)
# 检查命令输出(可选)
if result.returncode == 0:
print("执行成功2")
print(".rou.xml成功生成")
print(result.stdout) # 打印标准输出
else:
print("执行出错2")
print(result.stderr) # 打印错误输出
③ 生成后,将这三个文件放到"my_veins_inet"下备用
修改SUMO相关文件:
"my_veins_inet"下已经有了sumo相关的各种文件,现在需要根据具体要求修改它们
① 将square.sumocfg和square.launchd.xml改名为costco.sumocfg 和costco.launchd.xml,并根据新的rou, net和poly文件修改内容:
修改完成后,可以用sumo gui打开costco.sumocfg查看效果:
②修改.rou.xml:
为了让仿真效果尽可能的清晰,将随机生成的50辆车的代码注释掉,并替换成如下的代码:
python
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://sumo.dlr.de/xsd/routes_file.xsd">
<vType id="vtype0" accel="2.6" decel="4.5" sigma="0.5" length="4.5" minGap="2.5" maxSpeed="14" color="1,1,0"/>
<route id="route0" edges="-62088703#4 -62088703#3 -62088703#2 -62088703#1 474843229#0 474843229#1 474843229#2 474843229#3 474843229#4 474843229#5 474843229#6 466961753#1 466961753#2 466961753#3 466961753#4 466961753#5 466961753#6 466961753#7 466961753#8 -466961753#8"/>
<flow id="flow0" type="vtype0" route="route0" begin="0" period="3" number="5" arrivalPos="0" />
</routes>
<vType>
元素:
- 定义了一种车辆类型
vtype0
accel
(加速度): 车辆的最大加速度,这里是2.6米/秒²decel
(减速度): 车辆的最大减速度,这里是4.5米/秒²sigma
(驾驶员不确定性): 描述了驾驶员行为的不确定性,这里是0.5,范围从0(完全确定行为)到1(非常不确定的行为)length
(长度): 车辆的长度,这里是4.5米minGap
(最小间隙): 车辆之间的最小间隙,这里是2.5米maxSpeed
(最大速度): 车辆的最大速度,这里是14米/秒color
(颜色): 车辆的颜色,这里定义为黄色(RGB颜色模式中的1,1,0)
<route>
元素:
- 定义了一个路线
route0
id
属性指定了路线的唯一标识符edges
属性列出了构成该路线的边的序列。边是道路网中的基本元素,代表单向的道路。这里列出的边通过它们的唯一标识符来指定,负号表示逆向行驶
<flow>
元素:
- 定义了一个流量
flow0
,表示一定数量的车辆将沿指定的路线移动id
属性为流量提供了一个唯一标识type
属性指定了车辆类型,这里引用了之前定义的vtype0
route
属性指定了车辆将遵循的路线,这里是route0
begin
属性定义了流量开始的时间(秒),这里是从仿真开始的0秒period
属性定义了车辆生成的时间间隔(秒),这里是每3秒生成一次number
属性定义了将被生成的车辆总数,这里是5辆arrivalPos
属性定义了车辆到达目的地的位置,这里是0,通常表示车辆将尝试行驶整个路线
修改.ned & .ini文件:
①修改Scenario.ned:
修改灰色区域范围大小,以适应新的sumo地图
② 修改omnetpp.ini:
运行仿真文件
①右击omnetpp.ini,右击Runs as -> Run Configuration:
②设置完成后点击右下角:Apply -> run:
进入页面后选择genreal:
结果展示
最终仿真效果:
- 根据.rou.xml的设置,node[0]至node[4]五辆车预先定义要走的路线完全一致。
- 然而,node[0]变红代表了模拟突发事故,事故发生后,车辆之间开始相互通信,得到消息的车辆会变成绿色。
- node[3]和node[4]由于具备调头条件,直接掉头,更改了原有的路线并规划了新的路线。
- node[1]和node[2]由于不具备调头条件只能按照原有路线继续往前,在被node[0]挡住后,根据sumo内部的算法换道并继续前进。
仿真结果简单分析
吞吐量(overall throughput value)
上图显示了4个节点随着时间的吞吐量变化,可以明显的看到,对于这4个节点,吞吐量都在事故发送时开始增加,在0.1秒左右达到最高,并在0.2秒后快速下降,可见通讯速度很快。
包发送量(packet sent count)
可见,node0为2次,其他三个节点都为1次,这也很好理解,因为node0是事故发送的节点,所以需要发送一次事故包一次转发包;而其他节点只需要发送一次转发包即可。
思路和代码解读
现在,已经实现OMNET++,SUMO,VEINS和INET的联合仿真,但是背后的原理是什么其实依然一知半解,于是我开始解读代码,尝试理解为什么仿真会这样进行。
注意,以下所有内容都属于自问自答,完全可能有错误,欢迎大家指正,本人纯小白
Q1 :在最后的仿真路径中,Scenario.ned中的manager,,radio medium,node和physical environmnet分别到底代表什么?在图形界面中拖动他们的位置或改变他们的大小有什么实际的意义?
A:
manager
通常用于仿真管理任务,如控制仿真流程、管理节点间的通信、协调事件等radioMedium
定义了无线电传播的模型,如何模拟信号的传播和衰减、如何处理节点间的无线通信等node
代表仿真网络中的一个参与者或设备,在车辆网络仿真中,这些节点通常是车辆physicalEnvironment
模块定义了仿真的物理环境,包括地形、建筑物等,这些因素可能会影响信号的传播和车辆的移动在OMNeT++ IDE的图形界面中,拖动这些组件的位置或改变它们的大小主要是为了改善视觉布局,帮助仿真设计者更好地理解和组织仿真场景。这些操作不直接影响仿真逻辑或结果,仿真的逻辑和行为完全由NED文件中的参数定义和配置文件(
.ini
文件)中的设置决定。
Q2 :根据网络原理,网络协议应该分为5层,应用层,传输层,网络层,链路层,物理层,这些体现在项目的哪里?
A:
- 在这个仿真中,每一个节点(汽车)都被注册为一个名为VeinsInetCar的对象:
- 而通过跳转VeinsInetCar.nedf会发现,它继承自一个叫AdhocHost的对象:
- 使用同样的方法不停的跳转,会得出这样一个继承的关系:
- 而通过分析这些.ned文件,可以总结出以下内容:
- 所以,每个汽车所代表的VeinsInetCar都是从NodeBase一路继承过来的,而观察继承的路线就可以发现,在一路的继承中,就分别实现了网络协议中5大层的接口定义!
Q3 :根据Q2,有个随之而来的问题:我理解了为什么VeinsInetCar具备了完整的协议栈,网络协议的5大层实现在了哪里。但是我依然不理解:从NodeBase一直继承到了VeinsInetCar,现在已经有了无数的接口用来配置完整的协议,但是使用哪些接口具体配置哪些值,比如在链路层具体配置为无线还是VLAN,网络层具体配置为IPV4还是IPV6,传输层具体配置为TCP还是UDP,这些具体的配置在哪里?
A:
答案是:大部分在仿真文件omnetpp.ini中定义:
- 链路层具体配置:
- 网络层具体配置:
- 应用层具体配置:
物理层没有定义很好理解,这都是最底层的设定。
但是,显然会有一个问题,为什么没有传输层的相关定义?到底使用TCP还是UDP还是什么?
chatgpt给出了三种回答:
默认配置 :许多OMNeT++和INET模块,包括传输层协议模块,具有默认配置。例如,如果一个应用需要使用UDP或TCP,并且在其模块定义中正确指定了,那么即使在
.ini
文件中没有明确配置,这些传输层协议也会被自动实例化并使用默认设置。应用层决定 :在许多情况下,特别是在使用简单应用(如
UdpBasicApp
)或其他特定的应用模型时,所使用的传输协议(UDP、TCP等)可能已在应用层模块的实现中明确定义。例如,一些应用模型默认使用UDP进行通信,而无需在.ini
文件中进行额外配置。模块继承 :由于
VeinsInetCar
继承自AdhocHost
,进而继承自StandardHost
,一直到NodeBase
,这些基础模块可能已经包含了对传输层协议的支持。特别是在AdhocHost
或StandardHost
级别,通常会包括对主要传输层协议(如TCP和UDP)的支持,而无需在每个仿真场景的.ini
文件中进行单独配置。我认为都有一定的道理,根据上面的截图可以知道,应用层是由VeinsInetSampleApplication来实现的,跳转到其对应的.ned文件,可以发现它继承自VeinsInetApplicationBase:
再次跳转,找到这个VeinsInetApplicationBase:
这个代码并没有明确的规定UDP还是TCP,但是根据socket门的注释,应该是使用的UDP。
- 最后,为了验证,运行仿真并点击一个节点:
可见,虽然链路层出现了循环回路lo,传输层出现了tcp,但是真正实现从上到下沟通的协议还分别是:UDP,IPV4和WLAN
(我的推测是:由于没有明确的规定lo和tcp不能使用,所以他们也可能存在,并在某种方面帮助消息传播?)
Q4 :在最后的仿真结果中,汽车的行为是在哪里被定义的?比如,为什么node[0]会在20秒坏掉又在50秒重新启动?
A:
这个问题的答案其实已经在仿真结果下的文字中回答一部分了,但是其行为的核心,其实还是在于应用层所定义的这个"VeinsInetSampleApplication",核心的代码就定义在VeinsInetSampleApplication.cc中(veins_inet->src->veins_inet),以下是代码中最重要的两个函数:
cpp//设置哪个节点什么时候速度为0,什么时候恢复的函数 bool VeinsInetSampleApplication::startApplication() { // host[0] should stop at t=20s if (getParentModule()->getIndex() == 0) { //如果节点为0 auto callback = [this]() { //这个函数在20秒倒计时结束后被运行 getParentModule()->getDisplayString().setTagArg("i", 1, "red"); //车子变红 traciVehicle->setSpeed(0); //车子不会立刻停下来,因为仿真软件中定义的物理限制的影响,例如车辆的减速能力,但车子肯定会在20秒后很短时间内停下 //定义要发送的包的内容 auto payload = makeShared<VeinsInetSampleMessage>(); payload->setChunkLength(B(100)); payload->setRoadId(traciVehicle->getRoadId().c_str()); timestampPayload(payload); //定义包的名字并发送包 auto packet = createPacket("accident"); packet->insertAtBack(payload); sendPacket(std::move(packet)); // host should continue after 30s auto callback = [this]() { //这个函数在30秒倒计时结束后被运行 traciVehicle->setSpeed(-1); //车子恢复正常 }; timerManager.create(veins::TimerSpecification(callback).oneshotIn(SimTime(30, SIMTIME_S)));//30秒倒计时 }; timerManager.create(veins::TimerSpecification(callback).oneshotAt(SimTime(20, SIMTIME_S)));//20秒倒计时 } return true; } //规定节点在收到消息之后如何反应的函数 void VeinsInetSampleApplication::processPacket(std::shared_ptr<inet::Packet> pk) { auto payload = pk->peekAtFront<VeinsInetSampleMessage>(); EV_INFO << "Received packet: " << payload << endl; getParentModule()->getDisplayString().setTagArg("i", 1, "green"); //接收到包,车子变绿 traciVehicle->changeRoute(payload->getRoadId(), 999.9); //重新规划路径 if (haveForwarded) return; //用于避免重复处理或发送相同的消息 //如果是第一次收到消息 auto packet = createPacket("relay"); //创建用于转发的包 packet->insertAtBack(payload); sendPacket(std::move(packet)); //转发 haveForwarded = true; }
根据代码可以得出以下的逻辑:
当node[0]停止,即模拟事故发生时,会立刻装载一个大小为100Byte的包,并在其中添加当前所在路的ID,然后向通讯范围内的所有节点(汽车)发送这个包。每个收到这个包的节点(汽车)会立刻从包中获取事故路段的ID,并假设事故路段的通过时间为最大。这就导致所有会经过事故路段且拥有切换路径条件的汽车在收到这个包后会立刻切换路径。同时,向其通信范围的所有节点(汽车)转发一次包的内容。
Q5:
A: