Skip to content
Merged
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
36 changes: 36 additions & 0 deletions docs/COMMON-ERRORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,42 @@ something on air, could not classify it, moving on."

**Fix:** None required. Safe to ignore.

### My Short or Medium preset shows the wrong coding rate

**Cause:** Meshpoints running v0.7.1 or earlier shipped the Short and
Medium presets with `coding_rate: "4/8"` in the preset table. The
correct value per the [Meshtastic spec](https://meshtastic.org/docs/overview/radio-settings/)
is `4/5` for `ShortFast`, `ShortSlow`, `ShortTurbo`, `MediumFast`,
`MediumSlow`, and `LongFast`. Only `LongModerate`, `LongSlow`,
`LongTurbo`, and the deprecated `VeryLongSlow` use `4/8`.

If you picked one of the affected presets in the dashboard Radio tab
on a pre-fix install, your `config/local.yaml` has the wrong CR
cached, even after `git pull`.

**Impact:** RX is unaffected (the SX1302 decodes any CR automatically
because the LoRa header carries it). TX takes roughly 60% more
airtime than a stock Meshtastic node sending the "same" preset:
4/8 has 2x overhead per data byte vs 4/5's 1.25x. NodeInfo broadcasts,
DM ACKs, and relay (when enabled) all pay this airtime tax.

**Fix:** After `sudo git pull origin main && sudo systemctl restart
meshpoint`, do one of the following:

- **Dashboard re-pick** (recommended): open the dashboard, go to the
Radio tab, re-select your preset from the Radio Configuration
dropdown, click Save Radio, and apply the restart prompt. This
rewrites `local.yaml` with the corrected CR.
- **Manual edit**: edit `config/local.yaml` and set
`radio.coding_rate` to the correct value for your preset (`"4/5"`
for the six presets listed above, `"4/8"` for the four Long
presets). Then `sudo systemctl restart meshpoint`.

To confirm the fix took effect, check the startup banner or
`meshpoint status` for the active radio config, and watch the next
NodeInfo broadcast log line: airtime should drop noticeably (e.g.,
~723ms to ~456ms for a NodeInfo on MediumSlow).

---

## API and dashboard
Expand Down
2 changes: 1 addition & 1 deletion src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class RadioConfig:
slot: Optional[int] = None # Meshtastic 1-indexed slot; used when frequency_mhz absent
spreading_factor: int = 11
bandwidth_khz: float = 250.0
coding_rate: str = "4/8"
coding_rate: str = "4/5"
sync_word: int = 0x2B
preamble_length: int = 16
tx_power_dbm: int = 22
Expand Down
15 changes: 11 additions & 4 deletions src/radio/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ class ModemPreset:
bandwidth_khz=250,
coding_rate="4/5",
),
"LONG_TURBO": ModemPreset(
name="LONG_TURBO",
display_name="LongTurbo",
spreading_factor=11,
bandwidth_khz=500,
coding_rate="4/8",
),
"LONG_MODERATE": ModemPreset(
name="LONG_MODERATE",
display_name="LongModerate",
Expand Down Expand Up @@ -57,28 +64,28 @@ class ModemPreset:
display_name="MediumFast",
spreading_factor=9,
bandwidth_khz=250,
coding_rate="4/8",
coding_rate="4/5",
),
"MEDIUM_SLOW": ModemPreset(
name="MEDIUM_SLOW",
display_name="MediumSlow",
spreading_factor=10,
bandwidth_khz=250,
coding_rate="4/8",
coding_rate="4/5",
),
"SHORT_FAST": ModemPreset(
name="SHORT_FAST",
display_name="ShortFast",
spreading_factor=7,
bandwidth_khz=250,
coding_rate="4/8",
coding_rate="4/5",
),
"SHORT_SLOW": ModemPreset(
name="SHORT_SLOW",
display_name="ShortSlow",
spreading_factor=8,
bandwidth_khz=250,
coding_rate="4/8",
coding_rate="4/5",
),
"SHORT_TURBO": ModemPreset(
name="SHORT_TURBO",
Expand Down
1 change: 1 addition & 0 deletions src/transmit/tx_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
(9, 250): "MediumFast",
(10, 250): "MediumSlow",
(11, 250): "LongFast",
(11, 500): "LongTurbo",
(11, 125): "LongMod",
(12, 125): "LongSlow",
(12, 62): "VLongSlow",
Expand Down
85 changes: 85 additions & 0 deletions tests/test_radio_presets_canonical.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Lock MODEM_PRESETS to the upstream Meshtastic spec.

Each preset's (spreading_factor, bandwidth_khz, coding_rate) tuple
must match the canonical Meshtastic firmware values to avoid silent
drift like the v0.7.1-and-earlier bug where Short and Medium presets
shipped with CR 4/8 instead of 4/5, costing ~60% extra airtime per TX.

Reference: https://meshtastic.org/docs/overview/radio-settings/
"""

from __future__ import annotations

import unittest

from src.radio.presets import (
MODEM_PRESETS,
all_presets_list,
get_preset,
preset_from_params,
)

# Canonical (sf, bw_khz, cr) per upstream Meshtastic firmware
# RadioInterface.cpp. Keys must match MODEM_PRESETS keys exactly.
CANONICAL: dict[str, tuple[int, float, str]] = {
"SHORT_TURBO": (7, 500, "4/5"),
"SHORT_FAST": (7, 250, "4/5"),
"SHORT_SLOW": (8, 250, "4/5"),
"MEDIUM_FAST": (9, 250, "4/5"),
"MEDIUM_SLOW": (10, 250, "4/5"),
"LONG_FAST": (11, 250, "4/5"),
"LONG_TURBO": (11, 500, "4/8"),
"LONG_MODERATE": (11, 125, "4/8"),
"LONG_SLOW": (12, 125, "4/8"),
"VERY_LONG_SLOW": (12, 62.5, "4/8"),
}


class TestPresetCanonicalValues(unittest.TestCase):

def test_full_preset_coverage(self):
"""MODEM_PRESETS keys exactly match the canonical preset set."""
self.assertEqual(set(MODEM_PRESETS.keys()), set(CANONICAL.keys()))

def test_each_preset_matches_meshtastic_spec(self):
"""Every preset's (sf, bw, cr) tuple matches upstream firmware."""
for name, expected in CANONICAL.items():
with self.subTest(preset=name):
p = MODEM_PRESETS[name]
actual = (p.spreading_factor, p.bandwidth_khz, p.coding_rate)
self.assertEqual(
actual,
expected,
f"{name} drifted from upstream spec",
)

def test_get_preset_round_trip(self):
"""get_preset() returns the same instance MODEM_PRESETS holds."""
for name in CANONICAL:
with self.subTest(preset=name):
self.assertIs(get_preset(name), MODEM_PRESETS[name])
self.assertIs(get_preset(name.lower()), MODEM_PRESETS[name])

def test_reverse_lookup_round_trips(self):
"""preset_from_params(sf, bw, cr) returns the original preset name."""
for name, (sf, bw, cr) in CANONICAL.items():
with self.subTest(preset=name):
self.assertEqual(preset_from_params(sf, bw, cr), name)

def test_all_presets_list_includes_every_canonical_entry(self):
"""API-facing all_presets_list() exposes every canonical preset."""
api_names = {entry["name"] for entry in all_presets_list()}
self.assertEqual(api_names, set(CANONICAL.keys()))

def test_long_turbo_is_tx_capable(self):
"""LongTurbo mirrors ShortTurbo's tx_capable=True (BW500 region-aware)."""
self.assertTrue(MODEM_PRESETS["LONG_TURBO"].tx_capable)
self.assertTrue(MODEM_PRESETS["SHORT_TURBO"].tx_capable)

def test_very_long_slow_is_not_tx_capable(self):
"""Deprecated VeryLongSlow remains RX-only per existing constraint."""
self.assertFalse(MODEM_PRESETS["VERY_LONG_SLOW"].tx_capable)


if __name__ == "__main__":
unittest.main()
5 changes: 4 additions & 1 deletion tests/test_tx_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ def test_shortturbo(self):
def test_mediumfast(self):
self.assertEqual(PRESET_DISPLAY_NAMES[(9, 250)], "MediumFast")

def test_longturbo(self):
self.assertEqual(PRESET_DISPLAY_NAMES[(11, 500)], "LongTurbo")

def test_all_presets_present(self):
expected = {
"ShortFast", "ShortTurbo", "ShortSlow",
"MediumFast", "MediumSlow",
"LongFast", "LongMod", "LongSlow", "VLongSlow",
"LongFast", "LongTurbo", "LongMod", "LongSlow", "VLongSlow",
}
self.assertEqual(set(PRESET_DISPLAY_NAMES.values()), expected)

Expand Down
Loading