Middleware¶
CORNET's middleware layer bridges the NS-3 simulated network and the ROS 2 robot simulation. It provides four components: AoI Tracker, Clock Server, Packet Dispatcher, and TUN Manager.
1. Overview¶
NS-3 simulation ROS 2 / Gazebo
│ │
│ simulated packets │
▼ │
PacketDispatcher ──(on_dispatch)──► TUN interface
│ │
└──(physics time)──► ClockServer
│
AoITracker
(per-flow AoI CSV)
All middleware components run in the CORNET process. They have no ROS 2 imports and work at the Python level.
2. Clock Server¶
cornet.middleware.clock.ClockServer accepts physics-time ticks over a Unix
domain socket and notifies registered listeners.
from cornet.middleware.clock import ClockServer
def on_tick(t: float) -> None:
print(f"physics time: {t:.3f}s")
clock = ClockServer(
socket_path="/tmp/cornet_clock.sock",
clock_timeout_s=5.0,
on_tick=on_tick,
)
clock.start()
# ...
clock.stop()
The NS-3 scenario sends JSON messages over the socket:
{"t": 12.345}
If no client connects within clock_timeout_s seconds, the server falls back
to time.monotonic() advancing the clock at the real-time rate.
There is also a PositionServer for robot position updates:
{"name": "robot0", "x": 1.0, "y": 2.0, "z": 0.0}
3. Packet Dispatcher¶
cornet.middleware.dispatcher.PacketDispatcher implements a physics-time
priority queue. Packets enqueued with a simulated release timestamp are
dispatched via on_dispatch when wall_clock >= release_time / rtf.
from cornet.middleware.dispatcher import PacketDispatcher
def on_dispatch(flow_id: str, payload: bytes) -> None:
# forward payload to the TUN interface or application
print(f"dispatching {len(payload)} bytes for flow {flow_id}")
dispatcher = PacketDispatcher(
rtf=1.0, # real-time factor (1.0 = wall time matches sim time)
deadline_s=0.5, # discard packets that are >0.5 s late
ber=0.0, # bit-error rate (0 = lossless)
on_dispatch=on_dispatch,
)
dispatcher.start()
dispatcher.enqueue(flow_id="ue0->gnb0", payload=b"hello", physics_time=5.0)
# ...
dispatcher.stop()
BER modelling: When ber > 0, the dispatcher applies a geometric
distribution to each packet to simulate bit-level losses (stdlib only, no
numpy).
RTF = 0: Setting rtf=0 disables sleep and dispatches as fast as
possible (useful for non-real-time replay).
4. AoI Tracker¶
cornet.middleware.aoi.AoITracker tracks per-flow Age of Information (AoI):
$$\text{AoI}_f(t) = t - u_f$$
where $u_f$ is the physics timestamp of the last received update from flow $f$.
from cornet.middleware.aoi import AoITracker
tracker = AoITracker(
trace_path="/tmp/aoi_trace.csv",
sample_hz=1.0, # how often sample() records an AoI snapshot
)
# Called by the network plugin each time a status update is received:
tracker.record_update(flow_id="ue0->gnb0", physics_time=5.0)
# Called by the ClockServer on_tick callback:
tracker.sample(physics_time=5.1)
# At the end of the experiment:
stats = tracker.close()
# stats["ue0->gnb0"] = {"mean": 0.12, "max": 0.5, "p95": 0.3, ...}
The close() method writes the CSV trace and returns percentile statistics.
The EvalTool in tasks/pendulum_nr_control/eval/eval_tool.py reads these
statistics from analysis/aoi_statistics.json.
5. TUN Manager¶
cornet.middleware.tun.TunManager creates Linux TUN interfaces (one per node
IP) and configures policy routing so that NS-3 tap traffic is steered to the
correct interface.
from cornet.middleware.tun import TunManager
mgr = TunManager(ip_list=["10.0.0.1", "10.0.0.2"])
mgr.setup() # requires CAP_NET_ADMIN (root or ambient capability)
# ... run experiment ...
mgr.teardown() # idempotent — safe to call on error
Interface naming: cornet0, cornet1, … per the IP list order.
Routing tables: outbound table i+1, loopback table i+101 for each
interface i.
Teardown: teardown() is idempotent. Call it in a finally block to
ensure cleanup even on experiment failure.
6. Wiring the components¶
In a typical plugin, all four components are wired together:
# inside MyPlugin.start():
def on_tick(t):
self._tracker.sample(t)
self._clock = ClockServer(on_tick=on_tick)
self._dispatcher = PacketDispatcher(rtf=config.middleware.rtf, on_dispatch=self._forward)
self._tracker = AoITracker(trace_path=output_dir / "aoi.csv")
self._tun = TunManager(ip_list=list(context.network.node_ips.values()))
self._tun.setup()
self._clock.start()
self._dispatcher.start()
See cornet/middleware/ for the full implementation details.