Skip to content
Open
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
6 changes: 6 additions & 0 deletions sim/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
build/
*.o
*.a
*.d
*.csv
routing_comparison.csv
171 changes: 171 additions & 0 deletions sim/ADAPTIVE_ROUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Adaptive Routing Simulation — Design & Results

## Overview

This document describes the ADAPTIVE routing strategy added to the MeshCore simulation harness, including the density estimator, hash-based relay selection, relay suppression, and adaptive TX power saving. Empirical validation results across all scenario types are summarised below.

---

## 1. Density Estimator (`DensityEstimator.h`)

Passive, protocol-free neighbour density measurement. Each node counts how many **unique direct senders** it hears within a 60-second rolling window. Only hop_count==1 packets are counted (direct neighbours only — relays don't inflate the estimate).

| Threshold | Tier | Default value |
|-----------|--------|---------------|
| ≤ DENSITY_SPARSE_MAX | SPARSE | ≤ 4 neighbours |
| ≥ DENSITY_DENSE_MIN | DENSE | ≥ 15 neighbours |
| Between | MEDIUM | |

All thresholds are compile-time overridable (`-DDENSITY_WINDOW_MS=30000`, etc.).

**Implementation note:** Uses `std::unordered_set<uint32_t>` for unique-sender counting, eliminating the former fixed-array cap at 64 neighbours.

---

## 2. Hash-Based Relay Gate

In ADAPTIVE mode, only a deterministic subset of nodes relay each flood. Selection is based on a hash of the packet content XOR a per-node seed:

```
selected = hash(packet_seed XOR node_seed) % 100 < relay_pct
```

- `packet_seed`: first 4 bytes of packet payload (stable across all recipients of the same flood)
- `node_seed`: per-node constant — `node_idx × 0x9e3779b9` (in sim); firmware uses pub-key prefix

This is **zero-coordination**: every node independently makes the same relay/suppress decision for a given packet without any signalling. Different packets select different relay subsets, providing stochastic load balancing.

### Relay percentages by density tier

| Tier | Relay % | Rationale |
|--------|---------|-----------|
| SPARSE | 100% | Every relay counts; no redundancy to spare |
| MEDIUM | 25% | Enough redundancy to survive single relay failure |
| DENSE | 15% | Flood reaches all neighbours with 7–8 relayers in FM50; collisions reduced ≈ 60–70% vs DEFAULT |

These were derived empirically from `scenario_relay_pct_sweep` across FM50, FM100, Chain20, Grid5×5 topologies.

---

## 3. Relay Suppression

After transmitting or scheduling a relay, if a node overhears another node already successfully relaying the same flood (matched by packet hash), it cancels its own queued outbound. This catches the edge case where two ADAPTIVE-selected relays race.

Implemented in `SimNode::logRx()` via `_mgr_ref.removeOutboundByIdx()`.

---

## 4. Routing Strategies

Four strategies are available via `RoutingStrategy` enum in `RoutingStrategies.h`:

| Strategy | Description |
|----------|-------------|
| `DEFAULT` | Stock MeshCore: random backoff 0–5× base airtime. All nodes relay. |
| `SNR_WEIGHTED` | Nodes with better SNR relay sooner (lower backoff). |
| `PATH_SNR_HYBRID` | SNR-weighted backoff scaled by hop count — far nodes deprioritised. |
| `ADAPTIVE` | Hash-gate + density-aware relay pct + relay suppression. |

The `ADAPTIVE` strategy is the primary contribution. It matches or exceeds DEFAULT delivery rate while reducing total airtime by 60–70% in dense topologies.

---

## 5. Adaptive TX Power Saving

When `power_save_enabled = true`, ADAPTIVE-mode nodes reduce their transmit power based on current density tier:

| Tier | Default TX power | Example setting | TX current (SX1262) |
|--------|-----------------|-----------------|---------------------|
| SPARSE | 20 dBm (full) | 20 dBm | 120 mA |
| MEDIUM | configurable | 14 dBm | 45 mA |
| DENSE | configurable | 10 dBm | 25 mA |

TX power affects received SNR at all neighbours via a linear offset: `received_snr += (tx_power_dbm - 20.0)`.

**Capture effect**: reducing TX power in dense areas shrinks the interference radius — nearby nodes still receive well above LoRa sensitivity, while the node no longer stomps on distant clusters. The LoRa capture effect (6 dB threshold in SimRadio) means the stronger local signal always wins over a distant weaker interferer.

### Battery model (SX1262 typical)

```
tx_energy_mah = txCurrentMa(dbm) × airtime_s / 3600
rx_energy_mah = 5.5 mA × airtime_s / 3600
```

TX current lookup (`txCurrentMa()`):
- ≥ 20 dBm → 120 mA
- ≥ 17 dBm → 90 mA
- ≥ 14 dBm → 45 mA
- ≥ 10 dBm → 25 mA
- < 10 dBm → 15 mA

---

## 6. Empirical Results

### 6a. ADAPTIVE vs DEFAULT (FM50, SNR=8dB)

| Metric | DEFAULT | ADAPTIVE |
|--------|---------|----------|
| Delivery rate | 100% | 100% |
| Total TX packets | ~2500 | ~450 |
| Collisions | ~800 | ~120 |
| Airtime reduction | — | ≈ 65% |

### 6b. TX Power Saving (FM50, ADAPTIVE, 20 floods)

| Mode | Dense dBm | Med dBm | Delivery | Energy saving vs FULL |
|------|-----------|---------|----------|-----------------------|
| FULL_PWR | 20 | 20 | 100% | — |
| CONSERV | 14 | 17 | 100% | ≈ 20% |
| MODERATE | 10 | 14 | 100% | ≈ 35% |
| AGGRESSIVE | 7 | 10 | 100% | ≈ 45% |

TX power reduction does **not** hurt delivery in full-mesh or dense positional cluster topologies. Chain topologies (sparse by nature) stay at full power (SPARSE tier = 100% relay, full power always).

### 6c. Long chain (20 hops, SNR=6dB)

PATH_SNR_HYBRID outperforms DEFAULT on marginal multi-hop links (+5–8% DR). ADAPTIVE matches DEFAULT on chains because density stays SPARSE throughout — no relay suppression, all nodes relay at full power.

### 6d. Duty cycle enforcement

At 1% EU duty cycle (`duty_cycle_factor=99`), ADAPTIVE requires ≈ 40% fewer TX slots than DEFAULT, providing significantly more headroom before the duty cycle budget is exhausted.

### 6e. Legacy compatibility (mixed firmware)

Legacy nodes (DEFAULT strategy) interoperate fully. ADAPTIVE nodes relay legacy floods with the same hash gate. Legacy nodes relay ADAPTIVE-originated floods without suppression (they simply forward everything), which provides additional redundancy — no delivery regression observed.

---

## 7. Festival Weekend Battery Estimate

Assumptions: 50-node full-mesh, 1 flood/30s, SF8 BW62.5, SX1262, 2000 mAh battery.

| Mode | TX mAh/hr | RX mAh/hr | Radio total | Est. hours (radio only) |
|------|-----------|-----------|-------------|------------------------|
| FULL_PWR | 0.373 | 0.114 | 0.487 | ~4100 hrs |
| MODERATE | 0.078 | 0.114 | 0.192 | ~10400 hrs |
| AGGRESSIVE | 0.046 | 0.114 | 0.160 | ~12500 hrs |

**Note:** MCU idle current (~10–30 mA continuous) dominates in practice, adding ≈ 9.6 mAh/hr at 20 mA. On a 2000 mAh battery this limits runtime to ≈ 100–130 hours regardless of radio power level. TX power reduction remains valuable for: peak current management, heat reduction, and lowering the area RF noise floor during high-density events.

---

## 8. Configuration Reference

```cpp
// SimNode fields (set after addNode(), before run())
node->routing_strategy = RoutingStrategy::ADAPTIVE;
node->power_save_enabled = true;
node->power_save_dense_dbm = 10.0f; // TX power in DENSE tier
node->power_save_medium_dbm = 14.0f; // TX power in MEDIUM tier
node->full_power_dbm = 20.0f; // TX power in SPARSE tier (and non-ADAPTIVE)
node->duty_cycle_factor = 99.0f; // EU 1% duty cycle
node->p_forward = 1.0f; // manual relay probability (non-ADAPTIVE only)
```

Compile-time density thresholds (set in CMakeLists.txt or via `-D` flags):
```
DENSITY_WINDOW_MS (default: 60000)
DENSITY_SPARSE_MAX (default: 4)
DENSITY_DENSE_MIN (default: 15)
```
33 changes: 33 additions & 0 deletions sim/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Simulation Harness Changelog

## [Unreleased] — Adaptive Routing + TX Power Saving

### Added

#### Core Infrastructure
- **`SimRadio.h`**: Added `tx_power_dbm` field (public, default 20 dBm). Extended `TxCallback` signature to carry `tx_power_dbm` through to the bus. Existing capture-effect collision model (6 dB threshold) unchanged.
- **`SimBus.h`**: `InFlightPacket` now carries `tx_power_dbm`. `deliverPending()` applies `received_snr += (tx_power_dbm - 20.0)` at each receiver — lower TX power shrinks effective SNR without touching the channel model. Added `_tsnode_to_seq` compound-key map `(src_pub_key[0..3] << 32 | advert_ts)` for concurrent-flood tracking; prevents false metric attribution when two senders fire in the same simulated second.
- **`DensityEstimator.h`** (new): Passive sliding-window neighbour density estimator. Counts unique direct senders (hop_count==1) over a 60 s window. Returns SPARSE / MEDIUM / DENSE tier. No protocol messages required. Uses `std::unordered_set<uint32_t>` (no fixed cap).
- **`RoutingStrategies.h`**: Added `ADAPTIVE` to the `RoutingStrategy` enum. Added `adaptiveRelayPct()`, `adaptiveDelay()`, `hashBasedRelay()` helpers alongside existing `snrWeightedDelay()` and `pathSnrHybridDelay()`.
- **`SimNode.h`**: Added `routing_strategy` field (default `DEFAULT`). Added `DensityEstimator density` member fed from `logRx()`. Added power-save fields (`power_save_enabled`, `power_save_dense_dbm`, `power_save_medium_dbm`, `full_power_dbm`). Added battery energy tracking (`total_tx_energy_mah`, `total_rx_energy_mah`, `total_rx_time_ms`, `total_suppressed`). Added `static float txCurrentMa(float dbm)` and constants `RX_CURRENT_MA`, `IDLE_CURRENT_MA` matching SX1262 datasheet.
- **`scenario_adaptive.cpp`**: Validates ADAPTIVE vs DEFAULT vs PATH_SNR_HYBRID across FullMesh 50, Chain 20, Grid 5×5 at multiple SNR levels. Confirms ADAPTIVE reduces airtime ≈ 65% at equivalent delivery rate.
- **`scenario_concurrent.cpp`**: Tests 2–8 simultaneous flood sources. Confirms ADAPTIVE maintains delivery advantage in grid topologies under concurrent load; documents known limitation in adversarial full-mesh simultaneous-TX edge case.
- **`scenario_longchain.cpp`**: Tests 20-hop linear chains at marginal SNR. Confirms ADAPTIVE stays at SPARSE tier throughout and behaves identically to DEFAULT on chains.
- **`scenario_mixed.cpp`**: Tests ADAPTIVE nodes coexisting with DEFAULT (legacy) nodes. Confirms full interoperability with no delivery regression.
- **`scenario_dutycycle.cpp`**: Tests EU 1% duty cycle enforcement (`getAirtimeBudgetFactor()`). Confirms ADAPTIVE requires ≈ 40% fewer TX budget slots than DEFAULT.
- **`scenario_relay_pct_sweep.cpp`**: Grid sweep of DENSE% × MEDIUM% relay percentages across FM50, FM100, Chain20, Grid5×5. Identifies optimal operating point (DENSE=15%, MEDIUM=25%). Outputs CSV + ranked table.
- **`scenario_txpower.cpp`** (new): Tests adaptive TX power reduction at CONSERV / MODERATE / AGGRESSIVE levels across FM50, DC6x6 dense positional cluster, and CH20 chain. Measures delivery rate, latency, and per-node energy consumption. Includes 48-hour festival weekend battery projection.
- **`ADAPTIVE_ROUTING.md`** (new): Full design rationale, configuration reference, and empirical results.

### Fixed
- **`DensityEstimator`**: Replaced fixed `seen[64]` array with `std::unordered_set<uint32_t>` — eliminated silent 64-neighbour cap that could misclassify dense networks as MEDIUM.
- **`SimBus::onTransmit`**: Added `if (len > 255) len = 255` bounds guard before `memcpy` into 255-byte `InFlightPacket` buffer.
- **`SimNode::getRetransmitDelay`**: Fixed redundant TX power reset condition (`!power_save_enabled || strategy != ADAPTIVE` → `strategy != ADAPTIVE`).
- **`SimNode::getRetransmitDelay`**: Fixed p_forward gate floating-point edge: `>=` → `>` so `p_forward=1.0` never silently drops a relay.
- **`scenario_concurrent.cpp`**: Fixed stale pointer `ConcurrentResult* def_base = &group.back()` (invalidated by subsequent `push_back`) — replaced with value copy + `bool have_def` flag.
- **`scenario_relay_pct_sweep.cpp`**: Updated `addSweepNode` TxCallback from 4-arg to 5-arg to match updated `SimRadio` signature. Updated `on_recv` lambda to use `_tsnode_to_seq` compound key instead of old `_ts_to_seq`.
- **`scenario_adaptive.cpp`**: Removed unused variable `last_suppressed`.

### Changed
- `SimBus` protected members promoted from `private:` to `protected:` to allow `SweepBus` subclass access in `scenario_relay_pct_sweep.cpp`.
- `adaptiveRelayPct()`: DENSE raised 10% → 15%, MEDIUM raised 20% → 25% — improves resilience under concurrent multi-source floods without increasing airtime in the single-source case.
101 changes: 101 additions & 0 deletions sim/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
cmake_minimum_required(VERSION 3.16)
project(MeshCoreSim C CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# -------------------------------------------------------------------------
# Stub out Arduino-specific includes so MeshCore compiles on the host.
# -------------------------------------------------------------------------
add_library(arduino_stub INTERFACE)
target_include_directories(arduino_stub INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/stubs)

# -------------------------------------------------------------------------
# MeshCore core sources (no radio, no board, no Arduino peripherals)
# -------------------------------------------------------------------------
set(MESHCORE_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/..)

add_library(meshcore_core STATIC
${MESHCORE_ROOT}/src/Dispatcher.cpp
${MESHCORE_ROOT}/src/Mesh.cpp
${MESHCORE_ROOT}/src/Packet.cpp
${MESHCORE_ROOT}/src/Identity.cpp
${MESHCORE_ROOT}/src/Utils.cpp
${MESHCORE_ROOT}/src/helpers/unishox2.cpp
${MESHCORE_ROOT}/src/helpers/Compression.cpp
)

target_include_directories(meshcore_core PUBLIC
${MESHCORE_ROOT}/src
${CMAKE_CURRENT_SOURCE_DIR}/stubs
)

target_link_libraries(meshcore_core PUBLIC arduino_stub)

# Disable Arduino-specific debug macros
target_compile_definitions(meshcore_core PUBLIC
MESH_DEBUG=0
MESH_PACKET_LOGGING=0
BRIDGE_DEBUG=0
NDEBUG
ENABLE_COMPRESSION=1
)

# Force-include the Arduino stub so size_t and other types are available
# in MeshCore.h before any other headers are processed.
target_compile_options(meshcore_core PUBLIC
-include ${CMAKE_CURRENT_SOURCE_DIR}/stubs/Arduino.h
)

# -------------------------------------------------------------------------
# Ed25519 library (Nightcracker, needed for keypair/sign/verify)
# -------------------------------------------------------------------------
file(GLOB ED25519_SRCS ${MESHCORE_ROOT}/lib/ed25519/*.c)
add_library(ed25519 STATIC ${ED25519_SRCS})
target_include_directories(ed25519 PUBLIC ${MESHCORE_ROOT}/lib/ed25519)
target_link_libraries(meshcore_core PUBLIC ed25519)

# -------------------------------------------------------------------------
# rweather Crypto library (AES, SHA256, Ed25519 — Arduino-compatible)
# -------------------------------------------------------------------------
set(CRYPTO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/deps/arduinolibs/libraries/Crypto)
file(GLOB CRYPTO_SRCS
${CRYPTO_DIR}/AES128.cpp
${CRYPTO_DIR}/AESCommon.cpp
${CRYPTO_DIR}/BlockCipher.cpp
${CRYPTO_DIR}/Cipher.cpp
${CRYPTO_DIR}/Crypto.cpp
${CRYPTO_DIR}/Ed25519.cpp
${CRYPTO_DIR}/SHA256.cpp
${CRYPTO_DIR}/SHA512.cpp
${CRYPTO_DIR}/Hash.cpp
${CRYPTO_DIR}/HMAC.cpp
${CRYPTO_DIR}/Curve25519.cpp
${CRYPTO_DIR}/BigNumberUtil.cpp
${CRYPTO_DIR}/AuthenticatedCipher.cpp
)
add_library(crypto STATIC ${CRYPTO_SRCS} ${CMAKE_CURRENT_SOURCE_DIR}/stubs/RNG.cpp)
target_include_directories(crypto PUBLIC ${CRYPTO_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/stubs)
target_compile_definitions(crypto PUBLIC ARDUINO=100)
target_link_libraries(meshcore_core PUBLIC crypto)

# -------------------------------------------------------------------------
# Simulation harness headers
# -------------------------------------------------------------------------
add_library(meshcore_sim INTERFACE)
target_include_directories(meshcore_sim INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(meshcore_sim INTERFACE meshcore_core)

# -------------------------------------------------------------------------
# Scenarios — each scenario builds as its own binary
# -------------------------------------------------------------------------
file(GLOB SCENARIO_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/scenarios/*.cpp)
foreach(scenario_src ${SCENARIO_SRCS})
get_filename_component(scenario_name ${scenario_src} NAME_WE)
add_executable(${scenario_name} ${scenario_src})
target_link_libraries(${scenario_name} PRIVATE meshcore_sim crypto ed25519)
target_include_directories(${scenario_name} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/stubs
)
endforeach()
Loading