mirror of https://github.com/meshcore-dev/MeshCore
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.
321 lines
12 KiB
321 lines
12 KiB
#pragma once
|
|
#include <Mesh.h>
|
|
#include <Identity.h>
|
|
#include "SimRuntime.h"
|
|
#include "SimRadio.h"
|
|
#include "RoutingStrategies.h"
|
|
#include "DensityEstimator.h"
|
|
#include <string>
|
|
#include <vector>
|
|
#include <functional>
|
|
|
|
namespace sim {
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Per-packet delivery event — recorded whenever a node receives a message.
|
|
// -------------------------------------------------------------------------
|
|
struct DeliveryEvent {
|
|
int src_node;
|
|
int dst_node; // -1 = broadcast/flood
|
|
int recv_node;
|
|
uint8_t payload_type;
|
|
uint8_t route_type;
|
|
uint8_t hop_count;
|
|
float snr;
|
|
float rssi;
|
|
uint64_t tx_time_ms;
|
|
uint64_t rx_time_ms;
|
|
uint32_t latency_ms;
|
|
uint32_t airtime_ms;
|
|
bool is_duplicate;
|
|
};
|
|
|
|
// -------------------------------------------------------------------------
|
|
// SimNode — a full MeshCore node running in simulation.
|
|
// Subclasses Mesh to intercept packets and record metrics.
|
|
// -------------------------------------------------------------------------
|
|
class SimNode : public mesh::Mesh {
|
|
public:
|
|
int node_idx;
|
|
std::string name;
|
|
|
|
// Routing strategy — set before first flood, after addNode()
|
|
RoutingStrategy routing_strategy = RoutingStrategy::DEFAULT;
|
|
|
|
// Probabilistic forwarding — probability [0,1] that this node relays a flood.
|
|
// 1.0 = always forward (default). Values < 1.0 randomly drop relays to reduce
|
|
// broadcast storms at high node density. ADAPTIVE strategy overrides this
|
|
// dynamically each packet based on current density tier.
|
|
float p_forward = 1.0f;
|
|
|
|
// Passive neighbor density estimator — fed by logRx(), consumed by getRetransmitDelay().
|
|
DensityEstimator density;
|
|
|
|
// Metrics
|
|
std::vector<DeliveryEvent> deliveries;
|
|
uint32_t total_tx_packets = 0;
|
|
uint32_t total_rx_packets = 0;
|
|
uint32_t total_duplicates = 0;
|
|
uint64_t total_airtime_ms = 0;
|
|
uint32_t total_suppressed = 0; // relay cancellations from suppression logic
|
|
|
|
// last advert tracking — used by SimBus to match receipts to injections
|
|
uint32_t last_advert_ts = 0;
|
|
uint32_t last_advert_src_key = 0; // first 4 bytes of sender pub key (unique per node)
|
|
|
|
// Callback: called when any message payload arrives at this node
|
|
using OnRecvCallback = std::function<void(SimNode*, const DeliveryEvent&)>;
|
|
OnRecvCallback on_recv;
|
|
|
|
SimNode(int idx, const std::string& name,
|
|
SimRadio& radio, SimMillisClock& ms, SimRNG& rng,
|
|
SimRTCClock& rtc, SimPacketManager& mgr, SimTables& tables)
|
|
: mesh::Mesh(radio, ms, rng, rtc, mgr, tables)
|
|
, node_idx(idx), name(name)
|
|
, _radio_ref(radio), _ms_ref(ms), _rng_ref(rng), _mgr_ref(mgr), _tables_ref(tables)
|
|
{}
|
|
|
|
void init() {
|
|
// LocalIdentity(RNG*) generates a fresh Ed25519 keypair using the node's seeded RNG
|
|
self_id = mesh::LocalIdentity(&_rng_ref);
|
|
begin();
|
|
}
|
|
|
|
bool allowPacketForward(const mesh::Packet* pkt) override {
|
|
return true; // repeater mode — all nodes forward
|
|
}
|
|
|
|
// Duty cycle enforcement — override to set budget factor.
|
|
// duty_cycle = 1 / (1 + factor):
|
|
// factor=1 → 50% (default, no enforcement)
|
|
// factor=99 → 1% (EU legal limit)
|
|
// factor=9 → 10% (relaxed US usage)
|
|
// Default: no enforcement (matches stock MeshCore behaviour).
|
|
float duty_cycle_factor = 1.0f; // set externally to enforce a limit
|
|
float getAirtimeBudgetFactor() const override { return duty_cycle_factor; }
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Power-save mode — reduces TX power when density is DENSE or MEDIUM.
|
|
//
|
|
// power_save_enabled: activates adaptive TX power reduction
|
|
// power_save_dense_dbm: TX power (dBm) when DENSE (default 20 = full power)
|
|
// power_save_medium_dbm: TX power (dBm) when MEDIUM
|
|
//
|
|
// Typical LoRa TX current:
|
|
// +20 dBm → ~120 mA (SX1262 PA boost)
|
|
// +17 dBm → ~90 mA
|
|
// +14 dBm → ~45 mA
|
|
// +10 dBm → ~25 mA
|
|
//
|
|
// Battery model: energy_mah = sum over all TX of (tx_current_mA × airtime_s / 3600)
|
|
// + rx_current_mA × total_rx_time_s / 3600
|
|
// -----------------------------------------------------------------------
|
|
bool power_save_enabled = false;
|
|
float power_save_dense_dbm = 10.0f; // -10 dBm from 20 dBm max
|
|
float power_save_medium_dbm = 14.0f; // -6 dBm from 20 dBm max
|
|
float full_power_dbm = 20.0f; // reference full power
|
|
|
|
// Energy accounting (milliamp-hours consumed)
|
|
float total_tx_energy_mah = 0.0f;
|
|
float total_rx_energy_mah = 0.0f;
|
|
uint64_t total_rx_time_ms = 0; // time spent in RX mode (not TX)
|
|
|
|
// Current draw model (mA) — SX1262-based typical values
|
|
static float txCurrentMa(float dbm) {
|
|
if (dbm >= 20.0f) return 120.0f;
|
|
if (dbm >= 17.0f) return 90.0f;
|
|
if (dbm >= 14.0f) return 45.0f;
|
|
if (dbm >= 10.0f) return 25.0f;
|
|
return 15.0f; // low power / beacon mode
|
|
}
|
|
static constexpr float RX_CURRENT_MA = 5.5f; // SX1262 in RX continuous
|
|
static constexpr float IDLE_CURRENT_MA = 1.6f; // SX1262 sleep/standby
|
|
|
|
protected:
|
|
// -----------------------------------------------------------------------
|
|
// Routing strategy dispatch — overrides base mesh::Mesh backoff.
|
|
// -----------------------------------------------------------------------
|
|
uint32_t getRetransmitDelay(const mesh::Packet* pkt) override {
|
|
uint64_t now_ms = (uint64_t)_ms_ref.getMillis();
|
|
|
|
if (routing_strategy == RoutingStrategy::ADAPTIVE) {
|
|
DensityTier tier = density.tier(now_ms);
|
|
int relay_pct = adaptiveRelayPct(tier);
|
|
|
|
if (relay_pct < 100) {
|
|
// Deterministic hash-based relay selection.
|
|
// packet_seed: first 4 bytes of payload (stable across all recipients).
|
|
// node_seed: stable per-node identity (pub key prefix in firmware).
|
|
uint32_t pkt_seed = 0, node_seed = (uint32_t)node_idx * 0x9e3779b9u;
|
|
if (pkt->getRawLength() >= 4) memcpy(&pkt_seed, pkt->payload, 4);
|
|
|
|
if (!hashBasedRelay(pkt_seed, node_seed, relay_pct)) {
|
|
return 999999u; // not selected — silent
|
|
}
|
|
}
|
|
|
|
// Power-save: selected relay nodes reduce TX power based on density tier.
|
|
// This lowers received SNR at distant nodes (shrinks interference radius)
|
|
// while nearby nodes still receive well above LoRa sensitivity.
|
|
if (power_save_enabled) {
|
|
switch (tier) {
|
|
case DensityTier::DENSE:
|
|
_radio_ref.tx_power_dbm = power_save_dense_dbm;
|
|
break;
|
|
case DensityTier::MEDIUM:
|
|
_radio_ref.tx_power_dbm = power_save_medium_dbm;
|
|
break;
|
|
case DensityTier::SPARSE:
|
|
_radio_ref.tx_power_dbm = full_power_dbm;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Non-ADAPTIVE strategies always transmit at full power
|
|
if (routing_strategy != RoutingStrategy::ADAPTIVE) {
|
|
_radio_ref.tx_power_dbm = full_power_dbm;
|
|
}
|
|
|
|
// Manual p_forward gate (for non-ADAPTIVE strategies or explicit p_forward override)
|
|
if (routing_strategy != RoutingStrategy::ADAPTIVE && p_forward < 1.0f) {
|
|
uint32_t roll = _rng_ref.nextInt(0, 10000);
|
|
if ((float)roll / 10000.0f > p_forward) {
|
|
return 999999u;
|
|
}
|
|
}
|
|
|
|
uint32_t base = (_radio_ref.getEstAirtimeFor(pkt->getRawLength()) * 52 / 50) / 2;
|
|
if (base == 0) base = 10;
|
|
|
|
float snr = _radio_ref.getLastSNR();
|
|
uint8_t hops = pkt->getPathHashCount();
|
|
uint32_t rv = _rng_ref.nextInt(0, 10000);
|
|
|
|
switch (routing_strategy) {
|
|
case RoutingStrategy::SNR_WEIGHTED:
|
|
return snrWeightedDelay(base, snr, rv);
|
|
|
|
case RoutingStrategy::PATH_SNR_HYBRID:
|
|
return pathSnrHybridDelay(base, snr, hops, rv);
|
|
|
|
case RoutingStrategy::ADAPTIVE:
|
|
return adaptiveDelay(base, density.tier(now_ms), snr, hops, rv);
|
|
|
|
case RoutingStrategy::DEFAULT:
|
|
default:
|
|
return _rng_ref.nextInt(0, 5) * base;
|
|
}
|
|
}
|
|
|
|
|
|
void logRx(mesh::Packet* pkt, int len, float score) override {
|
|
total_rx_packets++;
|
|
uint32_t at = _radio_ref.getEstAirtimeFor(len);
|
|
total_airtime_ms += at;
|
|
total_rx_time_ms += at;
|
|
total_rx_energy_mah += RX_CURRENT_MA * (float)at / 3600000.0f;
|
|
|
|
if (!pkt || len < 4) return;
|
|
|
|
uint64_t now_ms = (uint64_t)_ms_ref.getMillis();
|
|
|
|
// Feed density estimator — use the actual transmitting node's ID so each
|
|
// relay of the same flood counts as a separate sender observation.
|
|
// In firmware, this would use a hash of the received frame's sync word or
|
|
// a time-of-arrival fingerprint; in sim we have the exact sender index.
|
|
{
|
|
int sender = _radio_ref.getLastSender();
|
|
uint32_t src_id = sender >= 0 ? (uint32_t)sender : 0u;
|
|
density.observe(src_id, pkt->getPathHashCount(), now_ms);
|
|
}
|
|
|
|
// Relay suppression — cancel a queued outbound if we hear another node
|
|
// successfully relay the same flood (hash-matched). Works for all strategies.
|
|
// In ADAPTIVE, the hash gate already prevents most nodes from queuing at all;
|
|
// this catches the edge case where two selected relays race.
|
|
if (pkt->isRouteFlood()) {
|
|
uint8_t pkt_hash[MAX_HASH_SIZE];
|
|
pkt->calculatePacketHash(pkt_hash);
|
|
|
|
int total = _mgr_ref.getOutboundTotal();
|
|
for (int i = total - 1; i >= 0; i--) {
|
|
mesh::Packet* queued = _mgr_ref.getOutboundByIdx(i);
|
|
if (!queued) continue;
|
|
uint8_t q_hash[MAX_HASH_SIZE];
|
|
queued->calculatePacketHash(q_hash);
|
|
if (memcmp(pkt_hash, q_hash, MAX_HASH_SIZE) == 0) {
|
|
_mgr_ref.removeOutboundByIdx(i);
|
|
total_suppressed++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void logTx(mesh::Packet* pkt, int len) override {
|
|
total_tx_packets++;
|
|
uint32_t at = _radio_ref.getEstAirtimeFor(len);
|
|
total_airtime_ms += at;
|
|
// Energy: TX current × airtime. Current depends on TX power at moment of transmission.
|
|
float current_ma = txCurrentMa(_radio_ref.tx_power_dbm);
|
|
total_tx_energy_mah += current_ma * (float)at / 3600000.0f;
|
|
}
|
|
|
|
void onAdvertRecv(mesh::Packet* pkt, const mesh::Identity& id,
|
|
uint32_t timestamp, const uint8_t* app_data, size_t app_data_len) override
|
|
{
|
|
last_advert_ts = timestamp;
|
|
memcpy(&last_advert_src_key, id.pub_key, 4);
|
|
recordDelivery(pkt, PAYLOAD_TYPE_ADVERT, -1);
|
|
}
|
|
|
|
void onPeerDataRecv(mesh::Packet* pkt, uint8_t type, int sender_idx,
|
|
const uint8_t* secret, uint8_t* data, size_t len) override
|
|
{
|
|
recordDelivery(pkt, type, sender_idx);
|
|
}
|
|
|
|
void onGroupDataRecv(mesh::Packet* pkt, uint8_t type, const mesh::GroupChannel& ch,
|
|
uint8_t* data, size_t len) override
|
|
{
|
|
recordDelivery(pkt, type, -1);
|
|
}
|
|
|
|
void onRawDataRecv(mesh::Packet* pkt) override {
|
|
recordDelivery(pkt, PAYLOAD_TYPE_RAW_CUSTOM, -1);
|
|
}
|
|
|
|
private:
|
|
public:
|
|
SimRadio& _radio_ref;
|
|
SimMillisClock& _ms_ref;
|
|
SimRNG& _rng_ref;
|
|
SimPacketManager& _mgr_ref;
|
|
SimTables& _tables_ref;
|
|
|
|
void recordDelivery(mesh::Packet* pkt, uint8_t payload_type, int sender_idx) {
|
|
uint64_t now = (uint64_t)_ms_ref.getMillis();
|
|
|
|
DeliveryEvent ev;
|
|
ev.src_node = sender_idx;
|
|
ev.dst_node = -1;
|
|
ev.recv_node = node_idx;
|
|
ev.payload_type = payload_type;
|
|
ev.route_type = pkt->getRouteType();
|
|
ev.hop_count = pkt->getPathHashCount();
|
|
ev.snr = pkt->getSNR();
|
|
ev.rssi = _radio_ref.getLastRSSI();
|
|
ev.tx_time_ms = 0; // filled in by SimBus when known
|
|
ev.rx_time_ms = now;
|
|
ev.latency_ms = 0;
|
|
ev.airtime_ms = _radio_ref.getEstAirtimeFor(pkt->getRawLength());
|
|
ev.is_duplicate = false;
|
|
|
|
deliveries.push_back(ev);
|
|
|
|
if (on_recv) on_recv(this, ev);
|
|
}
|
|
};
|
|
|
|
}
|
|
|