Files
lora_route_py/sim/radio/channel.py

293 lines
9.4 KiB
Python

"""
Wireless Channel Model.
Implements:
- Broadcast propagation to all nodes in range
- Airtime occupation tracking
- Collision detection (time overlap + |RSSI1 - RSSI2| < 6 dB)
- Transmission statistics for efficiency analysis
"""
import simpy
from typing import Dict, List, Optional, Callable
from dataclasses import dataclass, field
from sim.core.packet import Packet
from sim.radio import propagation, airtime as airtime_calc
@dataclass
class Transmission:
"""Represents an ongoing transmission on the channel."""
packet: Packet
sender_id: int
start_time: float
end_time: float
rssi: float
channel_busy_until: float
airtime: float = 0.0 # Add airtime field
@dataclass
class ReceivedPacket:
"""Represents a packet received by a node."""
packet: Packet
sender_id: int
rssi: float
rx_time: float
collision: bool = False
class Channel:
"""
Wireless channel with collision detection.
Manages:
- Transmissions and their time slots
- Collision detection based on time overlap and RSSI difference
- Packet delivery to nodes within range
- Transmission statistics for efficiency analysis
"""
COLLISION_RSSI_DIFF_DB = 6.0 # RSSI difference threshold for collision
def __init__(self, env: simpy.Environment):
"""
Initialize channel.
Args:
env: SimPy environment
"""
self.env = env
self.transmissions: List[Transmission] = []
self.collision_count = 0
# Efficiency metrics
self.total_transmissions = 0
self.total_airtime = 0.0
self.hello_transmissions = 0
self.data_transmissions = 0
self.ack_transmissions = 0
# Callback for packet reception (set by node manager)
self.receive_callback: Optional[Callable[[int, ReceivedPacket], None]] = None
# Node positions {node_id: (x, y)}
self.node_positions: Dict[int, tuple] = {}
def register_node(self, node_id: int, x: float, y: float):
"""Register node position for propagation calculation."""
self.node_positions[node_id] = (x, y)
def transmit(self, packet: Packet, sender_id: int) -> float:
"""
Transmit a packet on the channel.
Args:
packet: Packet to transmit
sender_id: ID of sending node
Returns:
Airtime in seconds
"""
# Calculate packet size and airtime
if packet.is_hello:
pkt_airtime = airtime_calc.get_hello_airtime()
self.hello_transmissions += 1
elif packet.is_ack:
pkt_airtime = airtime_calc.get_ack_airtime()
self.ack_transmissions += 1
else: # DATA
payload_size = len(packet.payload) if packet.payload else 16
pkt_airtime = airtime_calc.get_data_airtime(payload_size)
self.data_transmissions += 1
# Track transmission statistics
self.total_transmissions += 1
self.total_airtime += pkt_airtime
start_time = self.env.now
end_time = start_time + pkt_airtime
# Get sender position and calculate RSSI for each potential receiver
sender_pos = self.node_positions.get(sender_id)
# Check for collisions with ongoing transmissions
colliding = self._check_collision(start_time, end_time)
# Create transmission record
if sender_pos:
# Calculate RSSI at sender's own position (not really used)
tx_power = packet.rssi if packet.rssi else 14.0
sender_rssi = tx_power # At transmitter, RSSI = TX power
else:
sender_rssi = 14.0
transmission = Transmission(
packet=packet,
sender_id=sender_id,
start_time=start_time,
end_time=end_time,
rssi=sender_rssi,
channel_busy_until=end_time,
airtime=pkt_airtime,
)
if colliding:
self.collision_count += 1
# All packets involved in collision are dropped
# Don't deliver to receivers
else:
self.transmissions.append(transmission)
# Deliver to all nodes in range
self._deliver_packet(packet, sender_id, start_time, end_time)
# Clean up old transmissions
self._cleanup_transmissions()
return pkt_airtime
def _check_collision(self, start_time: float, end_time: float) -> bool:
"""
Check if transmission overlaps with any ongoing transmission.
Collision condition:
- Time overlap AND
- |RSSI1 - RSSI2| < 6 dB
Args:
start_time: Start time of new transmission
end_time: End time of new transmission
Returns:
True if collision detected
"""
for trans in self.transmissions:
# Check time overlap
if not (end_time <= trans.start_time or start_time >= trans.end_time):
# Time overlaps - check RSSI difference
# Since we're checking at the sender, we assume similar RSSI
# at nearby receivers (simplified model)
# In real scenario, would need per-receiver RSSI calculation
return True
return False
def _deliver_packet(
self, packet: Packet, sender_id: int, start_time: float, end_time: float
):
"""
Deliver packet to all nodes within communication range.
Args:
packet: Packet to deliver
sender_id: ID of sending node
start_time: Transmission start time
end_time: Transmission end time
"""
if not self.receive_callback:
return
sender_pos = self.node_positions.get(sender_id)
if not sender_pos:
return
for node_id, pos in self.node_positions.items():
if node_id == sender_id:
continue # Don't deliver to sender
# Calculate distance and RSSI
dist = propagation.calculate_distance(
sender_pos[0], sender_pos[1], pos[0], pos[1]
)
rssi = propagation.calculate_rssi(14.0, dist) # Use default TX power
# Check if packet can be received
if propagation.can_receive(rssi):
# Check if this reception is affected by collision
collision = self._is_reception_collided(
node_id, start_time, end_time, rssi
)
received = ReceivedPacket(
packet=packet,
sender_id=sender_id,
rssi=rssi,
rx_time=start_time,
collision=collision,
)
# Deliver to node
self.receive_callback(node_id, received)
def _is_reception_collided(
self, receiver_id: int, start_time: float, end_time: float, signal_rssi: float
) -> bool:
"""
Check if reception is affected by collision from other transmitters.
Args:
receiver_id: ID of receiving node
start_time: Packet start time
end_time: Packet end time
signal_rssi: RSSI of the signal we want to receive
Returns:
True if collision detected
"""
for trans in self.transmissions:
if trans.sender_id == receiver_id:
continue # Ignore self-transmissions in list
# Check time overlap
if not (end_time <= trans.start_time or start_time >= trans.end_time):
# Time overlaps - calculate RSSI of interfering signal
receiver_pos = self.node_positions.get(receiver_id)
sender_pos = self.node_positions.get(trans.sender_id)
if receiver_pos and sender_pos:
dist = propagation.calculate_distance(
sender_pos[0], sender_pos[1], receiver_pos[0], receiver_pos[1]
)
interference_rssi = propagation.calculate_rssi(14.0, dist)
# Check RSSI difference
rssi_diff = abs(signal_rssi - interference_rssi)
if rssi_diff < self.COLLISION_RSSI_DIFF_DB:
return True
return False
def _cleanup_transmissions(self):
"""Remove old transmissions that have ended."""
current_time = self.env.now
self.transmissions = [
t for t in self.transmissions if t.end_time > current_time
]
def get_channel_busy_until(self) -> float:
"""Get the time until which channel is busy."""
if not self.transmissions:
return self.env.now
return max(t.channel_busy_until for t in self.transmissions)
def get_efficiency_metrics(self) -> dict:
"""Get efficiency metrics for analysis."""
return {
"total_transmissions": self.total_transmissions,
"total_airtime": self.total_airtime,
"hello_transmissions": self.hello_transmissions,
"data_transmissions": self.data_transmissions,
"ack_transmissions": self.ack_transmissions,
}
def reset(self):
"""Reset channel state."""
self.transmissions.clear()
self.collision_count = 0
self.total_transmissions = 0
self.total_airtime = 0.0
self.hello_transmissions = 0
self.data_transmissions = 0
self.ack_transmissions = 0