#include "ns3/core-module.h"
#include "ns3/network-module.h"
#include "ns3/mobility-module.h"
#include "ns3/internet-module.h"
#include "ns3/applications-module.h"
#include "ns3/antenna-module.h"
#include "ns3/nr-module.h"
#include "ns3/flow-monitor-module.h"
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#include <cerrno>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <fstream>
#include <vector>
#include <algorithm>
#include <iostream>
#include <limits>
#include <sstream>
#include <string>
using namespace ns3;
NS_LOG_COMPONENT_DEFINE("Ns3A3Handover");
struct ConfigData {
uint16_t sourceCell=1, targetCell=2, numerology=1;
double a3Offset=3, hysteresis=1, effectiveMargin=4, speedKmh=90;
double movementStart=2, simTime=10, udpStart=1;
double frequency=3.5e9, bandwidth=20e6, gnbTxPower=30;
double packetInterval=0.001;
uint32_t tttMs=320, packetSize=1000;
std::string apiIp="127.0.0.1";
uint16_t apiPort=8080;
std::string outcomePath="/api/v1/a3-handover-output-kpis";
std::string livePath="/api/v1/a3-handover-live-kpis";
bool enableHttpPost=true;
};
static ConfigData cfg;
struct Measurement { bool valid=false; double rsrp=0, rsrq=0, time=-1; };
static Measurement meas[3];
static Ptr<MobilityModel> ueMobility;
static uint64_t imsi=0;
static uint16_t rnti=0, servingCell=0, hoSource=0, hoTarget=0;
static bool ueAttached=false, gnbConnected=false, hoStarted=false, hoCompleted=false;
static bool servingRsrpValid=false, targetRsrpValid=false;
static double attachTime=-1, hoStart=-1, hoEnd=-1, hoX=0;
static double servingRsrp=0, targetRsrp=0, rsrpDelta=0;
static double sinrSum=0, mcsSum=0;
static uint64_t sinrSamples=0, mcsSamples=0, harqAck=0, harqNack=0;
static uint32_t successfulHos=0, periodicLivePosts=0, a3TriggerLivePosts=0;
struct UdpData {
Ptr<FlowMonitor> monitor;
Ptr<Ipv4FlowClassifier> classifier;
Ipv4Address ueIp;
uint16_t port=1234;
uint64_t previousTx=0, previousRx=0, previousBytes=0;
double previousTime=1;
};
static UdpData udp;
// Positions use command-line strings in X,Y,Z meters format.
static std::string sourcePosition="0,0,10";
static std::string targetPosition="200,0,10";
static std::string uePosition="20,0,1.5";
// constant = original straight-line model; sumo = replay time,x,y,z CSV.
static std::string mobilitySource="constant";
static std::string sumoTraceFile="mobility/applied_offset_trajectory.csv";
struct SumoSample { double time; Vector position; };
static std::vector<SumoSample> sumoSamples;
static std::vector<SumoSample> LoadSumoCsv(const std::string& file) {
std::ifstream in(file);
NS_ABORT_MSG_IF(!in.is_open(), "Cannot open SUMO trajectory CSV: " << file);
std::string line;
NS_ABORT_MSG_IF(!std::getline(in,line), "Empty SUMO CSV: " << file);
if (!line.empty() && line.back()=='\r') line.pop_back();
NS_ABORT_MSG_IF(line!="time,x,y,z", "SUMO CSV must have header time,x,y,z");
std::vector<SumoSample> result;
unsigned lineNo=1;
while (std::getline(in,line)) {
++lineNo;
if (!line.empty() && line.back()=='\r') line.pop_back();
if (line.empty()) continue;
std::istringstream row(line);
std::string field;
double v[4];
for (unsigned i=0;i<4;++i) {
NS_ABORT_MSG_IF(!std::getline(row,field,','),
"Missing column in SUMO CSV line " << lineNo);
std::istringstream num(field);
num >> std::ws;
NS_ABORT_MSG_IF(!(num >> v[i]) || !std::isfinite(v[i]),
"Invalid numeric value in SUMO CSV line " << lineNo);
num >> std::ws;
NS_ABORT_MSG_IF(!num.eof(), "Invalid numeric field in SUMO CSV line " << lineNo);
}
NS_ABORT_MSG_IF(std::getline(row,field,','),
"Too many columns in SUMO CSV line " << lineNo);
NS_ABORT_MSG_IF(v[0]<0 || (!result.empty() && v[0]<=result.back().time),
"SUMO CSV times must be nonnegative and strictly increasing");
result.push_back({v[0],Vector(v[1],v[2],v[3])});
}
NS_ABORT_MSG_IF(result.size()<2, "SUMO CSV needs at least two samples");
NS_ABORT_MSG_IF(std::abs(result.front().time)>1e-9,
"SUMO CSV first timestamp must be 0.0 s");
return result;
}
static Vector ParsePosition(const std::string& label,const std::string& input) {
std::istringstream ss(input);
double x=0,y=0,z=0;
char comma1=0,comma2=0;
ss>>std::ws;
bool valid=static_cast<bool>(ss>>x>>comma1>>y>>comma2>>z);
ss>>std::ws;
NS_ABORT_MSG_IF(!valid || !ss.eof() || comma1!=',' || comma2!=',' ||
!std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z),
"Invalid " << label << ": expected X,Y,Z (meters)");
return Vector(x,y,z);
}
static std::string BandName(double hz) {
return hz>=3.3e9&&hz<=3.8e9?"n78":"Not mapped";
}
static void ValidateConfig() {
cfg.effectiveMargin=cfg.a3Offset+cfg.hysteresis;
NS_ABORT_MSG_IF(mobilitySource!="constant" && mobilitySource!="sumo",
"mobilitySource must be constant or sumo");
NS_ABORT_MSG_IF(cfg.sourceCell!=1 || cfg.targetCell!=2,
"This two-gNB scenario supports sourceCellId=1 and targetCellId=2 only");
NS_ABORT_MSG_IF(!std::isfinite(cfg.a3Offset) || !std::isfinite(cfg.hysteresis) ||
!std::isfinite(cfg.speedKmh) || !std::isfinite(cfg.movementStart) ||
!std::isfinite(cfg.simTime) || cfg.a3Offset<0 || cfg.hysteresis<0 ||
cfg.effectiveMargin>15 ||
std::abs(2*cfg.effectiveMargin-std::round(2*cfg.effectiveMargin))>1e-6 ||
(mobilitySource=="constant" && cfg.speedKmh<=0) || cfg.movementStart<0 ||
cfg.simTime<=cfg.movementStart || cfg.simTime<=cfg.udpStart,
"Invalid handover or simulation parameters");
bool supportedTtt=cfg.tttMs==0 || cfg.tttMs==40 || cfg.tttMs==64 ||
cfg.tttMs==80 || cfg.tttMs==100 || cfg.tttMs==128 || cfg.tttMs==160 ||
cfg.tttMs==256 || cfg.tttMs==320 || cfg.tttMs==480 || cfg.tttMs==512 ||
cfg.tttMs==640 || cfg.tttMs==1024 || cfg.tttMs==1280 ||
cfg.tttMs==2560 || cfg.tttMs==5120;
NS_ABORT_MSG_IF(!supportedTtt,"Unsupported RRC Time-to-Trigger (ms)");
std::cout<<"\n========== EVENT A3 COMMAND-LINE CONFIGURATION ==========\n"
<<"Source Cell ID : "<<cfg.sourceCell<<"\n"
<<"Target Cell ID : "<<cfg.targetCell<<"\n"
<<"A3 Offset : "<<cfg.a3Offset<<" dB\n"
<<"Hysteresis : "<<cfg.hysteresis<<" dB\n"
<<"Effective A3 Margin : "<<cfg.effectiveMargin<<" dB\n"
<<"TTT : "<<cfg.tttMs<<" ms\n"
<<"UE Speed (constant) : "<<cfg.speedKmh<<" km/h\n"
<<"Mobility Source : "<<mobilitySource<<"\n"
<<"SUMO CSV (if used) : "<<sumoTraceFile<<"\n"
<<"Mobility Start : "<<cfg.movementStart<<" s\n"
<<"Simulation Time : "<<cfg.simTime<<" s\n"
<<"Source gNB Position : "<<sourcePosition<<" m\n"
<<"Target gNB Position : "<<targetPosition<<" m\n"
<<"UE Start Position : "<<uePosition<<" m\n"
<<"=========================================================\n";
}
static double Missing() { return std::numeric_limits<double>::quiet_NaN(); }
static std::string JsonNumber1(double v) {
if(!std::isfinite(v)) return "null";
std::ostringstream s; s<<std::fixed<<std::setprecision(1)<<v; return s.str();
}
static std::string JsonKpi(double v,const std::string& unit) {
return "{\"value\": "+JsonNumber1(v)+", \"unit\": \""+unit+"\"}";
}
static std::string JsonId(uint16_t v,bool valid) {
return "{\"value\": "+(valid?JsonNumber1(v):"null")+", \"unit\": null}";
}
static std::string BuildOutcomeJson(bool final) {
std::ostringstream j;
j<<"{\n \"a3_handover_output_kpis\": ";
if(!final) j<<"{},\n";
else {
j<<"{\n"
<<" \"effective_a3_margin\": "<<JsonKpi(cfg.effectiveMargin,"dB")<<",\n"
<<" \"ho_start_time\": "<<JsonKpi(hoStarted?hoStart:Missing(),"s")<<",\n"
<<" \"ue_position_at_ho\": "<<JsonKpi(hoStarted?hoX:Missing(),"m")<<",\n"
<<" \"serving_rsrp_at_ho\": "<<JsonKpi(hoStarted&&servingRsrpValid?servingRsrp:Missing(),"dBm")<<",\n"
<<" \"target_rsrp_at_ho\": "<<JsonKpi(hoStarted&&targetRsrpValid?targetRsrp:Missing(),"dBm")<<",\n"
<<" \"rsrp_delta_at_ho\": "<<JsonKpi(hoStarted&&servingRsrpValid&&targetRsrpValid?rsrpDelta:Missing(),"dB")
<<"\n },\n";
}
j<<" \"radio_configuration_kpis\": {\n"
<<" \"frequency\": "<<JsonKpi(cfg.frequency/1e9,"GHz")<<",\n"
<<" \"band\": {\"value\": \""<<BandName(cfg.frequency)<<"\", \"unit\": null},\n"
<<" \"bandwidth\": "<<JsonKpi(cfg.bandwidth/1e6,"MHz")<<",\n"
<<" \"subcarrier_spacing\": "<<JsonKpi(15u*(1u<<cfg.numerology),"kHz")<<",\n"
<<" \"a3_offset\": "<<JsonKpi(cfg.a3Offset,"dB")<<"\n }\n}\n";
return j.str();
}
static std::string BuildLiveJson() {
bool hasServing=servingCell==1||servingCell==2;
uint16_t neighbor=hasServing?(servingCell==1?2:1):0;
bool sv=hasServing&&meas[servingCell].valid;
bool nv=hasServing&&meas[neighbor].valid;
double delta=sv&&nv?meas[neighbor].rsrp-meas[servingCell].rsrp:Missing();
std::ostringstream j;
j<<"{\n \"a3_handover_live_kpis\": {\n"
<<" \"serving_cell_id\": "<<JsonId(servingCell,hasServing)<<",\n"
<<" \"serving_cell_rsrp\": "<<JsonKpi(sv?meas[servingCell].rsrp:Missing(),"dBm")<<",\n"
<<" \"neighbor_cell_id\": "<<JsonId(neighbor,hasServing)<<",\n"
<<" \"neighbor_cell_rsrp\": "<<JsonKpi(nv?meas[neighbor].rsrp:Missing(),"dBm")<<",\n"
<<" \"rsrp_delta\": "<<JsonKpi(delta,"dB")<<",\n"
<<" \"ho_trigger_condition\": "<<JsonKpi(cfg.effectiveMargin,"dB")<<"\n }\n}\n";
return j.str();
}
static bool HttpPostJson(const std::string& path,const std::string& body) {
if(path.empty()||path[0]!='/'||path.find('\r')!=std::string::npos||path.find('\n')!=std::string::npos) {
std::cerr<<"[HTTP ERROR] Invalid URL path\n"; return false;
}
int fd=::socket(AF_INET,SOCK_STREAM,0);
if(fd<0) { std::cerr<<"[HTTP ERROR] socket: "<<std::strerror(errno)<<"\n"; return false; }
timeval timeout{2,0};
::setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,&timeout,sizeof(timeout));
::setsockopt(fd,SOL_SOCKET,SO_SNDTIMEO,&timeout,sizeof(timeout));
sockaddr_in addr{}; addr.sin_family=AF_INET; addr.sin_port=htons(cfg.apiPort);
if(::inet_pton(AF_INET,cfg.apiIp.c_str(),&addr.sin_addr)!=1) {
std::cerr<<"[HTTP ERROR] Invalid IPv4 address\n"; ::close(fd); return false;
}
if(::connect(fd,reinterpret_cast<sockaddr*>(&addr),sizeof(addr))<0) {
std::cerr<<"[HTTP ERROR] connect: "<<std::strerror(errno)<<"\n"; ::close(fd); return false;
}
std::ostringstream req;
req<<"POST "<<path<<" HTTP/1.1\r\nHost: "<<cfg.apiIp<<":"<<cfg.apiPort
<<"\r\nContent-Type: application/json\r\nAccept: application/json\r\nContent-Length: "
<<body.size()<<"\r\nConnection: close\r\n\r\n"<<body;
std::string data=req.str(); size_t offset=0;
while(offset<data.size()) {
ssize_t sent=::send(fd,data.data()+offset,data.size()-offset,MSG_NOSIGNAL);
if(sent<=0) { std::cerr<<"[HTTP ERROR] send failed\n"; ::close(fd); return false; }
offset+=static_cast<size_t>(sent);
}
std::string response; char buffer[2048];
while(response.size()<65536&&response.find("\r\n\r\n")==std::string::npos) {
ssize_t n=::recv(fd,buffer,sizeof(buffer),0);
if(n<=0) break;
response.append(buffer,static_cast<size_t>(n));
}
::close(fd);
std::istringstream status(response); std::string protocol; int code=0;
status>>protocol>>code;
std::cout<<"[HTTP] "<<path<<" status="<<code<<(code>=200&&code<300?" SUCCESS":" FAILED")<<"\n";
return code>=200&&code<300;
}
static void Publish(const std::string& label,const std::string& path,const std::string& json) {
std::cout<<"\n========== "<<label<<" JSON PAYLOAD ==========\n"<<json
<<"========================================\n";
if(cfg.enableHttpPost) {
std::cout<<"[HTTP] POST http://"<<cfg.apiIp<<":"<<cfg.apiPort<<path<<"\n";
HttpPostJson(path,json);
} else std::cout<<"[HTTP] Publishing disabled\n";
}
static void PublishA3TriggerLiveKpis() {
++a3TriggerLivePosts;
std::cout<<std::fixed<<std::setprecision(6)
<<"\n========== A3 TRIGGER LIVE KPI EVENT ==========\n"
<<"Simulation Time : "<<Simulator::Now().GetSeconds()<<" s\n"
<<"Source Cell ID : "<<hoSource<<"\nTarget Cell ID : "<<hoTarget
<<"\nA3 Offset : "<<cfg.a3Offset<<" dB\nHysteresis : "<<cfg.hysteresis
<<" dB\nTTT : "<<cfg.tttMs<<" ms\n===============================================\n";
Publish("A3 TRIGGER LIVE KPIS",cfg.livePath,BuildLiveJson());
}
static void StartMobility(Vector v) {
DynamicCast<ConstantVelocityMobilityModel>(ueMobility)->SetVelocity(v);
std::cout<<"\n[MOBILITY START] Time="<<Simulator::Now().GetSeconds()<<" s, Speed="<<cfg.speedKmh<<" km/h\n";
}
static void UeMeasurement(uint16_t rr,uint16_t cell,double rsrp,double rsrq,bool serving,uint8_t cc) {
if(cell<1||cell>2||!std::isfinite(rsrp)) return;
meas[cell]={true,rsrp,rsrq,Simulator::Now().GetSeconds()};
}
static void UeSinr(uint16_t cell,uint16_t rr,double sinr,uint16_t bwp) {
if(std::isfinite(sinr)&&sinr>0) { sinrSum+=sinr; ++sinrSamples; }
}
static void DlScheduling(NrSchedulingCallbackInfo info) {
if(rnti&&info.m_rnti==rnti) { mcsSum+=info.m_mcs; ++mcsSamples; }
}
static void DlHarq(const DlHarqInfo& info) {
if(info.m_rnti!=rnti) return;
if(info.m_harqStatus==DlHarqInfo::ACK) ++harqAck;
else if(info.m_harqStatus==DlHarqInfo::NACK) ++harqNack;
}
static void UeConnected(std::string context,uint64_t id,uint16_t cell,uint16_t rr) {
imsi=id; servingCell=cell; rnti=rr; ueAttached=true; attachTime=Simulator::Now().GetSeconds();
std::cout<<"\n[UE ATTACH] Time="<<attachTime<<" s, IMSI="<<imsi<<", Cell="<<cell<<", RNTI="<<rr<<"\n";
}
static void GnbConnected(std::string context,uint64_t id,uint16_t cell,uint16_t rr) {
if(cell==cfg.sourceCell) gnbConnected=true;
std::cout<<"[gNB RRC] Time="<<Simulator::Now().GetSeconds()<<" s, Cell="<<cell<<", RNTI="<<rr<<"\n";
}
static bool GetDlFlow(FlowMonitor::FlowStats& out) {
if(!udp.monitor||!udp.classifier) return false;
for(const auto& entry:udp.monitor->GetFlowStats()) {
auto tuple=udp.classifier->FindFlow(entry.first);
if(tuple.protocol==17&&tuple.destinationAddress==udp.ueIp&&tuple.destinationPort==udp.port) {
out=entry.second; return true;
}
}
return false;
}
static void PrintA3() {
if(servingCell!=1&&servingCell!=2) return;
uint16_t neighbor=servingCell==1?2:1;
bool both=meas[servingCell].valid&&meas[neighbor].valid;
double delta=both?meas[neighbor].rsrp-meas[servingCell].rsrp:Missing();
bool fresh=both&&Simulator::Now().GetSeconds()-meas[servingCell].time<=0.5&&
Simulator::Now().GetSeconds()-meas[neighbor].time<=0.5;
std::cout<<std::fixed<<std::setprecision(3)<<"[A3] Time="<<Simulator::Now().GetSeconds()
<<" s, Serving Cell ID ="<<servingCell<<", Neighbor Cell ID ="<<neighbor
<<", Serving Cell RSRP=";
if(meas[servingCell].valid) std::cout<<meas[servingCell].rsrp; else std::cout<<"N/A";
std::cout<<" dBm, Neighbor Cell RSRP=";
if(meas[neighbor].valid) std::cout<<meas[neighbor].rsrp; else std::cout<<"N/A";
std::cout<<" dBm, RSRP Delta=";
if(both) std::cout<<delta; else std::cout<<"N/A";
std::cout<<" dB, HO Trigger Condition (Offset+Hys)="<<cfg.effectiveMargin<<" dB, Entry="
<<(!both?"N/A":!fresh?"STALE":delta>cfg.effectiveMargin?"MET":"NOT MET")<<"\n";
}
static void GnbHoStart(std::string context,uint64_t id,uint16_t cell,uint16_t rr,uint16_t target) {
if(hoStarted) return;
hoStarted=true; hoStart=Simulator::Now().GetSeconds(); hoSource=cell; hoTarget=target;
hoX=ueMobility->GetPosition().x;
if(cell>=1&&cell<=2&&meas[cell].valid) { servingRsrpValid=true; servingRsrp=meas[cell].rsrp; }
if(target>=1&&target<=2&&meas[target].valid) { targetRsrpValid=true; targetRsrp=meas[target].rsrp; }
if(servingRsrpValid&&targetRsrpValid) rsrpDelta=targetRsrp-servingRsrp;
std::cout<<std::fixed<<std::setprecision(6)<<"\n========== A3 HANDOVER START ==========\n"
<<"Time: "<<hoStart<<" s\nSource Cell: "<<cell<<"\nTarget Cell: "<<target
<<"\nUE X: "<<hoX<<" m\nA3 Offset: "<<cfg.a3Offset
<<" dB\nEffective Margin: "<<cfg.effectiveMargin<<" dB\nTTT: "<<cfg.tttMs<<" ms\n";
PrintA3();
// Event-driven publication in addition to one-second reports.
PublishA3TriggerLiveKpis();
}
static void UeHoStart(std::string context,uint64_t id,uint16_t cell,uint16_t rr,uint16_t target) {
std::cout<<"[UE RRC HO START] "<<Simulator::Now().GetSeconds()<<" s, "<<cell<<" -> "<<target<<"\n";
}
static void UeHoEnd(std::string context,uint64_t id,uint16_t cell,uint16_t rr) {
hoCompleted=true; hoEnd=Simulator::Now().GetSeconds(); servingCell=cell; rnti=rr; ++successfulHos;
std::cout<<"\n========== A3 HANDOVER COMPLETE ==========\nTime: "<<hoEnd
<<" s\nServing Cell: "<<cell<<"\nHO Duration: "<<(hoEnd-hoStart)*1000
<<" ms\nStatus: SUCCESS\n";
}
static void GnbHoEnd(std::string context,uint64_t id,uint16_t cell,uint16_t rr) {
std::cout<<"[gNB RRC HO COMPLETE] "<<Simulator::Now().GetSeconds()<<" s, Cell="<<cell<<"\n";
}
static void PeriodicReport() {
double now=Simulator::Now().GetSeconds(); Vector p=ueMobility->GetPosition();
std::cout<<std::fixed<<std::setprecision(3)<<"\n[MOBILITY] Time="<<now
<<" s, UE Position=("<<p.x<<", "<<p.y<<", "<<p.z
<<") m, Serving Cell="<<servingCell<<"\n";
PrintA3();
std::cout<<"[RADIO] SINR=";
if(sinrSamples) std::cout<<10*std::log10(sinrSum/sinrSamples)<<" dB"; else std::cout<<"N/A";
std::cout<<", MCS=";
if(mcsSamples) std::cout<<mcsSum/mcsSamples; else std::cout<<"N/A";
std::cout<<", HARQ ACK="<<harqAck<<", NACK="<<harqNack<<"\n";
FlowMonitor::FlowStats stats;
if(GetDlFlow(stats)) {
double interval=now-udp.previousTime;
double throughput=interval>0?(stats.rxBytes-udp.previousBytes)*8.0/(interval*1e6):0;
std::cout<<"[UDP] Time="<<now<<" s, Tx Pkts="<<stats.txPackets-udp.previousTx
<<", Rx Pkts="<<stats.rxPackets-udp.previousRx<<", Throughput="<<throughput
<<" Mbps, Cell="<<servingCell<<"\n";
udp.previousTx=stats.txPackets; udp.previousRx=stats.rxPackets; udp.previousBytes=stats.rxBytes;
}
udp.previousTime=now; sinrSum=0; mcsSum=0; sinrSamples=0; mcsSamples=0; harqAck=0; harqNack=0;
}
static void PublishLiveKpis() {
double now=Simulator::Now().GetSeconds(); ++periodicLivePosts;
std::cout<<"\n[PERIODIC LIVE KPI] Simulation Time: "<<std::fixed<<std::setprecision(3)<<now<<" s\n";
Publish("PERIODIC A3 HANDOVER LIVE KPIS",cfg.livePath,BuildLiveJson());
if(now+1.0<cfg.simTime-1e-9) Simulator::Schedule(Seconds(1.0),&PublishLiveKpis);
}
int main(int argc,char* argv[]) {
CommandLine cmd(__FILE__);
cmd.AddValue("sourceCellId","Source cell ID (default 1)",cfg.sourceCell);
cmd.AddValue("targetCellId","Target cell ID (default 2)",cfg.targetCell);
cmd.AddValue("a3Offset","A3 offset in dB (default 3)",cfg.a3Offset);
cmd.AddValue("hysteresis","A3 hysteresis in dB (default 1)",cfg.hysteresis);
cmd.AddValue("ttt","Time-to-trigger in ms (default 320)",cfg.tttMs);
cmd.AddValue("sourceCellPos","Source gNB X,Y,Z meters (default 0,0,10)",sourcePosition);
cmd.AddValue("targetCellPos","Target gNB X,Y,Z meters (default 200,0,10)",targetPosition);
cmd.AddValue("ueStartPos","UE initial X,Y,Z meters (default 20,0,1.5)",uePosition);
cmd.AddValue("mobilityTime","UE mobility start time in seconds (default 2)",cfg.movementStart);
cmd.AddValue("simulationTime","Simulation duration in seconds (default 10)",cfg.simTime);
cmd.AddValue("speedKmh","UE speed in km/h for constant mode (default 90)",cfg.speedKmh);
cmd.AddValue("mobilitySource","constant or sumo (default constant)",mobilitySource);
cmd.AddValue("sumoTraceFile","CSV path with time,x,y,z for sumo mode",sumoTraceFile);
cmd.AddValue("apiIp","HTTP API IPv4 address",cfg.apiIp);
cmd.AddValue("apiPort","HTTP API TCP port",cfg.apiPort);
cmd.AddValue("enableHttpPost","Enable host-side HTTP publishing",cfg.enableHttpPost);
cmd.Parse(argc,argv);
ValidateConfig();
Vector p1=ParsePosition("sourceCellPos",sourcePosition);
Vector p2=ParsePosition("targetCellPos",targetPosition);
Vector pu=ParsePosition("ueStartPos",uePosition);
if (mobilitySource=="sumo") {
sumoSamples=LoadSumoCsv(sumoTraceFile);
pu=sumoSamples.front().position;
std::cout<<"[SUMO] Loaded "<<sumoSamples.size()<<" samples from "<<sumoTraceFile
<<"; first position=("<<pu.x<<","<<pu.y<<","<<pu.z<<") m"
<<"; last CSV time="<<sumoSamples.back().time<<" s\n";
NS_ABORT_MSG_IF(cfg.simTime<=cfg.movementStart,
"Simulation must extend beyond SUMO mobility start");
if (cfg.simTime<cfg.movementStart+sumoSamples.back().time)
std::cout<<"[SUMO] Simulation ends before CSV route finishes; route will be truncated.\n";
}
double dx=p2.x-pu.x,dy=p2.y-pu.y,distance=std::hypot(dx,dy);
NS_ABORT_MSG_IF(mobilitySource=="constant" && distance<=0,
"UE must not start at target gNB");
double speed=cfg.speedKmh/3.6;
Vector velocity(0,0,0);
if (mobilitySource=="constant")
velocity=Vector(speed*dx/distance,speed*dy/distance,0);
NodeContainer gnbs,ues; gnbs.Create(2); ues.Create(1);
MobilityHelper gm;
Ptr<ListPositionAllocator> gp=CreateObject<ListPositionAllocator>(); gp->Add(p1); gp->Add(p2);
gm.SetPositionAllocator(gp); gm.SetMobilityModel("ns3::ConstantPositionMobilityModel"); gm.Install(gnbs);
MobilityHelper um;
Ptr<ListPositionAllocator> up=CreateObject<ListPositionAllocator>(); up->Add(pu);
um.SetPositionAllocator(up);
um.SetMobilityModel(mobilitySource=="sumo" ? "ns3::WaypointMobilityModel"
: "ns3::ConstantVelocityMobilityModel");
um.Install(ues);
ueMobility=ues.Get(0)->GetObject<MobilityModel>();
if (mobilitySource=="constant") {
DynamicCast<ConstantVelocityMobilityModel>(ueMobility)->SetVelocity(Vector(0,0,0));
} else {
Ptr<WaypointMobilityModel> waypoint=DynamicCast<WaypointMobilityModel>(ueMobility);
NS_ABORT_MSG_IF(!waypoint,"SUMO mode requires WaypointMobilityModel");
// Hold at first position until mobilityTime, then replay SUMO time offsets.
waypoint->AddWaypoint(Waypoint(Seconds(0),pu));
for (const auto& sample:sumoSamples) {
double when=cfg.movementStart+sample.time;
if (when>0 && when<=cfg.simTime)
waypoint->AddWaypoint(Waypoint(Seconds(when),sample.position));
}
std::cout<<"[SUMO] Waypoint mobility configured, time offset="
<<cfg.movementStart<<" s; UE speed comes from CSV positions.\n";
}
Ptr<NrPointToPointEpcHelper> epc=CreateObject<NrPointToPointEpcHelper>();
Ptr<IdealBeamformingHelper> beam=CreateObject<IdealBeamformingHelper>();
Ptr<NrHelper> nr=CreateObject<NrHelper>();
nr->SetEpcHelper(epc); nr->SetBeamformingHelper(beam);
nr->SetAttribute("UseIdealRrc",BooleanValue(false));
nr->SetSchedulerTypeId(TypeId::LookupByName("ns3::NrMacSchedulerTdmaRR"));
beam->SetAttribute("BeamformingMethod",TypeIdValue(DirectPathBeamforming::GetTypeId()));
nr->SetHandoverAlgorithmType("ns3::NrA3RsrpHandoverAlgorithm");
nr->SetHandoverAlgorithmAttribute("Hysteresis",DoubleValue(cfg.effectiveMargin));
nr->SetHandoverAlgorithmAttribute("TimeToTrigger",TimeValue(MilliSeconds(cfg.tttMs)));
CcBwpCreator creator;
CcBwpCreator::SimpleOperationBandConf bandConf(cfg.frequency,cfg.bandwidth,1);
OperationBandInfo band=creator.CreateOperationBandContiguousCc(bandConf);
Ptr<NrChannelHelper> channel=CreateObject<NrChannelHelper>();
channel->ConfigureFactories("UMi","LOS","ThreeGpp");
channel->SetPathlossAttribute("ShadowingEnabled",BooleanValue(false));
channel->AssignChannelsToBands({band});
BandwidthPartInfoPtrVector bwps=CcBwpCreator::GetAllBwps({band});
nr->SetGnbAntennaAttribute("NumRows",UintegerValue(4));
nr->SetGnbAntennaAttribute("NumColumns",UintegerValue(8));
nr->SetGnbAntennaAttribute("AntennaElement",PointerValue(CreateObject<IsotropicAntennaModel>()));
nr->SetUeAntennaAttribute("NumRows",UintegerValue(2));
nr->SetUeAntennaAttribute("NumColumns",UintegerValue(4));
nr->SetUeAntennaAttribute("AntennaElement",PointerValue(CreateObject<IsotropicAntennaModel>()));
NetDeviceContainer gdev=nr->InstallGnbDevice(gnbs,bwps);
NetDeviceContainer udev=nr->InstallUeDevice(ues,bwps);
for(uint32_t i=0;i<2;++i) {
Ptr<NrGnbPhy> phy=NrHelper::GetGnbPhy(gdev.Get(i),0);
phy->SetAttribute("Numerology",UintegerValue(cfg.numerology));
phy->SetAttribute("TxPower",DoubleValue(cfg.gnbTxPower));
}
Ptr<NrGnbNetDevice> g1=DynamicCast<NrGnbNetDevice>(gdev.Get(0));
Ptr<NrGnbNetDevice> g2=DynamicCast<NrGnbNetDevice>(gdev.Get(1));
NS_ABORT_MSG_IF(g1->GetCellId()!=1||g2->GetCellId()!=2,"Unexpected cell IDs");
std::cout<<"\n========== NR CELL CONFIGURATION ==========\n"
<<"gNB 1 Cell ID : "<<g1->GetCellId()<<"\n"
<<"gNB 2 Cell ID : "<<g2->GetCellId()<<"\n"
<<"NR Band : "<<BandName(cfg.frequency)<<"\n"
<<"Carrier Frequency : "<<cfg.frequency/1e9<<" GHz\n"
<<"Bandwidth : "<<cfg.bandwidth/1e6<<" MHz\n"
<<"Numerology : "<<cfg.numerology<<"\n"
<<"Subcarrier Spacing : "<<15u*(1u<<cfg.numerology)<<" kHz\n"
<<"Channel Model : UMi LOS\nScheduler : TDMA Round Robin\n"
<<"RRC Protocol : REAL\n===========================================\n";
Publish("INITIAL RADIO CONFIGURATION",cfg.outcomePath,BuildOutcomeJson(false));
auto [remote,remoteIp]=epc->SetupRemoteHost("100Gb/s",2500,Seconds(0)); (void)remoteIp;
InternetStackHelper internet; internet.Install(ues);
Ipv4InterfaceContainer ip=epc->AssignUeIpv4Address(udev);
Ipv4StaticRoutingHelper routing;
routing.GetStaticRouting(ues.Get(0)->GetObject<Ipv4>())
->SetDefaultRoute(epc->GetUeDefaultGatewayAddress(),1);
udp.ueIp=ip.GetAddress(0); udp.previousTime=cfg.udpStart;
Config::Connect("/NodeList/*/DeviceList/*/NrUeRrc/ConnectionEstablished",MakeCallback(&UeConnected));
Config::Connect("/NodeList/*/DeviceList/*/NrGnbRrc/ConnectionEstablished",MakeCallback(&GnbConnected));
Config::Connect("/NodeList/*/DeviceList/*/NrUeRrc/HandoverStart",MakeCallback(&UeHoStart));
Config::Connect("/NodeList/*/DeviceList/*/NrUeRrc/HandoverEndOk",MakeCallback(&UeHoEnd));
Config::Connect("/NodeList/*/DeviceList/*/NrGnbRrc/HandoverStart",MakeCallback(&GnbHoStart));
Config::Connect("/NodeList/*/DeviceList/*/NrGnbRrc/HandoverEndOk",MakeCallback(&GnbHoEnd));
Ptr<NrUePhy> uePhy=NrHelper::GetUePhy(udev.Get(0),0);
NS_ABORT_MSG_IF(!uePhy->TraceConnectWithoutContext("ReportUeMeasurements",MakeCallback(&UeMeasurement)),"RSRP trace failed");
NS_ABORT_MSG_IF(!uePhy->TraceConnectWithoutContext("DlDataSinr",MakeCallback(&UeSinr)),"SINR trace failed");
for(uint32_t i=0;i<2;++i) {
Ptr<NrGnbMac> mac=NrHelper::GetGnbMac(gdev.Get(i),0);
NS_ABORT_MSG_IF(!mac->TraceConnectWithoutContext("DlScheduling",MakeCallback(&DlScheduling)),"Scheduling trace failed");
NS_ABORT_MSG_IF(!mac->TraceConnectWithoutContext("DlHarqFeedback",MakeCallback(&DlHarq)),"HARQ trace failed");
}
nr->AddX2Interface(gnbs);
nr->AttachToGnb(udev.Get(0),gdev.Get(0));
UdpServerHelper server(udp.port);
ApplicationContainer serverApps=server.Install(ues.Get(0));
UdpClientHelper client(udp.ueIp,udp.port);
client.SetAttribute("MaxPackets",UintegerValue(0xFFFFFFFF));
client.SetAttribute("PacketSize",UintegerValue(cfg.packetSize));
client.SetAttribute("Interval",TimeValue(Seconds(cfg.packetInterval)));
ApplicationContainer clientApps=client.Install(remote);
NrQosFlow qos(NrQosFlow::NGBR_LOW_LAT_EMBB);
Ptr<NrQosRule> rule=Create<NrQosRule>();
NrQosRule::PacketFilter filter; filter.localPortStart=udp.port; filter.localPortEnd=udp.port;
rule->Add(filter); nr->ActivateDedicatedQosFlow(udev.Get(0),qos,rule);
serverApps.Start(Seconds(0.5)); clientApps.Start(Seconds(cfg.udpStart));
serverApps.Stop(Seconds(cfg.simTime)); clientApps.Stop(Seconds(cfg.simTime));
FlowMonitorHelper flowHelper;
NodeContainer nodes; nodes.Add(remote); nodes.Add(ues);
udp.monitor=flowHelper.Install(nodes);
udp.classifier=DynamicCast<Ipv4FlowClassifier>(flowHelper.GetClassifier());
if (mobilitySource=="constant") {
Simulator::Schedule(Seconds(cfg.movementStart),&StartMobility,velocity);
} else {
std::cout<<"[SUMO] No constant-velocity start event; waypoints control movement.\n";
}
Simulator::Schedule(Seconds(cfg.movementStart),&PublishLiveKpis);
for(double t=cfg.udpStart+1;t<cfg.simTime-1e-9;t+=1)
Simulator::Schedule(Seconds(t),&PeriodicReport);
std::cout<<"\nStarting Event A3 handover simulation...\n";
Simulator::Stop(Seconds(cfg.simTime)); Simulator::Run(); PeriodicReport();
std::cout<<"\n========== FINAL HANDOVER SUMMARY ==========\n"
<<"Initial attachment: "<<(ueAttached&&gnbConnected?"SUCCESS":"NOT CONFIRMED")
<<"\nHO started: "<<(hoStarted?"YES":"NO")
<<"\nHO completed: "<<(hoCompleted?"YES":"NO")
<<"\nHO start time: "<<hoStart<<" s\nHO X: "<<(hoStarted?hoX:0)
<<" m\nFinal serving cell: "<<servingCell<<"\nSuccessful HOs: "<<successfulHos<<"\n";
if(hoStarted&&hoCompleted) std::cout<<"HO duration: "<<(hoEnd-hoStart)*1000<<" ms\n";
std::cout<<"\n========== LIVE KPI PUBLICATION SUMMARY ==========\n"
<<"Periodic live POST attempts : "<<periodicLivePosts
<<"\nA3 trigger POST attempts : "<<a3TriggerLivePosts
<<"\nTotal live POST attempts : "<<periodicLivePosts+a3TriggerLivePosts
<<"\n==================================================\n";
udp.monitor->CheckForLostPackets(); FlowMonitor::FlowStats stats;
if(GetDlFlow(stats)) {
double duration=cfg.simTime-cfg.udpStart;
double pdr=stats.txPackets?100.0*stats.rxPackets/stats.txPackets:0;
double throughput=stats.rxBytes*8.0/(duration*1e6);
std::cout<<"\n[UDP FINAL] Tx="<<stats.txPackets<<", Rx="<<stats.rxPackets
<<", PDR="<<pdr<<" %, Throughput="<<throughput<<" Mbps\n";
}
std::cout<<"[UDP SERVER] Received="<<DynamicCast<UdpServer>(serverApps.Get(0))->GetReceived()<<"\n";
udp.monitor->SerializeToXmlFile("nr-a3-handover-flowmon.xml",true,true);
std::string finalJson=BuildOutcomeJson(true);
Simulator::Destroy();
Publish("FINAL A3 HANDOVER OUTCOME",cfg.outcomePath,finalJson);
std::cout<<"Simulation completed.\n";
return 0;
}
To embed this project on your website, copy the following code and paste it into your website's HTML: