""" Flooding-based routing protocol. Simple flooding: when a node receives a DATA packet it hasn't seen, it forwards to ALL neighbors (except the sender). This is a baseline algorithm for comparison with gradient routing. """ import random from typing import Dict, Optional, Set from dataclasses import dataclass, field from sim.core.packet import Packet, PacketType from sim import config # Marker for broadcast forwarding BROADCAST = -1 @dataclass class NeighborInfo: """Information about a neighbor node.""" node_id: int rssi: float last_hello_time: float class FloodingRouting: """ Flooding routing protocol. Each node maintains: - seen_packets: Set of (src, seq) tuples to prevent duplicate forwarding - neighbors: Dict of known neighbors Forwarding logic: - If packet not seen before, forward to ALL neighbors - Use seen_packets cache with TTL to prevent infinite loops """ def __init__(self, node_id: int, is_sink: bool = False): """ Initialize routing. Args: node_id: This node's ID is_sink: Whether this node is the sink """ self.node_id = node_id self.is_sink = is_sink # Routing state self.parent: Optional[int] = None # Not used in flooding self.neighbors: Dict[int, NeighborInfo] = {} # Flooding state self.seen_packets: Set[tuple] = set() self.max_seen_cache = 1000 # Limit cache size # Sequence number for HELLO messages self.hello_seq = 0 # Cost (for compatibility with metrics) self.cost = 0 if is_sink else 1 def reset(self): """Reset routing state.""" self.seen_packets.clear() self.neighbors.clear() self.hello_seq = 0 self.cost = 0 if self.is_sink else 1 def create_hello_packet(self) -> Packet: """ Create a HELLO packet for neighbor discovery. Returns: HELLO packet with node ID """ packet = Packet( type=PacketType.HELLO, src=self.node_id, dst=-1, # Broadcast seq=self.hello_seq, hop=0, payload=str(self.node_id), # Just send our ID ) self.hello_seq += 1 return packet def process_hello(self, packet: Packet, rssi: float) -> bool: """ Process received HELLO packet. For flooding, we just track neighbors - no cost calculation. Args: packet: Received HELLO packet rssi: RSSI of received signal Returns: True if neighbor list changed """ # Update neighbor info old_neighbors = len(self.neighbors) self.neighbors[packet.src] = NeighborInfo( node_id=packet.src, rssi=rssi, last_hello_time=rssi, # Store time in rssi field ) return len(self.neighbors) != old_neighbors def get_next_hop(self, packet: Packet = None) -> int: """ Get next hops for forwarding. For flooding, returns BROADCAST to signal all neighbors. Args: packet: The packet to forward (for checking seen status) Returns: BROADCAST (-1) to forward to all neighbors """ return BROADCAST def should_forward(self, packet: Packet) -> bool: """ Check if this packet should be forwarded (not seen before). Args: packet: The packet to check Returns: True if packet should be forwarded """ packet_id = (packet.src, packet.seq) if packet_id in self.seen_packets: return False # Add to seen set self.seen_packets.add(packet_id) # Limit cache size if len(self.seen_packets) > self.max_seen_cache: # Remove oldest entries to_remove = len(self.seen_packets) - self.max_seen_cache + 100 self.seen_packets = set(list(self.seen_packets)[to_remove:]) return True def get_all_neighbors(self) -> list: """Get list of all neighbor IDs.""" return list(self.neighbors.keys()) def is_route_valid(self) -> bool: """Check if routing is valid.""" # Flooding is always "valid" - we can always forward return len(self.neighbors) > 0 def cleanup_stale_neighbors(self, current_time: float, timeout: float = 30.0): """Remove neighbors that haven't sent HELLO recently.""" stale = [ nid for nid, info in self.neighbors.items() if current_time - info.last_hello_time > timeout ] for nid in stale: del self.neighbors[nid] def get_routing_table(self) -> dict: """Get routing table for debugging/visualization.""" return { "node_id": self.node_id, "is_sink": self.is_sink, "cost": self.cost, "parent": self.parent, "neighbors": { nid: {"rssi": round(info.rssi, 2)} for nid, info in self.neighbors.items() }, "algorithm": "flooding", }