完成py_plan.md

This commit is contained in:
sinlatansen
2026-02-24 16:21:30 +08:00
parent 6c0526b2ef
commit 375febb4c0
21 changed files with 2041 additions and 4 deletions

259
sim/radio/channel.py Normal file
View 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