Skip to content

stachuman/meshcore_simv2

Repository files navigation

MeshCore Real Sim

A single-process network simulator for MeshCore LoRa mesh networks. Runs unmodified MeshCore firmware code against a configurable virtual radio layer with realistic RF propagation, collisions, and half-duplex constraints. Supports running multiple firmware versions simultaneously in the same simulation — useful for testing backward compatibility and mixed-version meshes.

Swim-lane event visualization

Geographic topology map

Interactive simulation view — live map, scrubber, and analytical overlays

Repository layout

orchestrator/          Multi-node simulator engine (C++) — main tool
  Orchestrator.cpp     Simulation loop, physics, collision detection
  JsonConfig.cpp       Config parser
  LuaEngine.cpp        Lua scripting bindings and event dispatch
  FirmwarePlugin.cpp   Plugin loader (dlopen/dlsym)
  firmware/            Firmware plugin sources (node factories, exports)
shims/                 Platform shim layer (Arduino, FS, crypto, radio)
simple_repeater/       Standalone single-repeater binary
companion_radio/       Standalone single-companion binary

MeshCore/              MeshCore firmware sources (not in repo; fetched by
                       `tools/firmware.py init`, never modified)
MeshCore-*/            Alternate firmware trees (e.g. -1.13, -default-scope)
                       used for multi-firmware testing — see docs/MULTI_FIRMWARE.md

topology_generator/    Python module — ITM-based topology generation from live network
tools/                 Python helpers — convert, inject, grid-generate, run, analyze
visualization/         Python + HTML — swim-lane + map viewer (visualize.py)
webapp/                Browser UI (FastAPI + vanilla JS + Docker)

test/                  t*.json test configs, run_tests.sh harness, Lua integration tests
simulation/            Generated topology + scenario configs (most regeneratable;
                       see "Working with Real Topologies" below)
docs/                  Config format, radio model, Lua API, multi-firmware guide
delay_optimization_v2/ Experimental sweep harness for delay parameter tuning
                       across network densities; produces CSV result tables

Building

Requires CMake 3.16+, a C++17 compiler, OpenSSL development libraries, Python 3.10+, and liblua5.4-dev (for Lua scripting, enabled by default).

First-time setup (unoptimized development build)

# Install system dependencies (Debian/Ubuntu)
sudo apt install build-essential cmake libssl-dev liblua5.4-dev python3 python3-venv

# Clone the repo
git clone https://github.com/stachuman/meshcore_simv2.git
cd meshcore_simv2

# Set up Python virtual environment
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

# Clone all MeshCore firmware sources (from firmware.json registry)
python3 tools/firmware.py init

# Build orchestrator + all firmware plugins
python3 tools/firmware.py build

# Quick run - simulation and then visualization
./tools/run_sim.sh ./test/t34_delay_bench_butterfly

Optimized Builds

For production use, build with optimization flags:

NativeRelease (maximum performance, local machine only):

cmake -S . -B build-native -DCMAKE_BUILD_TYPE=NativeRelease
cmake --build build-native -j$(nproc)
  • Uses -O3 -march=native -flto=auto -ffast-math
  • 4-6x faster than unoptimized builds
  • CPU-specific (not portable to different CPUs)
  • Used automatically by delay_optimization scripts

Release (portable, Docker-friendly):

cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release
cmake --build build-release -j$(nproc)
  • Uses -O3 -flto=auto
  • 2-3x faster than unoptimized builds
  • Portable across x86_64 CPUs
  • Used by Docker builds

Firmware Management

All MeshCore firmware sources are tracked in firmware.json. Use tools/firmware.py to manage them:

python3 tools/firmware.py list                  # show registered sources
python3 tools/firmware.py status                # detailed git/build status
python3 tools/firmware.py add myfork https://github.com/user/MeshCore.git --branch dev
python3 tools/firmware.py update                # git pull all sources
python3 tools/firmware.py build --build-type Release -j$(nproc)

To use a different MeshCore tree without the registry, point MESHCORE_DIR directly:

cmake -S . -B build -DMESHCORE_DIR=/path/to/MeshCore-fork

For multi-firmware testing (running nodes with different MeshCore versions simultaneously), see docs/MULTI_FIRMWARE.md.

The Python venv must be active when running visualization or topology tools (source venv/bin/activate).

Lua Scripting

Lua scripting is enabled by default (-DENABLE_LUA=ON). To disable it (removes the liblua5.4-dev dependency):

cmake -S . -B build -DENABLE_LUA=OFF

See docs/LUA_SCRIPTING.md for the full API reference, examples, and usage guide.

This produces three binaries:

  • build/orchestrator/orchestrator -- the multi-node simulator (main tool)
  • build/simple_repeater/simple_repeater -- standalone single-repeater binary
  • build/companion_radio/companion_radio -- standalone single-companion binary

Running simulations

Run a test config

build/orchestrator/orchestrator test/t02_hot_start_msg.json

NDJSON event log goes to stdout, summary and assertions to stderr.

Run with visualization

tools/run_sim.sh test/t06_msg_stats.json

This runs the orchestrator, saves events as t06_msg_stats_events.ndjson, and opens an interactive swim-lane visualizer in the browser (plus a topology map view if the config has coordinates).

Run all tests

bash test/run_tests.sh

Discovers all test/t*.json files and reports pass/fail.

Interactive mode

Step through the simulation manually and use Lua for ad-hoc queries:

build/orchestrator/orchestrator -i test/t01_hot_start_neighbors.json
[   0.000s] > step 6000
> {"events":0,"finished":false,"stepped_to_ms":6000}
[   6.000s] > lua sim:for_each_repeater(function(n,s) log(n..": "..s.neighbor_count.." neighbors") end)
[lua] relay1: 2 neighbors
[   6.000s] > lua local s = sim:msg_stats("alice"); log("sent="..s.total_sent)
[lua] sent=0

Lua script mode

Drive the simulation entirely from a Lua script:

build/orchestrator/orchestrator --lua test/lua/test_basic.lua test/t01_hot_start_neighbors.json

See docs/LUA_SCRIPTING.md for the full Lua API.

Simulation Config

Configs are JSON files with these sections:

{
  "simulation": {
    "duration_ms": 90000,
    "step_ms": 4,
    "warmup_ms": 5000,
    "hot_start": true
  },
  "nodes": [
    { "name": "alice", "role": "companion" },
    { "name": "relay1", "role": "repeater" },
    { "name": "bob", "role": "companion" }
  ],
  "topology": {
    "links": [
      { "from": "alice", "to": "relay1", "snr": 8.0, "rssi": -80.0, "bidir": true },
      { "from": "relay1", "to": "bob", "snr": 8.0, "rssi": -80.0, "bidir": true }
    ]
  },
  "commands": [
    { "at_ms": 6000, "node": "alice", "command": "msg bob hello" }
  ],
  "expect": [
    { "type": "cmd_reply_contains", "node": "alice", "command": "msg bob", "value": "msg sent to bob" }
  ]
}
  • hot_start -- injects mutual node awareness at t=0 (skips the slow advert exchange)
  • warmup_ms -- instant packet delivery during warmup (no collisions/physics)
  • message_schedule -- auto-generates periodic msg/msga commands (supports "ack": true)

See docs/CONFIG_FORMAT.md for full reference.

Working with Real Topologies

Generate simulation configs from real-world node data using the topology generator -- a two-stage pipeline:

  1. topology_generator -- fetches live node positions from the MeshCore network API, computes RF links using the ITM propagation model, and outputs a topology config
  2. inject_test.py -- places companion nodes, generates message schedules, and produces a ready-to-run simulation config

Quick start: Gdansk region test

The included prepare_gdansk_test.sh runs the full pipeline:

bash prepare_gdansk_test.sh
build/orchestrator/orchestrator simulation/gdansk_test.json

This fetches ~100 repeaters from the Gdansk/Pomerania region, computes ITM-based RF links, places 4 companions on well-connected repeaters, and generates a 15-minute test with direct messages and channel broadcasts.

Running the pipeline manually

# Step 1: Generate topology from live network data
python3 -m topology_generator \
    --region 53.7,17.3,54.8,19.5 \
    --freq-mhz 869.618 --tx-power-dbm 20.0 --antenna-height 5.0 \
    --sf 8 --bw 62500 --cr 4 \
    --max-distance-km 40 --min-snr -10.0 \
    --max-edges-per-node 12 --link-survival 0.4 \
    --clutter-db 6.0 \
    -v -o simulation/gdansk_topology.json

# Step 2: Inject companions and message schedules
python3 tools/inject_test.py simulation/gdansk_topology.json \
    --companions 4 --companion-names alice,bob,carol,dave \
    --min-neighbors 2 \
    --auto-schedule --channel \
    --msg-interval 70 --msg-count 5 \
    --chan-interval 80 --chan-count 4 \
    --duration 900000 \
    -v -o simulation/gdansk_test.json

# Step 3: Run and visualize
tools/run_sim.sh simulation/gdansk_test.json -v

Key topology generator flags:

Flag Default Description
--region (required) Bounding box: lat_min,lon_min,lat_max,lon_max
--freq-mhz 869.618 LoRa carrier frequency
--max-distance-km 40 Skip node pairs beyond this range
--min-snr -10.0 Drop links below this SNR
--link-survival 1.0 Stochastic link survival probability (0.4 = realistic sparsity)
--clutter-db 6.0 Additional attenuation for urban/suburban clutter
--max-edges-per-node 12 Safety cap on neighbor count
--api-cache none Cache API responses to avoid repeated downloads

See docs/TOPOLOGY_GENERATOR.md for full documentation.

Multi-firmware support

Nodes in a single simulation can run different MeshCore firmware versions simultaneously. Each firmware version is compiled into its own shared-library plugin (fw_<name>.so); nodes pick their plugin per-config via the firmware field. This is the core feature for testing backward compatibility, rollout transitions, and mixed-version mesh behavior.

Example: v1.13 and default-scope running side by side

# Clone the variants alongside this repo (firmware trees are gitignored; use
# whatever MeshCore branches/forks you care about)
git clone -b v1.13          https://github.com/ripplebiz/MeshCore.git MeshCore-1.13
git clone -b default-scope  https://github.com/ripplebiz/MeshCore.git MeshCore-default-scope

# Build the orchestrator with two extra plugins on top of fw_default
cmake -S . -B build \
    -DFIRMWARE_PLUGINS="v113=$(pwd)/MeshCore-1.13;scope=$(pwd)/MeshCore-default-scope"
cmake --build build -j$(nproc)

This produces three plugins next to the orchestrator binary: fw_default.so, fw_v113.so, fw_scope.so. A config can then mix them:

"nodes": [
  { "name": "alice", "role": "companion", "firmware": "fw_v113" },
  { "name": "relay", "role": "repeater",  "firmware": "fw_scope" },
  { "name": "bob",   "role": "companion" }
]

Alice runs v1.13, relay runs the default-scope branch, bob runs whatever fw_default was built from — all three exchange packets over the same virtual radio. A test can require specific plugins via the top-level _requires_plugins field; the test runner skips (with a clear message) when they aren't built.

See docs/MULTI_FIRMWARE.md for the full story: per-plugin include isolation, how dlopen keeps symbol tables separate, version-string detection, and troubleshooting.

A-vs-B comparison against a single fork

For a simpler "my fork vs. stock" test (not mixed simulation), build a separate tree with MESHCORE_DIR:

git clone https://github.com/YOUR_USER/MeshCore.git ../MeshCore-fork
cmake -S . -B build-fork -DMESHCORE_DIR=$(pwd)/../MeshCore-fork
cmake --build build-fork

build/orchestrator/orchestrator      test/t06_msg_stats.json 2>/tmp/stock.txt
build-fork/orchestrator/orchestrator test/t06_msg_stats.json 2>/tmp/fork.txt
diff /tmp/stock.txt /tmp/fork.txt

MESHCORE_DIR is cached — after switching branches in the fork tree, just cmake --build build-fork again, no reconfigure needed.

Radio Physics Model

The simulator models realistic LoRa radio behavior:

  • LoRa airtime -- exact symbol/preamble/payload timing for any SF/BW/CR combination
  • Half-duplex -- nodes cannot transmit while receiving (and vice versa)
  • Collisions -- 3-stage survival: capture effect (6dB), preamble grace window, FEC tolerance
  • Listen-before-talk -- preamble detection delay, SNR-gated channel busy notifications
  • SNR variance -- per-link Gaussian sampling (snr_std_dev)
  • Stochastic loss -- per-link drop probability (loss)
  • Adversarial modes -- per-node packet drop, bit corruption, or delayed replay
  • Message fate tracking -- follows each message through the relay chain, counting per-message collisions, drops, and ACK/PATH copies reaching the sender to diagnose delivery failures and ack efficiency

Delay Optimization

The delay_optimization_v2/ directory contains Lua-based scripts for sweeping MeshCore delay parameters (e.g. FLOOD_DELAY_MS, ADV_INTERVAL_MS) across different network densities (sparse, medium, dense, very dense). Each density has a pre-built topology and test config; run_sweep.sh drives the parameter grid search and outputs CSV results.

Visualization

The visualizer serves two interactive views:

python3 visualization/visualize.py events.ndjson --config config.json
  • Swim-lane view (port 8000) -- timeline of TX/RX/collision events per node, with packet tracing
  • Map view (port 8001) -- geographic topology with link SNR coloring (requires --config with node coordinates)

Controls: scroll to zoom, drag to pan, click packets for details, press T for spread-tree trace.

Test Generator

Generate grid topology tests with configurable dimensions:

python3 tools/gen_grid_test.py --rows 5 --cols 5 -n 4 -o test/t_custom_grid.json

Creates a repeater grid with companion nodes at the corners, auto-generates cross-grid messaging commands and discovery assertions.

Web App & Docker

A browser-based UI for the full simulation workflow -- from topology creation through simulation execution to result visualization.

Features:

  • Topology Creator -- generate topologies from the live MeshCore network API with ITM propagation modeling, or create manually
  • Topology Editor -- selection-driven node/link editing with an interactive map, SNR-colored links, drag-to-reposition
  • Scenario Editor -- visual config builder: define nodes, links, commands, message schedules, and assertions
  • Batch Simulations -- run simulations with real-time progress via SSE, view results in swim-lane and map visualizations
  • Interactive Simulations -- step-through investigation with swim-lane + live map side-by-side, scrubbable history, per-TX outcome overlays, collision/radio-busy heatmaps, TX effectiveness bars, and animated flood-propagation trees. See below for details.
  • Config Library -- save, duplicate, import/export simulation configs

No database (filesystem-only storage), no build step (CDN-served JS libraries).

Interactive simulation view

Live step-through investigation of a running sim with a swim-lane and map side-by-side. Send commands, advance the clock, watch the network react event-by-event. Screenshot at the top of this README.

Live visualization:

  • Swim-lane timeline (left) with per-node event lanes, airtime bars, packet tracing, and filterable event log
  • Geographic map (right) with live node state (idle / TX / RX / collision flashes)
  • Per-TX overlay lines colored by outcome:
    • green solid = delivered to this receiver
    • orange dashed = collision (with dark halo for visibility; tooltip shows interferer + capture margin)
    • red dashed = SNR below demod threshold
    • magenta dashed = stochastic link-loss roll
    • gray dashed = receiver was half-duplex busy
  • Hover any overlay for per-receiver SNR / margin / interferer details

History + scrubbable replay:

  • Rolling 2-minute event buffer in the frontend, with a density timeline (histogram of events/bucket) below the map
  • Click or drag the timeline to scrub to any past moment — the map freezes, overlays re-render as they were at that time
  • "Resume live" button returns to following the current frontier
  • Scrub-time syncs with the swim-lane viewport automatically

Analytical overlays (toggle independently in the map legend):

  • Radio busy — bars — per-node % of radio occupation (TX + RX), vertical bar at each node with color thresholds (green <10%, amber 10-30%, red >30%)
  • Radio busy — heatmap — same data as diffuse blue→purple spatial blobs, distinct palette so it composes cleanly with other overlays
  • Collision heatmap — amber→red hot zones on nodes that suffered collisions, plus an exact per-node collision count badge (log-scaled intensity so small counts stay visible)
  • TX effectiveness — % of each node's transmissions that reached ≥1 neighbor, inverse color (green = healthy, red = wasted airtime); answers "is this node talking into the void?"

Flood-propagation tree:

  • Click any TX overlay (on the map or in the swim-lane) to trace its full hop-by-hop spread
  • Animated expansion at real sim-time pacing: ★ marks the root sender, numbered ● dots appear at each arrival node in order with timing labels ("Hop 3 at relay1 +412 ms")
  • Cyan lines for relay hops, purple lines for response (PATH-return) hops
  • "Clear trace" button or click another TX to replace

Cross-view sync:

  • Click a TX box / node label in the swim-lane → the map flies to that node with a pulse ring
  • Drag the map's time scrubber → swim-lane viewport recenters on the scrubbed time
  • Trace a packet in either view → the other view renders the same tree

Sessions auto-terminate when you navigate away; a 60-second idle fallback handles unclean shutdowns.

Run locally (development)

cd webapp
pip install -r requirements.txt
uvicorn server.main:app --port 8000

Open http://localhost:8000

Docker image

Build requires the Release orchestrator binary and firmware plugins first:

# 1. Build orchestrator + all firmware plugins (Release)
python3 tools/firmware.py build --build-dir build-release --build-type Release -j$(nproc)

# 2. Build the Docker image
cd webapp
docker compose build

Run locally:

docker compose up

Deploy to another machine (e.g. NAS)

# On the build machine — save image to a tarball
docker save meshcore-sim:latest | gzip > meshcore-sim.tar.gz

# Copy to target
scp meshcore-sim.tar.gz user@nas-ip:/share/docker/

# On the target machine — load and run
docker load < meshcore-sim.tar.gz
docker run -d --name meshcore-sim \
    -p 8000:8000 \
    -v meshcore-data:/app/data \
    --restart unless-stopped \
    meshcore-sim:latest

The -v meshcore-data:/app/data volume persists simulation data across container restarts.

Note: The image is x86_64. The target machine must have an Intel/AMD CPU (most QNAP/Synology NAS models do).

About

MeshCore realistic simulator

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors