diff --git a/lib/common.py b/lib/common.py index 66f2b147..868fab87 100644 --- a/lib/common.py +++ b/lib/common.py @@ -181,7 +181,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 @@ -389,8 +389,8 @@ def setup_asymmetric_links(conf, nodes): rssiAB = conf.PTX + nodeA.antennaGain - pathLossAB - offsetAB rssiBA = conf.PTX + nodeB.antennaGain - pathLossAB - offsetBA - canAhearB = (rssiAB >= conf.SENSMODEM[conf.MODEM]) - canBhearA = (rssiBA >= conf.SENSMODEM[conf.MODEM]) + canAhearB = (rssiAB >= conf.current_preset["sensitivity"]) + canBhearA = (rssiBA >= conf.current_preset["sensitivity"]) totalPairs += 1 if canAhearB and canBhearA: diff --git a/lib/config.py b/lib/config.py index 6794a179..2080cf04 100644 --- a/lib/config.py +++ b/lib/config.py @@ -31,18 +31,275 @@ 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 self.INTERFERENCE_LEVEL = 0.05 # chance that at a given moment there is already a LoRa packet being sent on your channel, outside of the Meshtastic traffic. Given in a ratio from 0 to 1. self.COLLISION_DUE_TO_INTERFERENCE = False self.DMs = False # Set True for sending DMs (with random destination), False for broadcasts - # from RadioInterface.cpp RegionInfo regions[] + # from firmware RegionInfo regions[] in src/mesh/RadioInterface.cpp self.regions = { - "US": {"freq_start": 902e6, "freq_end": 928e6, "power_limit": 30}, - "EU433": {"freq_start": 433e6, "freq_end": 434e6, "power_limit": 12}, - "EU868": {"freq_start": 868e6, "freq_end": 868e6, "power_limit": 27}, + "US": { + "freq_start": 902.0e6, + "freq_end": 928.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 30, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "EU_433": { + "freq_start": 433.0e6, + "freq_end": 434.0e6, + "duty_cycle": 10, + "spacing": 0, + "power_limit": 10, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "EU_868": { + "freq_start": 869.4e6, + "freq_end": 869.65e6, + "duty_cycle": 10, + "spacing": 0, + "power_limit": 27, + "audio_permitted": False, + "frequency_switching": False, + "wide_lora": False + }, + "CN": { + "freq_start": 470.0e6, + "freq_end": 510.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 19, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "JP": { + "freq_start": 920.5e6, + "freq_end": 923.5e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 13, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "ANZ": { + "freq_start": 915.0e6, + "freq_end": 928.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 30, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "ANZ_433": { + "freq_start": 433.05e6, + "freq_end": 434.79e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 14, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "RU": { + "freq_start": 868.7e6, + "freq_end": 869.2e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 20, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "KR": { + "freq_start": 920.0e6, + "freq_end": 923.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 23, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "TW": { + "freq_start": 920.0e6, + "freq_end": 925.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 27, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "IN": { + "freq_start": 865.0e6, + "freq_end": 867.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 30, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "NZ_865": { + "freq_start": 864.0e6, + "freq_end": 868.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 36, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "TH": { + "freq_start": 920.0e6, + "freq_end": 925.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 16, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "UA_433": { + "freq_start": 433.0e6, + "freq_end": 434.7e6, + "duty_cycle": 10, + "spacing": 0, + "power_limit": 10, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "UA_868": { + "freq_start": 868.0e6, + "freq_end": 868.6e6, + "duty_cycle": 1, + "spacing": 0, + "power_limit": 14, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "MY_433": { + "freq_start": 433.0e6, + "freq_end": 435.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 20, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "MY_919": { + "freq_start": 919.0e6, + "freq_end": 924.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 27, + "audio_permitted": True, + "frequency_switching": True, + "wide_lora": False + }, + "SG_923": { + "freq_start": 917.0e6, + "freq_end": 925.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 20, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "PH_433": { + "freq_start": 433.0e6, + "freq_end": 434.7e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 10, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "PH_868": { + "freq_start": 868.0e6, + "freq_end": 869.4e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 14, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "PH_915": { + "freq_start": 915.0e6, + "freq_end": 918.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 24, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "KZ_433": { + "freq_start": 433.075e6, + "freq_end": 434.775e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 10, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "KZ_863": { + "freq_start": 863.0e6, + "freq_end": 868.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 30, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "NP_865": { + "freq_start": 865.0e6, + "freq_end": 868.0e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 30, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "BR_902": { + "freq_start": 902.0e6, + "freq_end": 907.5e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 30, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": False + }, + "LORA_24": { + "freq_start": 2400.0e6, + "freq_end": 2483.5e6, + "duty_cycle": 100, + "spacing": 0, + "power_limit": 10, + "audio_permitted": True, + "frequency_switching": False, + "wide_lora": True + }, } self.REGION = self.regions["US"] # Select a different region here self.CHANNEL_NUM = 27 # Channel number @@ -52,15 +309,86 @@ 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() in src/mesh/RadioInterface.cpp + # minimum sensitivity from https://www.rfwireless-world.com/calculators/LoRa-Sensitivity-Calculator.html, using a Noise Figure (NF) of 6dB + # minimum received power for CAD: 3dB less than sensitivity + # TODO: the 'bw' parameter is changed based on the region's 'wide_lora' setting. Implement this. + self.MODEM_PRESETS = { + "SHORT_TURBO": { + "bw": 500e3, + "cr": 5, + "sf": 7, + "sensitivity": -118.5, + "cad_threshold": -121.5 + }, + "SHORT_FAST": { + "bw": 250e3, + "cr": 5, + "sf": 7, + "sensitivity": -121.5, + "cad_threshold": -124.5 + }, + "SHORT_SLOW": { + "bw": 250e3, + "cr": 5, + "sf": 8, + "sensitivity": -124.0, + "cad_threshold": -127.0 + }, + "MEDIUM_FAST": { + "bw": 250e3, + "cr": 5, + "sf": 9, + "sensitivity": -126.5, + "cad_threshold": -129.5 + }, + "MEDIUM_SLOW": { + "bw": 250e3, + "cr": 5, + "sf": 10, + "sensitivity": -129.0, + "cad_threshold": -132.0 + }, + "LONG_TURBO": { + "bw": 500e3, + "cr": 8, + "sf": 11, + "sensitivity": -128.5, + "cad_threshold": -131.5 + }, + "LONG_FAST": { + "bw": 250e3, + "cr": 5, + "sf": 11, + "sensitivity": -131.5, + "cad_threshold": -134.5 + }, + "LONG_MODERATE": { + "bw": 125e3, + "cr": 8, + "sf": 11, + "sensitivity": -134.5, + "cad_threshold": -137.5 + }, + "LONG_SLOW": { + "bw": 125e3, + "cr": 8, + "sf": 12, + "sensitivity": -137.0, + "cad_threshold": -140.0 + }, + # It doesn't appear like this is a preset that actually exists in the firmware now? + "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 @@ -119,6 +447,11 @@ def __init__(self): # 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 diff --git a/lib/discrete_event.py b/lib/discrete_event.py index eba5edbd..8c8526bd 100644 --- a/lib/discrete_event.py +++ b/lib/discrete_event.py @@ -6,7 +6,7 @@ def sim_report(conf, data, subdir, param): os.makedirs(os.path.join("out", "report", subdir), exist_ok=True) - fname = f"simReport_{conf.MODEM}_{param}.csv" + fname = f"simReport_{conf.MODEM_PRESET}_{param}.csv" df_new = pd.DataFrame(data) df_new.to_csv(os.path.join("out", "report", subdir, fname), index=False) diff --git a/lib/interactive.py b/lib/interactive.py index 34cd5812..9d5a9b82 100644 --- a/lib/interactive.py +++ b/lib/interactive.py @@ -714,7 +714,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 - 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) diff --git a/lib/mac.py b/lib/mac.py index a77b0288..2e451828 100644 --- a/lib/mac.py +++ b/lib/mac.py @@ -50,7 +50,8 @@ def get_tx_delay_msec(node): # from RadioInterface::getTxDelayMsec 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 diff --git a/lib/node.py b/lib/node.py index a2489fd3..def1f7dc 100644 --- a/lib/node.py +++ b/lib/node.py @@ -170,7 +170,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 diff --git a/lib/packet.py b/lib/packet.py index fda92b77..f6b2a52d 100644 --- a/lib/packet.py +++ b/lib/packet.py @@ -27,9 +27,9 @@ def __init__(self, conf, nodes, origTxNodeId, destId, txNodeId, plen, seq, genTi self.onAirToN = [True for _ in range(self.conf.NR_NODES)] # configuration values - self.sf = self.conf.SFMODEM[self.conf.MODEM] - self.cr = self.conf.CRMODEM[self.conf.MODEM] - self.bw = self.conf.BWMODEM[self.conf.MODEM] + self.sf = self.conf.current_preset["sf"] + self.cr = self.conf.current_preset["cr"] + self.bw = self.conf.current_preset["bw"] self.freq = self.conf.FREQ self.tx_node = next(n for n in nodes if n.nodeid == self.txNodeId) for rx_node in nodes: @@ -38,10 +38,10 @@ def __init__(self, conf, nodes, origTxNodeId, destId, txNodeId, plen, seq, genTi dist_3d = calc_dist(self.tx_node.x, rx_node.x, self.tx_node.y, rx_node.y, self.tx_node.z, rx_node.z) offset = self.conf.LINK_OFFSET[(self.txNodeId, rx_node.nodeid)] self.LplAtN[rx_node.nodeid] = estimate_path_loss(self.conf, dist_3d, self.freq, self.tx_node.z, rx_node.z) + offset - self.rssiAtN[rx_node.nodeid] = self.txpow + self.tx_node.antennaGain - self.LplAtN[rx_node.nodeid] - if self.rssiAtN[rx_node.nodeid] >= self.conf.SENSMODEM[self.conf.MODEM]: + self.rssiAtN[rx_node.nodeid] = self.txpow + self.tx_node.antennaGain + rx_node.antennaGain - self.LplAtN[rx_node.nodeid] + if self.rssiAtN[rx_node.nodeid] >= self.conf.current_preset["sensitivity"]: self.sensedByN[rx_node.nodeid] = True - if self.rssiAtN[rx_node.nodeid] >= self.conf.CADMODEM[self.conf.MODEM]: + if self.rssiAtN[rx_node.nodeid] >= self.conf.current_preset["cad_threshold"]: self.detectedByN[rx_node.nodeid] = True self.packetLen = plen diff --git a/lib/phy.py b/lib/phy.py index 9e6994eb..a2ed987b 100644 --- a/lib/phy.py +++ b/lib/phy.py @@ -14,7 +14,7 @@ def verboseprint(*args, **kwargs): # CAD duration + airPropagationTime+TxRxTurnaround+MACprocessing -SLOT_TIME = 8.5 * (2.0 ** conf.SFMODEM[conf.MODEM]) / conf.BWMODEM[conf.MODEM] * 1000 + 0.2 + 0.4 + 7 +SLOT_TIME = 8.5 * (2.0 ** conf.current_preset["sf"]) / conf.current_preset["bw"] * 1000 + 0.2 + 0.4 + 7 def check_collision(conf, env, packet, rx_nodeId, packetsAtN): @@ -159,7 +159,7 @@ def estimate_path_loss(conf, dist, freq, txZ=conf.HM, rxZ=conf.HM): def zero_link_budget(dist): - return conf.PTX + 2 * conf.GL - estimate_path_loss(conf, dist, conf.FREQ) - conf.SENSMODEM[conf.MODEM] + return conf.PTX + 2 * conf.GL - estimate_path_loss(conf, dist, conf.FREQ) - conf.current_preset["sensitivity"] def rootFinder(func, x0, args=(), tol=1, maxiter=100): @@ -179,7 +179,7 @@ def rootFinder(func, x0, args=(), tol=1, maxiter=100): return x def zero_link_budget_with_gain(dist, gain): - return conf.PTX + gain - estimate_path_loss(conf, dist, conf.FREQ) - conf.SENSMODEM[conf.MODEM] + return conf.PTX + gain - estimate_path_loss(conf, dist, conf.FREQ) - conf.current_preset["sensitivity"] def estimate_max_range(gain): return rootFinder(zero_link_budget_with_gain, 1500, args=(gain,)) diff --git a/loraMesh.py b/loraMesh.py index 13b0bf9b..a8db42be 100644 --- a/loraMesh.py +++ b/loraMesh.py @@ -63,7 +63,7 @@ def parse_params(conf, args): exit(1) print("Number of nodes:", conf.NR_NODES) - print("Modem:", conf.MODEM) + print("Modem:", conf.MODEM_PRESET) print("Simulation time (s):", conf.SIMTIME/1000) print("Period (s):", conf.PERIOD/1000) print("Interference level:", conf.INTERFERENCE_LEVEL)