Day 14 完成了道路、車流與交通輸出,也讓紅色的 demo_car 通過兩座號誌路口。
但 SUMO 畫面中的兩台車即使彼此靠近,也不會因此自動交換封包。
如果我們想知道一筆車輛狀態訊息何時送出、經過多少延遲、是否在頻道中遺失,以及接收端何時做出反應,就需要另一套模擬能力:
SUMO
→ 車輛在哪裡?速度多少?走哪一條 Route?
OMNeT++
→ 哪個節點何時送出 Message?
→ Message 經過哪些 Module 與 Channel?
→ 接收端何時收到?留下哪些結果?
這就是 OMNeT++ 在本系列中的角色。
今天不會直接建立完整的 IEEE 802.11p、C-V2X 或 BSM/CAM 模型,而是先看懂 OMNeT++ 的核心語言:離散事件、Module、Network 與 Message。
最後會建立兩個抽象節點,讓 carA 在第 1 秒送出 vehicleStatus,由 carB 延遲回覆 ack,並在 Qtenv 逐步觀察三個事件。
讀完這篇文章,你應該能夠:
omnetpp.ini 的分工OMNeT++ 是一套以 C++ 為基礎、可延伸且元件化的離散事件模擬框架,主要用來建立網路與分散式系統模擬器。
它提供的核心能力包括:
這裡有一個很重要的邊界:OMNeT++ Core 提供模擬框架,不代表安裝完成後就自動擁有所有 Internet、Wi-Fi 或 V2X Protocol Model。
若要使用現成的 Ethernet、IPv4/IPv6、TCP/UDP、IEEE 802.11 與 Mobility 等模型,通常還會搭配 INET Framework。
車聯網研究則可能再使用 Veins 等框架,把 OMNeT++ 的網路模擬和 SUMO 的道路交通模擬接起來。
| 工具或框架 | 主要角色 | 今天會用到嗎? |
|---|---|---|
| OMNeT++ Core | 事件排程、Module、NED、C++ API、執行與結果紀錄 | 會,今天的主角 |
| INET Framework | 提供有線、無線、Internet Protocol 與 Mobility 等模型庫 | 不安裝,先知道邊界 |
| Veins | 提供車聯網模型與 SUMO/OMNeT++ 耦合能力 | Day 16 再進入 |
| SUMO | 模擬道路、車流與車輛移動 | 沿用 Day 13~Day 14 的場景 |
所以 OMNeT++ 不是 SUMO 的替代品,INET 與 Veins 也不是 OMNeT++ 的另一個名字。
它們解決不同層次的問題,再依研究需求組合起來。
Discrete-event Simulation(DES,離散事件模擬)把系統狀態的改變表示成一連串發生在特定模擬時間點的事件。
假設一個簡化模型只有以下三件事:
t = 1.000 s carA 的傳送 Timer 到期
t = 1.010 s carB 收到 Message
t = 1.040 s carA 收到 Ack
在 1.010 s 與 1.040 s 之間,如果沒有其他事件,Simulation Kernel 不必真的執行 30 次「每 1 ms 更新一次」的空步驟。
它可以直接取出下一個事件,將 Simulation Time 跳到 1.040 s,再執行該事件。

圖 1:Simulation Kernel 從 Future Event Set 取出下一筆事件,模擬時間只在需要處理事件時向前推進
OMNeT++ 會把尚未發生的排程事件保存在 Future Event Set(FES,未來事件集合)。
每次迴圈可以先簡化成:
取出最早事件
→ 將 Simulation Time 推進到該事件時間
→ 把 Message 交給目的 Simple Module
→ Module 處理後可能再排入新的事件
→ 重複直到沒有事件或達到停止條件
事件在模型中被視為瞬間發生。
如果要表達傳播、傳輸、處理或等待所花的時間,必須把延遲明確放進 Channel、sendDelayed()、Self-message 或其他模型邏輯。
Qtenv 上顯示的 1.040 s 是模擬世界的時間,不代表電腦真的執行了 1.040 秒。
一段 60 秒的模擬可能在真實世界幾毫秒內完成,也可能因為無線模型、節點數量與事件量很大而執行數小時。
因此應分開記錄:
| 時間 | 回答的問題 |
|---|---|
| Simulation Time | 模型中的封包、Timer 與節點狀態何時改變? |
| Wall-clock/CPU Time | 電腦實際花多少時間完成模擬? |
不能用動畫播放速度判斷 Protocol Latency,也不能因為 GUI 看起來卡頓,就直接推論模擬中的網路發生壅塞。
兩者關係很密切,但最好不要完全畫上等號。
cMessage 是 OMNeT++ 中用來表示訊息、封包或 Timer 等排程物件的基礎類別。
當一個 Message 被安排在某個時間送達某個 Simple Module 時,這次送達會形成一個事件。
可以先這樣理解:
Message
→ 攜帶名稱、時間與自訂資料的模擬物件
Event
→ Simulation Kernel 在特定時間把 Message 交給 Module 的動作
網路 Frame 或 Packet 常會使用 cMessage 的子類別 cPacket,也能透過 .msg 檔案產生帶有自訂欄位的訊息類別。
今天只使用基本 cMessage,因為目標是先看懂事件生命週期,不先加入真正的 V2X 欄位與無線封包格式。
Module 不一定只接收其他節點送來的 Message。
它也能使用 scheduleAt() 把 Message 排給未來的自己,這類 Message 稱為 Self-message(自我訊息)。
常見用途包括:
例如:
scheduleAt(simTime() + 1, timer);
代表把 timer 排到目前 Simulation Time 的 1 秒後。
到期時,它和其他 Message 一樣會交給 handleMessage(),Module 再用 isSelfMessage() 分辨這是自己的 Timer,還是從 Gate 收到的節點間訊息。
這個觀念之後會直接用在 V2X 週期訊息、重傳與攻擊時序上。
OMNeT++ Model 由彼此傳遞 Message 的 Module 組成。
Module 可以一層包一層,讓複雜系統保有清楚結構。
Simple Module 是模型中的 Active Component。
它的外部介面用 NED 宣告,行為則通常由繼承 cSimpleModule 的 C++ Class 實作。
常見生命週期方法包括:
| 方法 | 何時呼叫 | 常見用途 |
|---|---|---|
initialize() |
模擬開始時 | 讀取參數、建立 Timer、初始化狀態 |
handleMessage() |
Message 到達時 | 處理 Timer、封包與回應 |
finish() |
模擬正常結束時 | 寫出 Scalar 或最後統計 |
handleMessage() 執行期間,Simulation Time 本身不會因為 C++ 程式碼跑了幾行就自動增加。
要表達處理時間,仍要由模型明確排程。
Compound Module 內含其他 Submodule 與 Connection,用來表達系統結構。
例如一台完整的 V2X Vehicle Node,未來可能包含:
Vehicle Node
├── Application
├── Security
├── Network/Transport
├── MAC
├── Radio
└── Mobility
每一層可以是 Simple Module,也可以再由多個 Module 組成 Compound Module。
Compound Module 本身主要負責封裝結構,Active Behavior 應放在內部的 Simple Module。
Network 是可直接拿來執行的頂層 Model。
在 NED 中,network 可以理解為被標記成完整模擬模型的 Compound Module。
omnetpp.ini 會指定本次要執行哪一個 Network,也能設定 Simulation Time Limit、Random Seed 與 Module Parameter。

圖 2:NED 宣告 Model Structure,C++ 實作 Simple Module 行為,Simulation Kernel 再依 omnetpp.ini 排程並遞送 Message
Module 之間通常透過 Gate 傳遞 Message。
| 元件 | 角色 | 今天的例子 |
|---|---|---|
| Gate | Module 的輸入與輸出介面 | in、out |
| Connection | 在同一層 Module Hierarchy 連接 Gate | carA.out 連到 carB.in |
| Channel | 描述 Connection 的傳送行為 | 10 ms Propagation Delay |
Channel 可以加入 Propagation Delay、Data Rate 與 Bit Error Rate 等行為。
不過,今天的 10 ms Connection 只是一條固定延遲的抽象 Link。
它沒有模擬 Radio Range、Signal-to-Noise Ratio、干擾、媒體競爭或 V2X 安全封裝,不能把它稱為真正的 IEEE 802.11p 或 C-V2X Link。
這項限制很重要,因為畫出兩台車和一條線,只能證明 Model Topology 已建立,不能證明無線通訊模型已經完成。
omnetpp.ini 各自做什麼?初學 OMNeT++ 時最容易把三種檔案混在一起。
| 檔案 | 主要內容 | 要回答的問題 |
|---|---|---|
.ned |
Module Type、Parameter、Gate、Submodule 與 Connection | 模型由哪些元件組成?如何連接? |
.cc/.h |
Simple Module 的 C++ 行為 | Message 到達後要做什麼? |
omnetpp.ini |
Network、時間上限、參數、隨機種子與輸出設定 | 這次實驗要怎麼執行? |
.msg |
自訂 Message/Packet Class 定義 | Message 要有哪些欄位? |
可以把它們記成:
NED 定義結構
+ C++ 定義行為
+ omnetpp.ini 定義本次實驗
→ OMNeT++ Simulation Run
同一份 NED 與 C++ Model 可以搭配多組 omnetpp.ini Configuration,改變節點數量、延遲、流量與 Random Seed,而不需要複製整份程式碼。
這也是建立 Baseline 與攻擊組時的重要能力。
Day 14 的 <vehicle id="demo_car"> 是 SUMO 裡的交通參與者。
它知道自己的 Route、Lane、位置與速度,卻沒有 OMNeT++ 的 Application、MAC 或 Radio Module。
OMNeT++ 裡的 Vehicle Node 則是通訊端點。
它可以產生 Message、執行 Protocol 與記錄網路結果,但若沒有 Mobility Model 或 SUMO 整合,就不知道自己正在真實路網的哪一條 Lane 上移動。
| 同一台概念車輛的兩個表示 | 主要狀態 |
|---|---|
| SUMO Vehicle | Route、Lane、Position、Speed、Traffic Light Interaction |
| OMNeT++ Vehicle Node | Application、Packet、Protocol、Radio、Timer、Network Statistics |
Day 16 會透過 TraCI 與整合框架,讓 SUMO 車輛的建立、移動與離開反映到 OMNeT++ Node。
今天建立的 carA 與 carB 還只是固定的抽象 Module,不會讀取 Day 14 的 .net.xml 或 .rou.xml。
以今天的模型為例:
t = 0.000 s
→ carA 在 initialize() 排入一筆 Self-message
t = 1.000 s
→ Self-message 到達 carA
→ carA 建立 vehicleStatus 並從 out Gate 送出
t = 1.010 s
→ 經過 10 ms Channel Delay,carB 收到 vehicleStatus
→ carB 使用 20 ms Reply Delay 回傳同一個 Message
t = 1.040 s
→ 20 ms Reply Delay + 10 ms Channel Delay 後,carA 收到 ack
這裡同時出現三種時間來源:
| 時間來源 | 在哪裡設定 | 代表意義 |
|---|---|---|
| 1 s | scheduleAt() |
carA 的傳送 Timer |
| 10 ms | NED Connection | 節點間傳播延遲 |
| 20 ms | sendDelayed() |
carB 的抽象處理/回覆延遲 |
這些數值都是教學參數,不代表任何真實 V2X Radio、車款或 ECU 的效能。
OMNeT++ Model 可以使用不同 Runtime User Interface 執行。
| 執行環境 | 適合用途 | 限制 |
|---|---|---|
| Qtenv | 動畫、逐事件執行、檢查 Module、Message 與 FES | GUI 會增加執行成本,不適合大量批次實驗 |
| Cmdenv | 重複實驗、Parameter Study、批次執行 | 不提供相同的互動式動畫 |
今天用 Qtenv 看懂事件順序。
未來比較 Baseline、攻擊組與防護組時,則應使用 Cmdenv 批次執行並保存 Configuration、Seed 與結果,不用人工觀看動畫取代統計分析。
OMNeT++ 常見輸出包括:
| 輸出 | 內容 | 適合回答的問題 |
|---|---|---|
Scalar .sca |
每次 Run 的單一統計值 | 總收件數、平均延遲、Packet Delivery Ratio |
Vector .vec |
隨 Simulation Time 變化的序列 | Queue Length、Throughput、End-to-end Delay |
Eventlog .elog |
Event、Message 與 Module 互動紀錄 | Message 何時由哪個 Module 傳到哪裡? |
| Log | Module 執行時輸出的文字 | 初始化、條件判斷與除錯資訊 |
Eventlog 很適合檢查事件因果關係,但大型模擬的檔案可能快速增加。
正式實驗不應無條件替每一個 Run 開啟完整 Eventlog,而要先決定研究問題與需要留下的證據。
同樣地,記錄大量 Vector 也會增加儲存與執行負擔。
結果欄位、Warm-up Period、Random Seed 與 Repetition 都應在實驗設計階段一起規劃。
今天使用 OMNeT++ 6.4.0 的核心功能,不安裝 INET 或 Veins。
實作目標不是完成 V2X Protocol,而是驗證:
NED Structure
→ C++ Behavior
→ Self-message
→ Connection Delay
→ Message Arrival
→ Eventlog/Scalar Result
本文查證時的官方現行版本是 OMNeT++ 6.4.0,發布日期為 2026 年 5 月 8 日。
官方目前建議在 Linux 與 macOS 使用 opp_env 管理 OMNeT++ 及其相依套件。
第一次使用前,先依官方 Installation Guide 安裝 Nix 與 opp_env。
準備完成後,建立獨立 Workspace 並安裝固定版本:
mkdir -p ~/omnetpp-workspace
cd ~/omnetpp-workspace
opp_env init
opp_env install omnetpp-6.4.0
opp_env shell omnetpp-6.4.0
進入 opp_env shell 後確認工具版本,再啟動 IDE:
opp_run -h | head -n 5
omnetpp
opp_env 安裝的套件只能在對應的 opp_env shell 或 opp_env run 環境中使用。
若你已使用官方 Archive 或其他方式完成安裝,可以沿用既有環境,但要記錄 opp_run -h 輸出開頭的實際版本,不要只寫「使用最新版」。
安裝會下載並建置相依套件,所需空間與時間依系統而異。實驗用 VM 建議先建立 Snapshot,也不要混用不同 OMNeT++/INET/Veins 版本的未驗證組合。
在 IDE 選擇:
File
→ New
→ OMNeT++ Project
Project Name 輸入:
day15-basics
選擇 Empty Project,完成後在 Project Root 建立三個檔案:
VehicleNode.ned
VehicleNode.cc
omnetpp.ini
今天把 NED 與 C++ 放在 Project Root,目的是讓第一個模型容易閱讀。
較大的專案通常會再使用 src/、simulations/ 與 Package 分層整理。
在 VehicleNode.ned 填入:
simple VehicleNode
{
parameters:
bool initiator = default(false);
double replyDelay @unit(s) = default(20ms);
gates:
input in;
output out;
}
network Day15Network
{
parameters:
@display("bgb=700,260");
submodules:
carA: VehicleNode {
parameters:
initiator = true;
@display("p=180,130");
}
carB: VehicleNode {
parameters:
initiator = false;
@display("p=520,130");
}
connections:
carA.out --> { delay = 10ms; } --> carB.in;
carB.out --> { delay = 10ms; } --> carA.in;
}
這份 NED 做了四件事:
VehicleNode Simple Module Typein 與 out GateDay15Network 建立 carA 與 carB 兩個 Instanceinitiator 決定誰負責啟動第一筆事件,replyDelay 則表示收到訊息後的教學用回覆延遲。
在 VehicleNode.cc 填入:
#include <omnetpp.h>
using namespace omnetpp;
class VehicleNode : public cSimpleModule
{
private:
int messagesReceived = 0;
protected:
void initialize() override;
void handleMessage(cMessage *msg) override;
void finish() override;
};
Define_Module(VehicleNode);
void VehicleNode::initialize()
{
if (par("initiator").boolValue()) {
cMessage *timer = new cMessage("sendStatusTimer");
scheduleAt(SimTime(1, SIMTIME_S), timer);
}
}
void VehicleNode::handleMessage(cMessage *msg)
{
if (msg->isSelfMessage()) {
EV_INFO << "Timer expired, sending vehicleStatus\n";
delete msg;
cMessage *status = new cMessage("vehicleStatus");
send(status, "out");
}
else {
messagesReceived++;
EV_INFO << "Received " << msg->getName()
<< " at " << simTime() << "\n";
if (!par("initiator").boolValue()) {
msg->setName("ack");
double replyDelay = par("replyDelay").doubleValueInUnit("s");
sendDelayed(msg, replyDelay, "out");
}
else {
delete msg;
}
}
}
void VehicleNode::finish()
{
recordScalar("messagesReceived", messagesReceived);
}
Define_Module(VehicleNode) 會把 C++ Class 註冊給 OMNeT++。
若漏掉這一行,NED 雖然能看見 VehicleNode Type,Runtime 卻找不到負責執行它的 C++ Class。
程式中的 Message Ownership 也要留意:
scheduleAt() 後,Timer 先交給 Simulation Kernelsend() 或 sendDelayed() 後,Message 不再屬於目前 Module今天的 carB 直接把收到的 Message 改名為 ack 後送回,carA 收到後負責刪除。
在 omnetpp.ini 填入:
[General]
network = Day15Network
sim-time-limit = 2s
record-eventlog = true
network 指定頂層 Model,sim-time-limit 替實驗設定最晚停止時間,record-eventlog 則讓今天的短實驗保留事件紀錄。
這個模型在 carA 收到 ack 後已沒有下一筆事件,所以會在 1.040 s 正常結束,不會為了湊到時間上限而空跑到 2 s。
正式的大型 Run 不一定要開啟 Eventlog,應依分析需求決定。
在 IDE 執行:
Project
→ Build Project
Build 成功後,在 Project Explorer 選取 omnetpp.ini,再執行:
Run As
→ OMNeT++ Simulation
若 Run Configuration 要求選擇 User Interface,使用 Qtenv。
Qtenv 應該會顯示 carA 與 carB,中間有兩條方向相反的 Connection。
使用 Single Step 逐事件執行,觀察 Simulation Time:
| 預期時間 | 事件 |
|---|---|
1.000 s |
sendStatusTimer 到達 carA,送出 vehicleStatus |
1.010 s |
vehicleStatus 經 10 ms Delay 到達 carB |
1.040 s |
ack 經 20 ms Reply Delay 與 10 ms Connection Delay 到達 carA |
如果只看到時間跳到 1.000 s,卻沒有訊息繼續移動,依序檢查:
carA 的 initiator 是否為 true
Define_Module(VehicleNode) 是否存在send(..., "out") 一致Day15Network
模擬正常結束後,Project 的 results/ 目錄通常會出現 Scalar 與 Eventlog 檔案,實際檔名會依 Configuration 與 Run Number 決定。
Scalar 中的預期結果是:
| Module | messagesReceived |
|---|---|
carA |
1 |
carB |
1 |
carA 收到一筆 ack,carB 收到一筆 vehicleStatus。
Self-message 由 isSelfMessage() 分支處理,沒有列入這個收件計數。
接著在 IDE 開啟 Eventlog 或 Sequence Chart,確認:
carA Timer
→ carA 傳送 vehicleStatus
→ carB 接收並延遲回覆
→ carA 接收 ack
今天的完成標準不是動畫「看起來有一個點在移動」,而是 NED、C++、Configuration、Event Time 與 Scalar Result 能互相對上。
第一個模型完成後,要主動列出它沒有涵蓋的部分:
因此它只能證明 OMNeT++ 的基本事件與 Message Flow 已經接通。
研究結論必須和 Model Fidelity 對齊,不能從一條固定延遲 Connection 推論真實 V2X 在城市路口的可靠度。
這一關只做觀察,不加入攻擊行為。
| 項目 | 本篇任務 |
|---|---|
| 主線概念 | 找出 Timer、Message Arrival 與 Reply Delay 對應的三個事件 |
| 隔離工具 | OMNeT++ Qtenv 與兩個抽象 Module,不連接真實車輛或無線網路 |
| 輸出證據 | NED/C++/INI、Eventlog、三個事件時間與 messagesReceived Scalar |
把這份結果保存為 Day 15 Baseline。
Day 16 接上 SUMO 後,才能比較「固定 Module」與「隨交通場景建立並移動的 Vehicle Node」有何差異。
安全與法律提醒: 本篇只操作本機離散事件模擬,不會對道路車輛、RSU、行動網路或公共基礎設施傳送任何資料。
模擬器能讓攻擊情境更安全且可重現,但它不會自動讓研究結論可信。
後續 V2X 資安實驗至少要保存:
.msg、omnetpp.ini 與 SUMO Scenario如果攻擊組同時更換 Channel Model、車流、Random Seed 與 Message Rate,就很難知道結果究竟由哪一項造成。
這和 Day 14 比較正常車流與兩倍車流的原則相同:先固定其他條件,再改變要研究的變因。
還要保留 Day 13 建立的 Ground Truth 邊界:
SUMO 真實模擬位置
≠
V2X Message 對外宣告的位置
Day 21 與 Day 30 的假位置情境,正是要讓兩者產生可控制的差異,再觀察接收端與交通行為受到什麼影響。
omnetpp.ini 定義本次實驗今天讓兩個固定的抽象 Module 完成第一次 Message Exchange,也看懂 Simulation Kernel 如何推進事件時間。
Day 16 將把 SUMO 與 OMNeT++ 接起來,認識 TraCI 如何同步交通模擬與網路模擬,並讓 SUMO 中的 Vehicle 變成會移動的 OMNeT++ Node。