You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

280 lines
11 KiB

// scenario_txpower.cpp
// Tests adaptive TX power reduction in dense areas.
//
// Hypothesis: In a dense full-mesh network, reducing TX power when in DENSE/MEDIUM
// tier provides:
// 1. Battery savings — lower TX current × same airtime = less energy
// 2. Reduced interference radius — quieter nodes don't stomp on distant clusters
// 3. Capture effect benefit — nearby nodes still win capture battles
//
// Counter-hypothesis: In a chain topology, TX power reduction kills hop range
// on marginal links and collapses delivery.
//
// Test matrix:
// Power save levels: OFF, CONSERVATIVE (-6dB DENSE), AGGRESSIVE (-10dB DENSE/-6dB MEDIUM)
// Topologies: FullMesh 50, FullMesh 100, Chain 20, Grid 5×5
// Strategy: ADAPTIVE only (power-save only makes sense paired with hash-gate suppression)
//
// Energy model: SX1262 typical current draw
// +20 dBm → 120 mA TX 5.5 mA RX
// +14 dBm → 45 mA TX
// +10 dBm → 25 mA TX
//
// "Festival weekend" use case: 48-hour runtime estimate at each power level.
#include "SimBus.h"
#include "SimMetrics.h"
#include "RoutingStrategies.h"
#include <cstdio>
#include <vector>
#include <string>
#include <cstring>
using namespace sim;
struct PowerResult {
const char* topo;
int num_nodes;
const char* power_mode;
float dense_dbm;
float medium_dbm;
float avg_delivery_rate;
float avg_latency_ms;
uint32_t total_tx;
uint32_t total_collisions;
float total_tx_energy_mah; // TX energy across all nodes
float total_rx_energy_mah; // RX energy across all nodes
float total_energy_mah;
float per_node_energy_mah; // average per node
};
struct PowerLevel {
const char* name;
float dense_dbm;
float medium_dbm;
bool enabled;
};
static PowerResult runPowerTest(
RFChannelModel* model,
const char* topo_name,
int num_nodes, int grid_rows, int grid_cols,
float snr,
const PowerLevel& plevel,
int num_floods)
{
SimBus bus;
bus.tick_ms = 5;
for (int i = 0; i < num_nodes; i++) {
char name[32];
if (grid_rows > 0)
snprintf(name, sizeof(name), "n%d_%d", i/grid_cols, i%grid_cols);
else
snprintf(name, sizeof(name), "node%d", i);
bus.addNode(name, (uint32_t)(i + 1) * 0xdeadbeef);
}
bus.channel_model = model;
for (auto& b : bus.nodes) {
b.node->routing_strategy = RoutingStrategy::ADAPTIVE;
b.node->power_save_enabled = plevel.enabled;
b.node->power_save_dense_dbm = plevel.dense_dbm;
b.node->power_save_medium_dbm = plevel.medium_dbm;
b.node->full_power_dbm = 20.0f;
b.radio->tx_power_dbm = 20.0f;
}
// Warmup — prime density estimators.
// Need enough floods + time for relay storms to settle so density observations
// accumulate. FM100 needs more time — initial collision storm destroys most
// relay packets; later floods get through as relay timing spreads out.
uint64_t warmup_ms = grid_rows > 0 ? 8000 : 3000;
for (int i = 0; i < 3; i++) {
bus.sendFloodText(0, "warmup");
bus.run(warmup_ms);
}
bus.resetStats();
uint64_t prop_ms = grid_rows > 0 ? 8000 : 5000;
for (int i = 0; i < num_floods; i++) {
bus.sendFloodText(0, "bench");
bus.run(prop_ms);
}
auto stats = bus.metrics.aggregate(num_floods);
float tx_energy = 0, rx_energy = 0;
uint32_t total_tx = 0;
for (auto& b : bus.nodes) {
tx_energy += b.node->total_tx_energy_mah;
rx_energy += b.node->total_rx_energy_mah;
total_tx += b.node->total_tx_packets;
}
float total_energy = tx_energy + rx_energy;
return {
topo_name, num_nodes, plevel.name, plevel.dense_dbm, plevel.medium_dbm,
stats.avg_delivery_rate, stats.avg_latency_ms,
total_tx, bus.totalCollisions(),
tx_energy, rx_energy, total_energy,
num_nodes > 0 ? total_energy / (float)num_nodes : 0.0f
};
}
static const char* fmtDbm(float dbm) {
static char buf[16];
snprintf(buf, sizeof(buf), "%+.0fdBm", dbm - 20.0f);
return buf;
}
int main() {
printf("MeshCore Adaptive TX Power Scenario\n");
printf("=====================================\n");
printf("ADAPTIVE strategy + variable TX power reduction in DENSE/MEDIUM tier\n");
printf("Battery model: SX1262 typical current (120/45/25 mA at 20/14/10 dBm)\n\n");
FILE* csv = fopen("txpower_results.csv", "w");
if (csv)
fprintf(csv, "topo,num_nodes,power_mode,dense_dbm,medium_dbm,"
"avg_delivery_rate,avg_latency_ms,total_tx,total_collisions,"
"total_tx_energy_mah,total_rx_energy_mah,total_energy_mah,per_node_energy_mah\n");
// DC6x6: dense 6×6 positional cluster — each node hears ~8 neighbors.
// Range=1.5, spacing=1.0 → most nodes see full 3×3 neighborhood.
// This models a festival grounds: dense, positional, realistic.
// FM50: FullMesh 50, the validated ADAPTIVE scenario.
// CH20: chain — verifies power-save doesn't hurt sparse hop range.
struct Config {
const char* name; int nodes, rows, cols; float snr; bool dense_cluster;
};
Config configs[] = {
{ "FM50", 50, 0, 0, 8.0f, false },
{ "DC6x6", 36, 6, 6, 8.0f, true }, // dense 6×6 positional cluster
{ "CH20", 20, 0, 0, 8.0f, false },
};
// Power save levels to test.
// Dense tier gets more aggressive reduction; medium tier gets conservative.
PowerLevel levels[] = {
{ "FULL_PWR", 20.0f, 20.0f, false }, // baseline: no power save
{ "CONSERV", 14.0f, 17.0f, true }, // conservative: -6dB DENSE, -3dB MEDIUM
{ "MODERATE", 10.0f, 14.0f, true }, // moderate: -10dB DENSE, -6dB MEDIUM
{ "AGGRESSIVE", 7.0f, 10.0f, true }, // aggressive: -13dB DENSE, -10dB MEDIUM
};
for (auto& cfg : configs) {
RFChannelModel* model = nullptr;
if (cfg.dense_cluster) {
// Dense positional cluster: range=1.5, spacing=1.0
// Interior nodes each hear ~8 neighbors → DENSE tier (≥15 after relays)
auto* pm = new PositionalModel(1.5f, cfg.snr + 4.f, cfg.snr);
for (int r = 0; r < cfg.rows; r++)
for (int c = 0; c < cfg.cols; c++)
pm->addNode((float)c, (float)r);
model = pm;
} else if (cfg.name[0] == 'C') {
model = new ChainModel(cfg.snr);
} else {
model = new FullMeshModel(cfg.snr);
}
printf("\n=== %s (%d nodes, SNR=%.0fdB) ===\n", cfg.name, cfg.nodes, cfg.snr);
printf(" %-12s %-14s | dr%% lat(ms) tx coll "
"tx_E(mAh) rx_E(mAh) tot_E(mAh) node_E(mAh) | Δdr Δenergy\n",
"mode", "dense/med pwr");
PowerResult baseline{};
bool have_base = false;
int num_floods = 20;
for (auto& lvl : levels) {
auto r = runPowerTest(model, cfg.name, cfg.nodes,
cfg.rows, cfg.cols, cfg.snr, lvl, num_floods);
char pwr_label[32];
snprintf(pwr_label, sizeof(pwr_label), "%+.0f/%+.0fdBm",
lvl.dense_dbm - 20.0f, lvl.medium_dbm - 20.0f);
if (!have_base) {
baseline = r; have_base = true;
printf(" %-12s %-14s | %5.1f%% %7.0f %-5u %-6u "
"%9.3f %9.3f %9.3f %9.3f\n",
lvl.name, pwr_label,
r.avg_delivery_rate*100.f, r.avg_latency_ms,
r.total_tx, r.total_collisions,
r.total_tx_energy_mah, r.total_rx_energy_mah,
r.total_energy_mah, r.per_node_energy_mah);
} else {
float dr_d = (r.avg_delivery_rate - baseline.avg_delivery_rate)*100.f;
float e_pct = baseline.total_energy_mah > 0
? (r.total_energy_mah - baseline.total_energy_mah)
/ baseline.total_energy_mah * 100.f : 0.f;
const char* flag = r.avg_delivery_rate < baseline.avg_delivery_rate - 0.03f
? " !!" : "";
printf(" %-12s %-14s | %5.1f%% %7.0f %-5u %-6u "
"%9.3f %9.3f %9.3f %9.3f | %+5.1f%% %+6.1f%%%s\n",
lvl.name, pwr_label,
r.avg_delivery_rate*100.f, r.avg_latency_ms,
r.total_tx, r.total_collisions,
r.total_tx_energy_mah, r.total_rx_energy_mah,
r.total_energy_mah, r.per_node_energy_mah,
dr_d, e_pct, flag);
}
if (csv)
fprintf(csv, "%s,%d,%s,%.1f,%.1f,%.4f,%.1f,%u,%u,%.4f,%.4f,%.4f,%.4f\n",
cfg.name, cfg.nodes, lvl.name, lvl.dense_dbm, lvl.medium_dbm,
r.avg_delivery_rate, r.avg_latency_ms,
r.total_tx, r.total_collisions,
r.total_tx_energy_mah, r.total_rx_energy_mah,
r.total_energy_mah, r.per_node_energy_mah);
}
delete model;
}
// Festival weekend projection: 48-hour runtime
printf("\n=== FESTIVAL WEEKEND PROJECTION (48-hour runtime) ===\n");
printf("Assumptions: FM50, 1 flood/30s, SF8, SX1262 radio\n");
printf("Battery: 2000 mAh (typical USB power bank to LoRa node)\n");
printf("Floods/hour: 120 | TX per flood (ADAPTIVE DENSE): ~7 nodes\n\n");
// Per-node per-flood energy at each power level (TX component dominates)
struct FestEst {
const char* mode;
float tx_dbm;
float relay_fraction; // fraction of floods this node relays (DENSE hash gate)
};
FestEst fests[] = {
{ "FULL_PWR ", 20.0f, 0.15f },
{ "CONSERV ", 14.0f, 0.15f },
{ "MODERATE ", 10.0f, 0.15f },
{ "AGGRESSIVE", 7.0f, 0.15f },
};
float airtime_s = 0.623f; // ~623ms per packet at SF8 BW62.5
float floods_per_hour = 120.0f;
float rx_fraction = 1.0f; // node receives every flood (full mesh)
float battery_mah = 2000.0f;
printf(" %-12s TX pwr TX mAh/hr RX mAh/hr Tot mAh/hr Hours\n", "Mode");
for (auto& f : fests) {
float tx_ma = SimNode::txCurrentMa(f.tx_dbm);
float tx_mah_hr = tx_ma * floods_per_hour * f.relay_fraction * airtime_s / 3600.f;
float rx_mah_hr = SimNode::RX_CURRENT_MA * floods_per_hour * rx_fraction * airtime_s / 3600.f;
float tot_mah_hr = tx_mah_hr + rx_mah_hr;
float hours = battery_mah / tot_mah_hr;
printf(" %-12s %+.0fdBm %7.3f %7.3f %8.3f %5.0f hrs\n",
f.mode, f.tx_dbm - 20.0f, tx_mah_hr, rx_mah_hr, tot_mah_hr, hours);
}
printf("\nNote: excludes MCU idle current (~10-30 mA) which dominates in practice.\n");
printf("With MCU at 20mA continuous: add ~9.6 mAh/hr → max ~130hrs on 2000mAh.\n");
printf("TX power reduction still meaningful: saves TX peak current spikes and heat.\n");
if (csv) fclose(csv);
printf("\nCSV written to txpower_results.csv\n");
return 0;
}