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