260 lines
8.2 KiB
Python
260 lines
8.2 KiB
Python
"""
|
|
Wireless Channel Model.
|
|
|
|
Implements:
|
|
- Broadcast propagation to all nodes in range
|
|
- Airtime occupation tracking
|
|
- Collision detection (time overlap + |RSSI1 - RSSI2| < 6 dB)
|
|
"""
|
|
|
|
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
|
|
|
|
|
|
@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
|
|
"""
|
|
|
|
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
|
|
|
|
# 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()
|
|
elif packet.is_ack:
|
|
pkt_airtime = airtime_calc.get_ack_airtime()
|
|
else: # DATA
|
|
payload_size = len(packet.payload) if packet.payload else 16
|
|
pkt_airtime = airtime_calc.get_data_airtime(payload_size)
|
|
|
|
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,
|
|
)
|
|
|
|
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 reset(self):
|
|
"""Reset channel state."""
|
|
self.transmissions.clear()
|
|
self.collision_count = 0
|