""" 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