Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d12c62f
feat(sim): add map and terrain inputs
Komzpa May 2, 2026
2d0cdfb
fix(sim): handle uncovered SRTM scenario bbox
Komzpa May 3, 2026
63bbdd1
fix(sim): clarify map terrain import defaults
Komzpa May 5, 2026
efdb5ed
fix(sim): preserve map altitude during terrain recompute
Komzpa May 5, 2026
8568faf
feat(sim): import local NodeDB positions
Komzpa May 6, 2026
6e619a2
fix(sim): validate nodedb port and edge srtm tiles
Komzpa May 6, 2026
59fe5d6
fix(sim): sample srtm edge coordinates from existing tiles
Komzpa May 6, 2026
fade708
fix(sim): limit auto srtm tiles to reachable paths
Komzpa May 6, 2026
5645d61
docs(sim): clarify nodedb and srtm usage
Komzpa May 6, 2026
cf1ab3e
fix(sim): fit bounds to imported node coordinates
Komzpa May 6, 2026
7fd31e9
fix(sim): keep bounds unchanged on rejected terrain imports
Komzpa May 6, 2026
a7f0399
fix(sim): adapt terrain inputs to current radio config
Komzpa May 15, 2026
5351c12
fix(sim): reset imported bounds and cap terrain cache
Komzpa May 17, 2026
7b37280
fix(sim): preserve numeric NodeDB roles
Komzpa May 17, 2026
f3afb94
fix(sim): harden terrain input reuse
Komzpa May 18, 2026
a71e586
fix(sim): decode near-zero integer coordinates
Komzpa May 18, 2026
362527a
fix(sim): distinguish scaled integer coordinates
Komzpa May 18, 2026
4c9d024
fix(sim): harden map altitude and role imports
Komzpa Aug 17, 2026
4db15ff
fix(sim): reject unusable scenario origin blocks
Komzpa Aug 17, 2026
81ad320
fix(sim): isolate concurrent srtm tile downloads
Komzpa Aug 17, 2026
64cc94c
fix(sim): invalidate terrain loss cache across grid swaps
Komzpa Aug 17, 2026
a249052
perf(sim): prefilter srtm link pairs by maximum flat range
Komzpa Aug 17, 2026
b0530ee
fix(sim): bound nodedb TCP connection attempts
Komzpa Aug 17, 2026
a5d3f53
feat(sim): add land-cover clutter inputs
Komzpa May 2, 2026
5fed3a7
feat(sim): add capture-aware rf physics
Komzpa May 2, 2026
46b59d0
fix(sim): stabilize moving capture snapshots
Komzpa May 15, 2026
565281e
feat(sim): add batumi radio preset
Komzpa May 2, 2026
c9a5610
feat(sim): add radio policy compare workflow
Komzpa May 2, 2026
2a682cc
fix(sim): keep radio compare slice self-contained
Komzpa May 14, 2026
3c343dd
feat(sim): add dynamic coding rate policy
Komzpa May 2, 2026
bbcd09d
fix(sim): allow cold-start rescue coding retries
Komzpa May 24, 2026
a410d42
fix(sim): break clutter terrain import cycle
Komzpa Aug 21, 2026
f1113a6
fix(sim): defer terrain link imports
Komzpa Aug 21, 2026
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
130 changes: 119 additions & 11 deletions DISCRETE_EVENT_SIM.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ To start one simulation with the default configurations, run:

```python3 loraMesh.py [nr_nodes]```

If no argument is given, you first have to place the nodes on a plot. After you place a node, you can change its [role](https://meshtastic.org/docs/settings/config/device#role), hopLimit, height (elevation) and antenna gain. These settings will automatically save when you place a new node or when you start the simulation.
If no argument is given, you first have to place the nodes on a plot. After you place a node, you can change its [role](https://meshtastic.org/docs/settings/config/device#role), hopLimit, antenna height above local ground, and antenna gain. These settings will automatically save when you place a new node or when you start the simulation.

![](/img/configNode.png)

Expand All @@ -23,6 +23,107 @@ Short deterministic smoke runs can also override the configured duration and mes

```python3 loraMesh.py 2 --no-gui --simtime-seconds 5 --period-seconds 0.5```

The same headless path can import positioned real-mesh nodes. `--from-map`
reads a Meshtastic map `/api/v1/nodes` JSON endpoint; the public default is
`https://meshtastic.liamcottle.net/api/v1/nodes`, but you can pass another
compatible endpoint URL. These map endpoints usually return a broad node list,
so pass a local area-of-interest bounding box. `--map-bbox` uses the common
`min_lat,min_lon,max_lat,max_lon` order that most GIS tools call
`south,west,north,east`; you can copy those four numbers from OpenStreetMap's
Export panel, geojson.io's bbox readout, QGIS, or any other tool that shows the
extent of the map view or selected polygon. Keep the box tight enough for the
local scenario you want to simulate:

```python3 loraMesh.py --from-map https://meshtastic.liamcottle.net/api/v1/nodes --map-bbox 41.50,41.50,41.82,41.86 --map-limit 50 --no-gui```

You can also import positioned nodes from the NodeDB cached by a local
Meshtastic device. This uses the Python client `interface.nodesByNum` data that
backs `meshtastic --nodes`, not the pretty-printed table. Use TCP for a network
device, or omit `--nodedb-host` to use Meshtastic serial auto-detection. For a
quick local-device run, pass the device address and cap the imported node count:

```python3 loraMesh.py --from-nodedb --nodedb-host 192.168.1.23 --map-limit 50 --no-gui```

NodeDB often contains old or far-away positions. Add `--map-bbox` when you want
to restrict the run to one local area:

```python3 loraMesh.py --from-nodedb --nodedb-host 192.168.1.23 --map-bbox 41.50,41.50,41.82,41.86 --map-limit 50 --no-gui```

Imported nodes use the same `HM` antenna height and `hopLimit` defaults as
generated and file-backed scenarios. Change those config values when the
position source does not carry the simulation value you want.

Terrain obstruction can be added to map, NodeDB, or origin-backed scenario inputs
without creating a custom terrain file. `--terrain-srtm` downloads missing SRTM
HGT tiles from Mapzen Terrain Tiles on AWS into a local cache and feeds the
terrain grid directly into terrain-aware node geometry:

```python3 loraMesh.py --from-nodedb --nodedb-host 192.168.1.23 --map-limit 50 --terrain-srtm --no-gui```

With an explicit `--map-bbox`, SRTM samples that whole requested rectangle. When
the terrain bbox is derived from imported or file-backed nodes, Meshtasticator
keeps the download smaller: it loads tiles around the selected nodes and along
flat-link candidate paths, instead of downloading every tile in a large
edge-to-edge rectangle. That candidate-path selection scales with the number of
node pairs, so combine `--terrain-srtm` with `--map-limit` when importing a
broad map region. When publishing screenshots, reports, or derived
datasets from this terrain source, attribute the terrain data to
[Mapzen Terrain Tiles on AWS](https://registry.opendata.aws/terrain-tiles/),
SRTM/NASA, and their underlying open elevation sources:

```python3 loraMesh.py --from-map https://meshtastic.liamcottle.net/api/v1/nodes --map-bbox 41.50,41.50,41.82,41.86 --map-limit 50 --terrain-srtm --no-gui```

Map payload `altitude` values are absolute GPS/MSL altitude, not antenna height,
so map import keeps using `HM` as the fallback antenna height above local
ground. When `--terrain-srtm` is enabled, each map node is checked
against its own SRTM ground sample: plausible positive map altitudes are used as
absolute node altitude, while missing, below-ground, or implausibly high values
fall back to `SRTM ground + antenna height` for 3D distance calculations.

Land-cover clutter is a separate optional CSV grid. Use it for broad urban,
open, water, or forest excess-loss inputs without pretending Meshtasticator is a
building-level ray tracer:

```python3 loraMesh.py --from-file nodeConfig.yaml --terrain-srtm --clutter-grid clutter.csv --no-gui```

`tools/osm_to_clutter_csv.py` can build a coarse clutter grid from public
OpenStreetMap building, landuse, natural, and water polygons. The simulator
never fetches OpenStreetMap data implicitly.

Two optional RF models can make dense or weak-link runs less binary:

```python3 loraMesh.py 20 --no-gui --phy-loss-model --capture-collision-model```

`--phy-loss-model` keeps RSSI/sensitivity as the hearability gate, then applies
a smooth SNR-to-payload-success curve that depends on packet size and LoRa
coding rate. `--capture-collision-model` keeps CAD-detectable but undecodable
packets on the RF timeline as interference energy, and uses capture/preamble
overlap rules instead of treating every overlap as identical.

Packaged real-mesh presets can be listed and loaded directly:

```python3 loraMesh.py --list-presets```

The `batumi` preset includes sanitized Batumi/Georgia-area node geometry, a
matching bundled terrain grid, an OpenStreetMap-derived land-cover clutter grid,
and an aggregate radio calibration over generated path features. Terrain,
clutter, and the fitted link-calibration model are enabled automatically for the
preset; use `--terrain-srtm` for a fresh SRTM terrain sample, `--clutter-grid`
for a different land-cover grid, or `--no-clutter` for old-style comparison
runs. The calibration report is in `docs/batumi_radio_calibration.md`.

```python3 loraMesh.py --preset batumi --no-gui --simtime-seconds 5 --period-seconds 2 --phy-loss-model --capture-collision-model```

Dynamic Coding Rate is opt-in and chooses LoRa CR 4/5..4/8 per outgoing packet
without changing the preset's SF or bandwidth:

```python3 loraMesh.py 20 --no-gui --phy-loss-model --capture-collision-model --dcr```

The policy keeps ordinary first-attempt traffic compact, spends extra FEC on
quiet retries, ACKs, non-busy direct relays, and last-hop relays, then records
`dcrTxByCr` and `dcrAirtimeByCr` in simulation results. This keeps idle airtime
as a reserve instead of turning every quiet packet into CR 4/8.

If you placed the nodes yourself, after a simulation the number of nodes, their coordinates and configuration are automatically saved and you can rerun the scenario with:

```python3 loraMesh.py --from-file```
Expand All @@ -41,16 +142,23 @@ To simulate different parameters, you will have to change the *batchSim.py* scri
Here we list some of the configurations, which you can change to model your scenario in */lib/config.py*. These apply to all nodes, except those that you configure per node when using the plot.
### Modem
The LoRa modem ([see Meshtastic radio settings](https://meshtastic.org/docs/overview/radio-settings#predefined-channels)) that is used, as defined below:
|Modem | Name | Bandwidth (kHz) | Coding rate | Spreading Factor | Data rate (kbps)
|--|--|--|--|--|--|
| 0 |Short Fast|250|4/8|7|6.8
| 1 |Short Slow|250|4/8|8|3.9
| 2 |Mid Fast|250|4/8|9|2.2
| 3 |Mid Slow|250|4/8|10|1.2
| 4 |Long Fast|250|4/8|11|0.67
| 5 |Long Moderate|125|4/8|11|0.335
| 6 |Long Slow|125|4/8|12|0.18
| 7 |Very Long Slow|62.5|4/8|12|0.09
| Modem | Name | Bandwidth (kHz) | Base coding rate | Spreading Factor | Nominal data rate (kbps) |
|--|--|--:|--:|--:|--:|
| 0 | Short Turbo | 500 | 4/5 | 7 | 21.9 |
| 1 | Short Fast | 250 | 4/5 | 7 | 10.9 |
| 2 | Short Slow | 250 | 4/5 | 8 | 6.25 |
| 3 | Medium Fast | 250 | 4/5 | 9 | 3.52 |
| 4 | Medium Slow | 250 | 4/5 | 10 | 1.95 |
| 5 | Long Turbo | 500 | 4/8 | 11 | 1.34 |
| 6 | Long Fast | 250 | 4/5 | 11 | 1.07 |
| 7 | Long Moderate | 125 | 4/8 | 11 | 0.336 |
| 8 | Long Slow | 125 | 4/8 | 12 | 0.183 |
| 9 | Very Long Slow | 62.5 | 4/8 | 12 | 0.0916 |

The simulator stores coding rates as their LoRa denominators (`5` through
`8`, meaning CR 4/5 through 4/8). This table shows the configured base CR; when
`--dcr` is enabled, the simulator may select a different CR for each outgoing
packet while leaving the preset's SF and bandwidth unchanged.

### Period
Mean period (in ms) with which the nodes generate a new message following an exponential distribution. E.g. if you set it to 300s, each node will generate a message on average once every five minutes.
Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,56 @@
# Meshtasticator
Discrete-event and interactive simulator for [Meshtastic](https://meshtastic.org/).

## Quick start

Install the Python dependencies, then ask the CLI what runnable scenarios it
already knows about:

```bash
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt

./loraMesh.py --list-presets
./loraMesh.py --list-modem-presets
```

Run the packaged Batumi/Georgia-area radio scenario headlessly:

```bash
./loraMesh.py --preset batumi --no-gui --simtime-seconds 60 --period-seconds 5
```

Run the radio-physics comparison workflow in one command:

```bash
python3 tools/radio_policy_compare.py --policies static,dcr --simtime-seconds 60 --period-seconds 5
```

For CI-style runs, write JSON/Markdown artifacts:

```bash
python3 tools/radio_policy_compare.py \
--simtime-seconds 120 \
--period-seconds 5 \
--json-output out/radio_policy_compare.json \
--markdown-output out/radio_policy_compare.md
```

Threshold flags such as `--max-reach-drop-pp` are accepted only when a later
policy such as `dcr` is compared against the `static` baseline.

For manual radio-physics experiments, keep the same scenario and traffic load
while enabling packet loss and capture-aware collisions:

```bash
./loraMesh.py --preset batumi --no-gui --simtime-seconds 60 --period-seconds 5 \
--phy-loss-model --capture-collision-model
```

See [Radio Physics Quickstart](docs/radio_physics_quickstart.md) for what the
flags mean and which result fields to compare.

## Discrete-event simulator
The discrete-event simulator mimics the radio section of the device software in order to understand its working. It can also be used to assess the performance of your scenario, or the scalability of the protocol.

Expand Down
Loading
Loading