Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ nodeLanguageServer/**
nodeLanguageServer.*/**
dist/**
program
venv
1,855 changes: 1,855 additions & 0 deletions burningManSim.py

Large diffs are not rendered by default.

33 changes: 16 additions & 17 deletions lib/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def find_random_position(conf, nodes):
pathLoss = phy.estimate_path_loss(conf, dist, conf.FREQ)
rssi = conf.PTX + 2*conf.GL - pathLoss
# At least one node should be able to reach it
if rssi >= conf.SENSMODEM[conf.MODEM]:
if rssi >= conf.current_preset["sensitivity"]:
foundMax = True
if foundMin and foundMax:
x = posx
Expand Down Expand Up @@ -357,36 +357,35 @@ def save(self):


def setup_asymmetric_links(conf, nodes):
asymLinkRng = random.Random(conf.SEED)
"""
Analyze connectivity with baseline path loss (no longer pre-generates static LINK_OFFSET).
LINK_OFFSET is now applied dynamically per packet transmission for realistic fading effects.
"""
totalPairs = 0
symmetricLinks = 0
asymmetricLinks = 0
noLinks = 0
for i in range(conf.NR_NODES):
for b in range(conf.NR_NODES):
if i != b:
if conf.MODEL_ASYMMETRIC_LINKS:
conf.LINK_OFFSET[(i, b)] = asymLinkRng.gauss(conf.MODEL_ASYMMETRIC_LINKS_MEAN, conf.MODEL_ASYMMETRIC_LINKS_STDDEV)
else:
conf.LINK_OFFSET[(i, b)] = 0

# Clear any existing static LINK_OFFSET since we're now using dynamic per-packet offsets
conf.LINK_OFFSET = {}

for a in range(conf.NR_NODES):
for b in range(conf.NR_NODES):
if a != b:
# Calculate constant RSSI in both directions
# Calculate baseline RSSI in both directions (no random offset)
nodeA = nodes[a]
nodeB = nodes[b]
distAB = calc_dist(nodeA.x, nodeB.x, nodeA.y, nodeB.y, nodeA.z, nodeB.z)
pathLossAB = phy.estimate_path_loss(conf, distAB, conf.FREQ, nodeA.z, nodeB.z)

offsetAB = conf.LINK_OFFSET[(a, b)]
offsetBA = conf.LINK_OFFSET[(b, a)]

rssiAB = conf.PTX + nodeA.antennaGain + nodeB.antennaGain - pathLossAB - offsetAB
rssiBA = conf.PTX + nodeB.antennaGain + nodeA.antennaGain - pathLossAB - offsetBA
# Use baseline path loss (no random offset for analysis)
rssiAB = conf.PTX + nodeA.antennaGain + nodeB.antennaGain - pathLossAB
rssiBA = conf.PTX + nodeB.antennaGain + nodeA.antennaGain - pathLossAB

canAhearB = (rssiAB >= conf.SENSMODEM[conf.MODEM])
canBhearA = (rssiBA >= conf.SENSMODEM[conf.MODEM])
# Add some margin for dynamic variations (±5dB) when checking connectivity
margin = 5 # Account for dynamic fading range
canAhearB = (rssiAB >= conf.current_preset["sensitivity"] - margin)
canBhearA = (rssiBA >= conf.current_preset["sensitivity"] - margin)

totalPairs += 1
if canAhearB and canBhearA:
Expand Down
91 changes: 79 additions & 12 deletions lib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def __init__(self):
self.YSIZE = 15000 # vertical size of the area to simulate in m
self.OX = 0.0 # origin x-coordinate
self.OY = 0.0 # origin y-coordinate
self.MINDIST = 10 # minimum distance between each node in the area in m
self.MINDIST = 1 # minimum distance between each node in the area in m

self.GL = 0 # antenna gain of each node in dBi
self.HM = 1.0 # height of each node in m
Expand All @@ -31,7 +31,7 @@ def __init__(self):
self.ONE_HR_INTERVAL = self.ONE_MIN_INTERVAL * 60

### Discrete-event specific ###
self.MODEM = 4 # LoRa modem to use: 0 = ShortFast, 1 = Short Slow, ... 7 = Very Long Slow (default 4 is LongFast)
self.MODEM_PRESET = "LONG_FAST" # LoRa modem preset to use (default LONG_FAST matches firmware)
self.PERIOD = 100 * self.ONE_SECOND_INTERVAL # mean period of generating a new message with exponential distribution in ms
self.PACKETLENGTH = 40 # payload in bytes
self.SIMTIME = 30 * self.ONE_MIN_INTERVAL # duration of one simulation in ms
Expand All @@ -50,15 +50,75 @@ def __init__(self):

### PHY parameters (normally no change needed) ###
self.PTX = self.REGION["power_limit"]
# from RadioInterface::applyModemConfig()
self.BWMODEM = np.array([250e3, 250e3, 250e3, 250e3, 250e3, 125e3, 125e3, 62.5e3]) # bandwidth
self.SFMODEM = np.array([7, 8, 9, 10, 11, 11, 12, 12]) # spreading factor
self.CRMODEM = np.array([8, 8, 8, 8, 8, 8, 8, 8]) # coding rate
# minimum sensitivity from https://www.rfwireless-world.com/calculators/LoRa-Sensitivity-Calculator.html
self.SENSMODEM = np.array([-121.5, -124.0, -126.5, -129.0, -131.5, -134.5, -137.0, -140.0])
# minimum received power for CAD (3dB less than sensitivity)
self.CADMODEM = np.array([-124.5, -127.0, -129.5, -132.0, -134.5, -137.5, -140.0, -143.0])
self.FREQ = self.REGION["freq_start"]+self.BWMODEM[self.MODEM]*self.CHANNEL_NUM

# Modem presets from firmware RadioInterface::applyModemConfig()
self.MODEM_PRESETS = {
"SHORT_TURBO": {
"bw": 500e3,
"sf": 7,
"cr": 5,
"sensitivity": -121.5,
"cad_threshold": -124.5
},
"SHORT_FAST": {
"bw": 250e3,
"sf": 7,
"cr": 5,
"sensitivity": -121.5,
"cad_threshold": -124.5
},
"SHORT_SLOW": {
"bw": 250e3,
"sf": 8,
"cr": 5,
"sensitivity": -124.0,
"cad_threshold": -127.0
},
"MEDIUM_FAST": {
"bw": 250e3,
"sf": 9,
"cr": 5,
"sensitivity": -126.5,
"cad_threshold": -129.5
},
"MEDIUM_SLOW": {
"bw": 250e3,
"sf": 10,
"cr": 5,
"sensitivity": -129.0,
"cad_threshold": -132.0
},
"LONG_FAST": {
"bw": 250e3,
"sf": 11,
"cr": 5,
"sensitivity": -131.5,
"cad_threshold": -134.5
},
"LONG_MODERATE": {
"bw": 125e3,
"sf": 11,
"cr": 8,
"sensitivity": -134.5,
"cad_threshold": -137.5
},
"LONG_SLOW": {
"bw": 125e3,
"sf": 12,
"cr": 8,
"sensitivity": -137.0,
"cad_threshold": -140.0
},
"VERY_LONG_SLOW": {
"bw": 62.5e3,
"sf": 12,
"cr": 8,
"sensitivity": -140.0,
"cad_threshold": -143.0
}
}

self.FREQ = self.REGION["freq_start"] + self.MODEM_PRESETS[self.MODEM_PRESET]["bw"] * self.CHANNEL_NUM
self.HEADERLENGTH = 16 # number of Meshtastic header bytes
self.ACKLENGTH = 2 # ACK payload in bytes
self.NOISE_LEVEL = -119.25 # some noise level in dB, based on SNR_MIN and minimum receiver sensitivity
Expand Down Expand Up @@ -92,7 +152,7 @@ def __init__(self):
# Adds a random offset to the link quality of each link
self.MODEL_ASYMMETRIC_LINKS = True
self.MODEL_ASYMMETRIC_LINKS_MEAN = 0
self.MODEL_ASYMMETRIC_LINKS_STDDEV = 3
self.MODEL_ASYMMETRIC_LINKS_STDDEV = 5
# Stores the offset for each link
# Populated when the simulator first starts
self.LINK_OFFSET = {}
Expand All @@ -116,9 +176,16 @@ def __init__(self):
self.SMART_POSITION_DISTANCE_THRESHOLD = 100
# 30s minimum time in firmware
self.SMART_POSITION_DISTANCE_MIN_TIME = 30 * self.ONE_SECOND_INTERVAL
# 5 minute maximum location update interval
self.GPS_MAX_UPDATE_INTERVAL = 5 * self.ONE_MIN_INTERVAL
# This mirrors the firmware's approach to monitoring channel utilization
self.CHANNEL_UTILIZATION_PERIODS = 6

@property
def current_preset(self):
"""Returns the currently selected modem preset configuration"""
return self.MODEM_PRESETS[self.MODEM_PRESET]

# Function that needs to be run to ensure the router dependent variables change appropriately
def update_router_dependencies(self):
# Example: Overwrite hop limit in the case of X new awesome routing algorithm
Expand Down
2 changes: 1 addition & 1 deletion lib/interactive.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,7 @@ def calc_receivers(self, tx, receivers):
pathLoss = phy.estimate_path_loss(conf, dist_3d, conf.FREQ, tx.z, rx.z)
RSSI = conf.PTX + tx.antennaGain + rx.antennaGain - pathLoss
SNR = RSSI-conf.NOISE_LEVEL
if RSSI >= conf.SENSMODEM[conf.MODEM]:
if RSSI >= conf.current_preset["sensitivity"]:
rxs.append(rx)
rssis.append(RSSI)
snrs.append(SNR)
Expand Down
18 changes: 14 additions & 4 deletions lib/mac.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,32 @@ def get_tx_delay_msec_weighted(node, rssi): # from RadioInterface::getTxDelayMs
CWsize = int((snr - SNR_MIN) * (CWmax - CWmin) / (SNR_MAX - SNR_MIN) + CWmin)
if node.isRouter:
CW = random.randint(0, 2 * CWsize - 1)
delay = CW * SLOT_TIME
else:
CW = random.randint(0, 2 ** CWsize - 1)
delay = (2 * CWmax * SLOT_TIME) + (CW * SLOT_TIME)
verboseprint(f'Node {node.nodeid} has CW size {CWsize} and picked CW {CW}')
return CW * SLOT_TIME
return delay


def get_tx_delay_msec(node): # from RadioInterface::getTxDelayMsec
channelUtil = node.airUtilization / node.env.now * 100
CWsize = int(channelUtil * (CWmax - CWmin) / 100 + CWmin)
CW = random.randint(0, 2 ** CWsize - 1)

if node.isRouter:
CW = random.randint(0, 2 * CWsize - 1)
delay = CW * SLOT_TIME
else:
CW = random.randint(0, 2 ** CWsize - 1)
delay = (2 * CWmax * SLOT_TIME) + (CW * SLOT_TIME)

verboseprint(f'Current channel utilization is {channelUtil}, so picked CW {CW}')
return CW * SLOT_TIME
return delay


def get_retransmission_msec(node, packet): # from RadioInterface::getRetransmissionMsec
packetAirtime = int(airtime(node.conf, node.conf.SFMODEM[node.conf.MODEM], node.conf.CRMODEM[node.conf.MODEM], packet.packetLen, node.conf.BWMODEM[node.conf.MODEM]))
preset = node.conf.current_preset
packetAirtime = int(airtime(node.conf, preset["sf"], preset["cr"], packet.packetLen, preset["bw"]))
channelUtil = node.airUtilization / node.env.now * 100
CWsize = int(channelUtil * (CWmax - CWmin) / 100 + CWmin)
return 2 * packetAirtime + (2 ** CWsize + 2 ** (int((CWmax + CWmin) / 2))) * SLOT_TIME + PROCESSING_TIME_MSEC
74 changes: 53 additions & 21 deletions lib/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@ def __init__(self, conf, nodes, env, bc_pipe, nodeid, period, messages, packetsA
self.airUtilization = 0
self.droppedByDelay = 0
self.rebroadcastPackets = 0
self.packetsHeard = 0 # Track total packets received for rebroadcast analysis
self.isMoving = False
self.gpsEnabled = False
self.nextGpsUpdateTime = 0 # Will be set if GPS is enabled
# Track last broadcast position/time
self.lastBroadcastX = self.x
self.lastBroadcastY = self.y
Expand All @@ -76,6 +78,8 @@ def __init__(self, conf, nodes, env, bc_pipe, nodeid, period, messages, packetsA
self.isMoving = True
if self.moveRng.random() <= self.conf.APPROX_RATIO_OF_NODES_MOVING_W_GPS_ENABLED:
self.gpsEnabled = True
# Random start time for GPS updates (0 to 5 minutes)
self.nextGpsUpdateTime = self.moveRng.randint(0, self.conf.GPS_MAX_UPDATE_INTERVAL)

# Randomly assign a movement speed
possibleSpeeds = [
Expand Down Expand Up @@ -137,18 +141,14 @@ def move_node(self, env):
self.x = new_x
self.y = new_y

if self.gpsEnabled:
distanceTraveled = calc_dist(self.lastBroadcastX, self.x, self.lastBroadcastY, self.y)
timeElapsed = env.now - self.lastBroadcastTime
if distanceTraveled >= self.conf.SMART_POSITION_DISTANCE_THRESHOLD and timeElapsed >= self.conf.SMART_POSITION_DISTANCE_MIN_TIME:
currentUtil = self.channel_utilization_percent()
if currentUtil < 25.0:
self.send_packet(NODENUM_BROADCAST, "POSITION")
self.lastBroadcastX = self.x
self.lastBroadcastY = self.y
self.lastBroadcastTime = env.now
else:
self.verboseprint(f"At time {env.now} node {self.nodeid} SKIPS POSITION broadcast (util={currentUtil:.1f}% > 25%)")
if self.gpsEnabled and env.now >= self.nextGpsUpdateTime:
self.send_packet(NODENUM_BROADCAST, "POSITION")
self.lastBroadcastX = self.x
self.lastBroadcastY = self.y
self.lastBroadcastTime = env.now
# Schedule next GPS update in exactly 5 minutes
self.nextGpsUpdateTime = env.now + self.conf.GPS_MAX_UPDATE_INTERVAL
self.verboseprint(f"At time {env.now} node {self.nodeid} sends POSITION broadcast, next at {self.nextGpsUpdateTime}")

# Wait until next move
nextMove = self.get_next_time(self.conf.ONE_MIN_INTERVAL)
Expand All @@ -171,7 +171,7 @@ def send_packet(self, destId, type=""):
def get_next_time(self, period):
nextGen = self.nodeRng.expovariate(1.0 / float(period))
# do not generate message near the end of the simulation (otherwise flooding cannot finish in time)
if self.env.now+nextGen + self.hopLimit * airtime(self.conf, self.conf.SFMODEM[self.conf.MODEM], self.conf.CRMODEM[self.conf.MODEM], self.conf.PACKETLENGTH, self.conf.BWMODEM[self.conf.MODEM]) < self.conf.SIMTIME:
if self.env.now+nextGen + self.hopLimit * airtime(self.conf, self.conf.current_preset["sf"], self.conf.current_preset["cr"], self.conf.PACKETLENGTH, self.conf.current_preset["bw"]) < self.conf.SIMTIME:
return nextGen
return -1

Expand Down Expand Up @@ -240,10 +240,33 @@ def transmit(self, packet):
if self.leastReceivedHopLimit[packet.seq] > packet.hopLimit: # no ACK received yet, so may start transmitting
self.verboseprint('At time', round(self.env.now, 3), 'node', self.nodeid, 'started low level send', packet.seq, 'hopLimit', packet.hopLimit, 'original Tx', packet.origTxNodeId)
self.nrPacketsSent += 1
for rx_node in self.nodes:
if packet.sensedByN[rx_node.nodeid]:
if check_collision(self.conf, self.env, packet, rx_node.nodeid, self.packetsAtN) == 0:
self.packetsAtN[rx_node.nodeid].append(packet)

# Log broadcast to CSV
if hasattr(self.conf, 'BROADCAST_CSV_WRITER'):
broadcast_type = 'original' if packet.origTxNodeId == self.nodeid else 'rebroadcast'
node_type = 'router' if self.isRouter else 'client'
self.conf.BROADCAST_CSV_WRITER.writerow([
round(self.env.now, 3), # timestamp
self.nodeid, # node_id
node_type, # node_type
broadcast_type, # broadcast_type
packet.seq, # seq
packet.destId, # dest_id
packet.origTxNodeId, # orig_tx_node_id
packet.hopLimit, # hop_limit
packet.packetLen, # packet_length
self.x, # x
self.y, # y
self.z, # z
self.nodeConfig.get('environmentalAttenuation', 0), # environmental_attenuation
self.nodeConfig.get('inGroundClutter', False) # in_ground_clutter
])
# OPTIMIZATION: Only check nodes that can actually sense this packet
# Build list of sensing node IDs first to avoid O(n) iteration
sensing_node_ids = [node_id for node_id, can_sense in enumerate(packet.sensedByN) if can_sense]
for rx_node_id in sensing_node_ids:
if check_collision(self.conf, self.env, packet, rx_node_id, self.packetsAtN) == 0:
self.packetsAtN[rx_node_id].append(packet)
packet.startTime = self.env.now
packet.endTime = self.env.now + packet.timeOnAir
self.txAirUtilization += packet.timeOnAir
Expand Down Expand Up @@ -302,10 +325,14 @@ def receive(self, in_pipe):
realAckReceived = False
for sentPacket in self.packets:
# check if ACK for message you currently have in queue
# Routers ignore implicit ACKs from other routers (matching firmware behavior)
if sentPacket.txNodeId == self.nodeid and sentPacket.seq == p.seq:
self.verboseprint('At time', round(self.env.now, 3), 'node', self.nodeid, 'received implicit ACK for message in queue.')
ackReceived = True
sentPacket.ackReceived = True
if not self.isRouter: # Only non-routers respect implicit ACKs
self.verboseprint('At time', round(self.env.now, 3), 'node', self.nodeid, 'received implicit ACK for message in queue.')
ackReceived = True
sentPacket.ackReceived = True
else:
self.verboseprint('At time', round(self.env.now, 3), 'router', self.nodeid, 'ignores implicit ACK for message in queue.')
# check if real ACK for message sent
if sentPacket.origTxNodeId == self.nodeid and p.isAck and sentPacket.seq == p.requestId:
self.verboseprint('At time', round(self.env.now, 3), 'node', self.nodeid, 'received real ACK.')
Expand All @@ -322,11 +349,16 @@ def receive(self, in_pipe):
self.packets.append(pAck)
self.env.process(self.transmit(pAck))
# Rebroadcasting Logic for received message. This is a broadcast or a DM not meant for us.
elif not p.destId == self.nodeid and not ackReceived and not realAckReceived and p.hopLimit > 0:
# Routers ignore implicit ACKs from other routers (matching firmware FloodingRouter::perhapsCancelDupe)
elif not p.destId == self.nodeid and p.hopLimit > 0 and (
self.isRouter or (not ackReceived and not realAckReceived)
):
self.packetsHeard += 1 # Count packets that could potentially be rebroadcast
# FloodingRouter: rebroadcast received packet
if self.conf.SELECTED_ROUTER_TYPE == self.conf.ROUTER_TYPE.MANAGED_FLOOD:
if not self.isClientMute:
self.verboseprint('At time', round(self.env.now, 3), 'node', self.nodeid, 'rebroadcasts received packet', p.seq)
self.rebroadcastPackets += 1 # Track rebroadcast count
pNew = MeshPacket(self.conf, self.nodes, p.origTxNodeId, p.destId, self.nodeid, p.packetLen, p.seq, p.genTime, p.wantAck, False, None, self.env.now, self.verboseprint)
pNew.hopLimit = p.hopLimit - 1
self.packets.append(pNew)
Expand Down
Loading