完成py_plan.md
This commit is contained in:
3
sim/radio/__init__.py
Normal file
3
sim/radio/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Radio module."""
|
||||
|
||||
__all__ = ["airtime", "propagation"]
|
||||
170
sim/radio/airtime.py
Normal file
170
sim/radio/airtime.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
LoRa Airtime Calculation.
|
||||
|
||||
Implements the real LoRa airtime formula for accurate simulation.
|
||||
Reference: Semtech SX1276/77/78/79 Datasheet
|
||||
"""
|
||||
|
||||
import math
|
||||
from sim import config
|
||||
|
||||
|
||||
def calculate_symbol_time(sf: int, bw: int) -> float:
|
||||
"""
|
||||
Calculate symbol time.
|
||||
|
||||
T_symbol = 2^SF / BW
|
||||
|
||||
Args:
|
||||
sf: Spreading Factor (7-12)
|
||||
bw: Bandwidth in Hz
|
||||
|
||||
Returns:
|
||||
Symbol time in seconds
|
||||
"""
|
||||
return (2**sf) / bw
|
||||
|
||||
|
||||
def calculate_payload_airtime(
|
||||
payload_size: int,
|
||||
sf: int,
|
||||
bw: int,
|
||||
cr: int,
|
||||
use_header: bool = True,
|
||||
low_data_rate_optimize: bool = None,
|
||||
) -> float:
|
||||
"""
|
||||
Calculate payload airtime.
|
||||
|
||||
Args:
|
||||
payload_size: Payload size in bytes
|
||||
sf: Spreading Factor (7-12)
|
||||
bw: Bandwidth in Hz
|
||||
cr: Coding Rate (5-8, represents 4/5 to 4/8)
|
||||
use_header: Whether packet header is present
|
||||
low_data_rate_optimize: Low Data Rate Optimization flag
|
||||
Set to True if SF >= 11 or BW <= 125kHz
|
||||
|
||||
Returns:
|
||||
Payload airtime in seconds
|
||||
"""
|
||||
# Determine DE (Low Data Rate Optimization)
|
||||
if low_data_rate_optimize is None:
|
||||
# Auto-detect: DE = 1 if SF >= 11 or BW <= 125 kHz
|
||||
de = 1 if (sf >= 11 or bw <= 125000) else 0
|
||||
else:
|
||||
de = 1 if low_data_rate_optimize else 0
|
||||
|
||||
# H = 0 if header is present, 1 if no header
|
||||
h = 0 if use_header else 1
|
||||
|
||||
# Calculate number of payload symbols
|
||||
# N_payload = 8 + max(ceil((8*PL - 4*SF + 28 - 16 - 20*H) / (4*(SF - 2*DE))) * (CR + 4), 0)
|
||||
numerator = 8 * payload_size - 4 * sf + 28 - 16 - 20 * h
|
||||
denominator = 4 * (sf - 2 * de)
|
||||
|
||||
if denominator <= 0:
|
||||
# SF - 2*DE <= 0, use minimum
|
||||
n_payload = 0
|
||||
else:
|
||||
n_payload = 8 + max(math.ceil(numerator / denominator) * (cr + 4), 0)
|
||||
|
||||
# Calculate time
|
||||
symbol_time = calculate_symbol_time(sf, bw)
|
||||
return n_payload * symbol_time
|
||||
|
||||
|
||||
def calculate_preamble_airtime(sf: int, bw: int, preamble: int = None) -> float:
|
||||
"""
|
||||
Calculate preamble airtime.
|
||||
|
||||
T_preamble = (PREAMBLE + 4.25) * T_symbol
|
||||
|
||||
Args:
|
||||
sf: Spreading Factor (7-12)
|
||||
bw: Bandwidth in Hz
|
||||
preamble: Number of preamble symbols (default from config)
|
||||
|
||||
Returns:
|
||||
Preamble airtime in seconds
|
||||
"""
|
||||
if preamble is None:
|
||||
preamble = config.PREAMBLE
|
||||
|
||||
symbol_time = calculate_symbol_time(sf, bw)
|
||||
return (preamble + 4.25) * symbol_time
|
||||
|
||||
|
||||
def calculate_packet_airtime(
|
||||
payload_size: int,
|
||||
sf: int = None,
|
||||
bw: int = None,
|
||||
cr: int = None,
|
||||
preamble: int = None,
|
||||
) -> float:
|
||||
"""
|
||||
Calculate total packet airtime.
|
||||
|
||||
T_packet = T_preamble + T_payload
|
||||
|
||||
Args:
|
||||
payload_size: Payload size in bytes
|
||||
sf: Spreading Factor (default from config)
|
||||
bw: Bandwidth in Hz (default from config)
|
||||
cr: Coding Rate (default from config)
|
||||
preamble: Number of preamble symbols (default from config)
|
||||
|
||||
Returns:
|
||||
Total packet airtime in seconds
|
||||
"""
|
||||
if sf is None:
|
||||
sf = config.SF
|
||||
if bw is None:
|
||||
bw = config.BW
|
||||
if cr is None:
|
||||
cr = config.CR
|
||||
|
||||
preamble_time = calculate_preamble_airtime(sf, bw, preamble)
|
||||
payload_time = calculate_payload_airtime(payload_size, sf, bw, cr)
|
||||
|
||||
return preamble_time + payload_time
|
||||
|
||||
|
||||
def calculate_ack_time(ack_seq: int = 1) -> float:
|
||||
"""
|
||||
Calculate ACK packet airtime.
|
||||
|
||||
ACK packet structure:
|
||||
- 1 byte for type
|
||||
- 1 byte for seq
|
||||
- 1 byte for dst
|
||||
- Total: 3 bytes (minimal)
|
||||
|
||||
Args:
|
||||
ack_seq: ACK sequence number (affects total size)
|
||||
|
||||
Returns:
|
||||
ACK airtime in seconds
|
||||
"""
|
||||
# ACK = type(1) + seq(1) + dst(1) + reserved(1) = 4 bytes minimum
|
||||
ack_size = 4
|
||||
return calculate_packet_airtime(ack_size)
|
||||
|
||||
|
||||
# Convenience function for quick calculations
|
||||
def get_hello_airtime() -> float:
|
||||
"""Get airtime for HELLO packet (minimal size)."""
|
||||
# HELLO = type(1) + src(1) + cost(4) + seq(1) = ~7 bytes
|
||||
return calculate_packet_airtime(7)
|
||||
|
||||
|
||||
def get_data_airtime(payload_size: int = 16) -> float:
|
||||
"""Get airtime for DATA packet."""
|
||||
# DATA = type(1) + src(1) + dst(1) + seq(2) + hop(1) + payload(n)
|
||||
base_size = 6
|
||||
return calculate_packet_airtime(base_size + payload_size)
|
||||
|
||||
|
||||
def get_ack_airtime() -> float:
|
||||
"""Get airtime for ACK packet."""
|
||||
return calculate_ack_time()
|
||||
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
|
||||
61
sim/radio/propagation.py
Normal file
61
sim/radio/propagation.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Radio propagation model."""
|
||||
|
||||
import math
|
||||
import random
|
||||
from sim import config
|
||||
|
||||
|
||||
def calculate_distance(x1: float, y1: float, x2: float, y2: float) -> float:
|
||||
"""Calculate Euclidean distance between two points."""
|
||||
return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
||||
|
||||
|
||||
def calculate_rssi(tx_power: float, distance: float) -> float:
|
||||
"""
|
||||
Calculate RSSI using free-space path loss model.
|
||||
|
||||
RSSI = TX_POWER - 10*n*log10(d) + Gaussian_noise
|
||||
|
||||
Args:
|
||||
tx_power: Transmit power in dBm
|
||||
distance: Distance in meters
|
||||
|
||||
Returns:
|
||||
RSSI in dBm
|
||||
"""
|
||||
if distance < 1.0:
|
||||
distance = 1.0 # Avoid log(0)
|
||||
|
||||
# Path loss
|
||||
path_loss = 10 * config.PATH_LOSS_EXPONENT * math.log10(distance)
|
||||
|
||||
# Gaussian noise
|
||||
noise = random.gauss(0, config.NOISE_SIGMA)
|
||||
|
||||
rssi = tx_power - path_loss + noise
|
||||
return rssi
|
||||
|
||||
|
||||
def can_receive(rssi: float) -> bool:
|
||||
"""Check if packet can be received based on RSSI threshold."""
|
||||
return rssi >= config.RSSI_THRESHOLD
|
||||
|
||||
|
||||
def calculate_link_penalty(rssi: float) -> float:
|
||||
"""
|
||||
Calculate link penalty for routing.
|
||||
|
||||
penalty = max(0, (RSSI_THRESHOLD - RSSI) / SCALE)
|
||||
|
||||
Args:
|
||||
rssi: Received signal strength
|
||||
|
||||
Returns:
|
||||
Link penalty (higher = worse link)
|
||||
"""
|
||||
return max(0.0, (config.RSSI_THRESHOLD - rssi) / config.LINK_PENALTY_SCALE)
|
||||
|
||||
|
||||
def is_link_viable(rssi: float) -> bool:
|
||||
"""Check if link is viable for communication."""
|
||||
return can_receive(rssi)
|
||||
Reference in New Issue
Block a user