mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-07-26 19:42:46 +02:00
fix(repeater): wire the flood reception delay base into the dispatcher
The delays.rx_delay_base config value was stored and reported but never applied. Now that the core dispatcher implements MeshCore's flood reception-quality delay, feed it through: - set dispatcher.rx_delay_base from delays.rx_delay_base at startup and re-apply it on live config updates of the delays section (web API saves already pass that section) - point the mesh CLI get/set rxdelay at the delays section; it read and wrote repeater.rx_delay_base, which nothing consumes, so the CLI knob was silently dead - delegate RepeaterHandler.calculate_packet_score to the shared core packet_score (same firmware packetScoreInt formula, one impl) Suppression works through the existing engine seen-cache because it dedupes at process time, after the dispatcher hold. Default remains 0 (delay disabled), matching firmware.
This commit is contained in:
@@ -272,6 +272,15 @@ class ConfigManager:
|
||||
self.daemon.advert_helper.reload_config()
|
||||
logger.info("Reloaded AdvertHelper config")
|
||||
|
||||
# Re-apply the flood reception delay base when delays changed
|
||||
if "delays" in sections and self.daemon and getattr(self.daemon, "dispatcher", None):
|
||||
delays_cfg = self.daemon.config.get("delays", {})
|
||||
self.daemon.dispatcher.rx_delay_base = float(delays_cfg.get("rx_delay_base", 0.0))
|
||||
logger.info(
|
||||
f"Reloaded flood RX delay base: delays.rx_delay_base="
|
||||
f"{self.daemon.dispatcher.rx_delay_base}"
|
||||
)
|
||||
|
||||
# Re-apply dispatcher path hash mode when mesh section changed
|
||||
if "mesh" in sections and self.daemon and hasattr(self.daemon, "dispatcher"):
|
||||
mesh_cfg = self.daemon.config.get("mesh", {})
|
||||
|
||||
+4
-24
@@ -22,7 +22,7 @@ from openhop_core.protocol.constants import (
|
||||
ROUTE_TYPE_TRANSPORT_DIRECT,
|
||||
ROUTE_TYPE_TRANSPORT_FLOOD,
|
||||
)
|
||||
from openhop_core.protocol.packet_utils import PacketHeaderUtils, PathUtils
|
||||
from openhop_core.protocol.packet_utils import PacketHeaderUtils, PathUtils, packet_score
|
||||
|
||||
from repeater.airtime import AirtimeManager
|
||||
from repeater.data_acquisition import StorageCollector
|
||||
@@ -1119,29 +1119,9 @@ class RepeaterHandler(BaseHandler):
|
||||
|
||||
@staticmethod
|
||||
def calculate_packet_score(snr: float, packet_len: int, spreading_factor: int = 8) -> float:
|
||||
|
||||
# SNR thresholds per SF (from MeshCore RadioLibWrappers.cpp)
|
||||
snr_thresholds = {7: -7.5, 8: -10.0, 9: -12.5, 10: -15.0, 11: -17.5, 12: -20.0}
|
||||
|
||||
if spreading_factor < 7:
|
||||
return 0.0
|
||||
|
||||
threshold = snr_thresholds.get(spreading_factor, -10.0)
|
||||
|
||||
# Below threshold = no chance of success
|
||||
if snr < threshold:
|
||||
return 0.0
|
||||
|
||||
# Success rate based on SNR above threshold
|
||||
success_rate_based_on_snr = (snr - threshold) / 10.0
|
||||
|
||||
# Collision penalty: longer packets more likely to collide (max 256 bytes)
|
||||
collision_penalty = 1.0 - (packet_len / 256.0)
|
||||
|
||||
# Combined score
|
||||
score = success_rate_based_on_snr * collision_penalty
|
||||
|
||||
return max(0.0, min(1.0, score))
|
||||
"""Reception-quality score in [0, 1] via the shared core scorer
|
||||
(MeshCore RadioLibWrappers packetScoreInt)."""
|
||||
return packet_score(snr, spreading_factor, packet_len)
|
||||
|
||||
def _calculate_tx_delay(self, packet: Packet, snr: float = 0.0) -> float:
|
||||
|
||||
|
||||
@@ -570,7 +570,9 @@ class MeshCLI:
|
||||
return f"> {loop_detect}"
|
||||
|
||||
elif param == "rxdelay":
|
||||
delay = self.repeater_config.get("rx_delay_base", 0.0)
|
||||
# The flood RX delay base lives in the delays section (the value
|
||||
# the dispatcher and web API use), not the repeater section.
|
||||
delay = self.config.get("delays", {}).get("rx_delay_base", 0.0)
|
||||
return f"> {delay}"
|
||||
|
||||
elif param == "txdelay":
|
||||
@@ -741,7 +743,7 @@ class MeshCLI:
|
||||
delay = float(value)
|
||||
if delay < 0:
|
||||
return "Error: cannot be negative"
|
||||
self.repeater_config["rx_delay_base"] = delay
|
||||
self.config.setdefault("delays", {})["rx_delay_base"] = delay
|
||||
saved, _ = self.config_manager.save_to_file()
|
||||
self.config_manager.live_update_daemon(["repeater", "delays"])
|
||||
return "OK"
|
||||
|
||||
@@ -330,6 +330,11 @@ class RepeaterDaemon:
|
||||
self.config.get("repeater", {}).get("dispatcher_dedupe_enabled", False)
|
||||
)
|
||||
self.dispatcher = Dispatcher(self.radio, dedupe_enabled=dedupe_enabled)
|
||||
# Flood reception-quality delay base (MeshCore "set rxdelay");
|
||||
# 0 keeps flood processing immediate, the firmware default.
|
||||
self.dispatcher.rx_delay_base = float(
|
||||
self.config.get("delays", {}).get("rx_delay_base", 0.0)
|
||||
)
|
||||
logger.info("Dispatcher initialized")
|
||||
logger.info("Dispatcher dedupe enabled: %s", dedupe_enabled)
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Wiring for the flood reception-quality delay (MeshCore "set rxdelay").
|
||||
|
||||
The delay itself lives in the core Dispatcher; the repeater's job is to feed
|
||||
``delays.rx_delay_base`` from its config into ``dispatcher.rx_delay_base`` at
|
||||
startup and on live config updates, and to expose the same value through the
|
||||
mesh CLI. The packet scorer the delay uses is shared with the engine's
|
||||
TX-delay scoring.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from openhop_core.protocol.packet_utils import packet_score
|
||||
|
||||
from repeater.config_manager import ConfigManager
|
||||
from repeater.engine import RepeaterHandler
|
||||
from repeater.handler_helpers.mesh_cli import MeshCLI
|
||||
|
||||
|
||||
class _DummyDispatcher:
|
||||
def __init__(self):
|
||||
self.rx_delay_base = 0.0
|
||||
|
||||
def set_default_path_hash_mode(self, mode):
|
||||
pass
|
||||
|
||||
|
||||
def _daemon_with_dispatcher(config):
|
||||
return SimpleNamespace(
|
||||
config=config,
|
||||
radio=None,
|
||||
repeater_handler=None,
|
||||
advert_helper=None,
|
||||
dispatcher=_DummyDispatcher(),
|
||||
)
|
||||
|
||||
|
||||
def _cfg_mgr():
|
||||
return SimpleNamespace(
|
||||
save_to_file=MagicMock(return_value=(True, None)),
|
||||
live_update_daemon=MagicMock(),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Live config update -> dispatcher
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_live_update_applies_rx_delay_base_to_dispatcher():
|
||||
config = {"delays": {"rx_delay_base": 10.0}}
|
||||
daemon = _daemon_with_dispatcher(config)
|
||||
manager = ConfigManager("/tmp/config.yaml", config, daemon)
|
||||
|
||||
assert manager.live_update_daemon(["delays"])
|
||||
assert daemon.dispatcher.rx_delay_base == 10.0
|
||||
|
||||
|
||||
def test_live_update_clears_rx_delay_base_when_removed():
|
||||
config = {"delays": {}}
|
||||
daemon = _daemon_with_dispatcher(config)
|
||||
daemon.dispatcher.rx_delay_base = 10.0
|
||||
manager = ConfigManager("/tmp/config.yaml", config, daemon)
|
||||
|
||||
assert manager.live_update_daemon(["delays"])
|
||||
assert daemon.dispatcher.rx_delay_base == 0.0
|
||||
|
||||
|
||||
def test_live_update_without_dispatcher_does_not_crash():
|
||||
config = {"delays": {"rx_delay_base": 5.0}}
|
||||
daemon = _daemon_with_dispatcher(config)
|
||||
daemon.dispatcher = None
|
||||
manager = ConfigManager("/tmp/config.yaml", config, daemon)
|
||||
|
||||
assert manager.live_update_daemon(["delays"])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Mesh CLI reads/writes the delays section (the one the dispatcher uses)
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_cli_set_rxdelay_writes_delays_section():
|
||||
config = {"repeater": {}, "mesh": {}}
|
||||
cli = MeshCLI("/tmp/cfg.yaml", config, _cfg_mgr())
|
||||
|
||||
assert cli._cmd_set("rxdelay 10") == "OK"
|
||||
assert config["delays"]["rx_delay_base"] == 10.0
|
||||
assert "rx_delay_base" not in config["repeater"]
|
||||
cli.config_manager.live_update_daemon.assert_called_with(["repeater", "delays"])
|
||||
|
||||
|
||||
def test_cli_get_rxdelay_reads_delays_section():
|
||||
config = {"repeater": {"rx_delay_base": 99.0}, "mesh": {}, "delays": {"rx_delay_base": 7.5}}
|
||||
cli = MeshCLI("/tmp/cfg.yaml", config, _cfg_mgr())
|
||||
|
||||
assert cli._cmd_get("rxdelay") == "> 7.5"
|
||||
|
||||
|
||||
def test_cli_get_rxdelay_defaults_to_disabled():
|
||||
cli = MeshCLI("/tmp/cfg.yaml", {"repeater": {}, "mesh": {}}, _cfg_mgr())
|
||||
|
||||
assert cli._cmd_get("rxdelay") == "> 0.0"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Engine scoring delegates to the shared core scorer
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_engine_score_matches_shared_packet_score():
|
||||
for snr, packet_len, sf in ((-5.0, 24, 7), (-10.0, 128, 12), (5.0, 50, 8), (0.0, 200, 10)):
|
||||
assert RepeaterHandler.calculate_packet_score(snr, packet_len, sf) == packet_score(
|
||||
snr, sf, packet_len
|
||||
)
|
||||
Reference in New Issue
Block a user