完成py_plan.md
This commit is contained in:
259
sim/radio/channel.py
Normal file
259
sim/radio/channel.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user