mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-06-26 13:01:06 +02:00
fix: move modem CRC baseline into radio wrapper
This commit is contained in:
+37
-2
@@ -101,6 +101,41 @@ class NullRadio:
|
||||
return True
|
||||
|
||||
|
||||
class BaselineCrcCounterRadio:
|
||||
"""Radio proxy that exposes CRC errors relative to repeater startup.
|
||||
|
||||
pyMC modem transports report the modem firmware's cumulative CRC counter.
|
||||
The SX1262 wrapper's counter starts at process startup, which lets the engine
|
||||
persist deltas without knowing the radio backend. Mirror that wrapper flow
|
||||
here by normalizing the modem's raw counter at the transport boundary.
|
||||
"""
|
||||
|
||||
def __init__(self, radio):
|
||||
self._radio = radio
|
||||
self._crc_baseline = self._read_raw_crc_count()
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
return getattr(self._radio, name)
|
||||
|
||||
@property
|
||||
def crc_error_count(self) -> int:
|
||||
current = self._read_raw_crc_count()
|
||||
if self._crc_baseline <= 0 and current > 0:
|
||||
self._crc_baseline = current
|
||||
return 0
|
||||
return max(0, current - self._crc_baseline)
|
||||
|
||||
@crc_error_count.setter
|
||||
def crc_error_count(self, value: Any) -> None:
|
||||
setattr(self._radio, "crc_error_count", value)
|
||||
|
||||
def _read_raw_crc_count(self) -> int:
|
||||
try:
|
||||
return int(getattr(self._radio, "crc_error_count", 0) or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def resolve_storage_dir(
|
||||
config: Dict[str, Any],
|
||||
*,
|
||||
@@ -618,7 +653,7 @@ def get_radio_for_board(board_config: dict):
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to initialize pymc_tcp radio: {e}") from e
|
||||
|
||||
return radio
|
||||
return BaselineCrcCounterRadio(radio)
|
||||
|
||||
elif radio_type == "pymc_usb":
|
||||
try:
|
||||
@@ -660,7 +695,7 @@ def get_radio_for_board(board_config: dict):
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to initialize pymc_usb radio: {e}") from e
|
||||
|
||||
return radio
|
||||
return BaselineCrcCounterRadio(radio)
|
||||
|
||||
raise RuntimeError(
|
||||
f"Unknown radio type: {radio_type}. "
|
||||
|
||||
+2
-24
@@ -135,9 +135,7 @@ class RepeaterHandler(BaseHandler):
|
||||
self.noise_floor_interval = NOISE_FLOOR_INTERVAL # 30 seconds
|
||||
self._background_task = None
|
||||
self._cached_noise_floor = None
|
||||
self._last_crc_error_count = (
|
||||
None if self._is_pymc_modem_radio() else 0
|
||||
) # Track radio counter for delta persistence
|
||||
self._last_crc_error_count = 0 # Track radio counter for delta persistence
|
||||
|
||||
# Cache transport keys for efficient lookup
|
||||
self._transport_keys_cache = None
|
||||
@@ -1408,18 +1406,6 @@ class RepeaterHandler(BaseHandler):
|
||||
except Exception as e:
|
||||
logger.error(f"Error recording noise floor: {e}")
|
||||
|
||||
def _is_pymc_modem_radio(self) -> bool:
|
||||
"""Return True when using PyMC modem transports with device-owned stats."""
|
||||
radio_type = str(self.config.get("radio_type", "")).lower()
|
||||
return radio_type in {"pymc_usb", "pymc_tcp"}
|
||||
|
||||
@staticmethod
|
||||
def _get_radio_crc_error_count(radio) -> int:
|
||||
try:
|
||||
return int(getattr(radio, "crc_error_count", 0) or 0) if radio else 0
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
async def _record_crc_errors_async(self):
|
||||
"""Persist CRC error delta from the radio hardware counter."""
|
||||
if not self.storage:
|
||||
@@ -1427,15 +1413,7 @@ class RepeaterHandler(BaseHandler):
|
||||
|
||||
try:
|
||||
radio = self.dispatcher.radio if self.dispatcher else None
|
||||
current = self._get_radio_crc_error_count(radio)
|
||||
if self._last_crc_error_count is None:
|
||||
if current <= 0:
|
||||
logger.debug("Waiting for PyMC modem CRC baseline")
|
||||
return
|
||||
self._last_crc_error_count = current
|
||||
logger.debug(f"Baselined PyMC modem CRC errors at {current}")
|
||||
return
|
||||
|
||||
current = getattr(radio, "crc_error_count", 0) if radio else 0
|
||||
delta = current - self._last_crc_error_count
|
||||
if delta > 0:
|
||||
self.storage.record_crc_errors(delta)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
from repeater.config import BaselineCrcCounterRadio
|
||||
|
||||
|
||||
def test_baseline_crc_counter_radio_reports_delta_from_initial_raw_count():
|
||||
raw = SimpleNamespace(crc_error_count=20_000, frequency=869_618_000)
|
||||
radio = BaselineCrcCounterRadio(raw)
|
||||
|
||||
assert radio.frequency == 869_618_000
|
||||
assert radio.crc_error_count == 0
|
||||
|
||||
raw.crc_error_count = 20_003
|
||||
|
||||
assert radio.crc_error_count == 3
|
||||
|
||||
|
||||
def test_baseline_crc_counter_radio_handles_delayed_modem_counter():
|
||||
raw = SimpleNamespace(crc_error_count=0)
|
||||
radio = BaselineCrcCounterRadio(raw)
|
||||
|
||||
assert radio.crc_error_count == 0
|
||||
|
||||
raw.crc_error_count = 145
|
||||
assert radio.crc_error_count == 0
|
||||
|
||||
raw.crc_error_count = 148
|
||||
assert radio.crc_error_count == 3
|
||||
@@ -1790,85 +1790,6 @@ class TestMissedEngineBranches:
|
||||
handler.storage.record_crc_errors.assert_called_once_with(5)
|
||||
assert handler._last_crc_error_count == 9
|
||||
|
||||
@pytest.mark.parametrize("radio_type", ["sx1262", "sx1262_ch341", "kiss"])
|
||||
@pytest.mark.asyncio
|
||||
async def test_record_crc_errors_async_non_pymc_radios_keep_startup_delta_behavior(
|
||||
self, radio_type
|
||||
):
|
||||
config = _make_config(radio_type=radio_type)
|
||||
dispatcher = _make_dispatcher()
|
||||
dispatcher.radio.crc_error_count = 145
|
||||
with (
|
||||
patch("repeater.engine.StorageCollector"),
|
||||
patch("repeater.engine.RepeaterHandler._start_background_tasks"),
|
||||
):
|
||||
from repeater.engine import RepeaterHandler
|
||||
|
||||
handler = RepeaterHandler(config, dispatcher, LOCAL_HASH)
|
||||
|
||||
assert handler._last_crc_error_count == 0
|
||||
|
||||
await handler._record_crc_errors_async()
|
||||
|
||||
handler.storage.record_crc_errors.assert_called_once_with(145)
|
||||
assert handler._last_crc_error_count == 145
|
||||
|
||||
@pytest.mark.parametrize("radio_type", ["pymc_usb", "pymc_tcp"])
|
||||
@pytest.mark.asyncio
|
||||
async def test_record_crc_errors_async_pymc_modem_baselines_existing_device_stats(
|
||||
self, radio_type
|
||||
):
|
||||
config = _make_config(radio_type=radio_type)
|
||||
dispatcher = _make_dispatcher()
|
||||
dispatcher.radio.crc_error_count = 20_000
|
||||
with (
|
||||
patch("repeater.engine.StorageCollector"),
|
||||
patch("repeater.engine.RepeaterHandler._start_background_tasks"),
|
||||
):
|
||||
from repeater.engine import RepeaterHandler
|
||||
|
||||
handler = RepeaterHandler(config, dispatcher, LOCAL_HASH)
|
||||
|
||||
await handler._record_crc_errors_async()
|
||||
|
||||
handler.storage.record_crc_errors.assert_not_called()
|
||||
assert handler._last_crc_error_count == 20_000
|
||||
|
||||
dispatcher.radio.crc_error_count = 20_003
|
||||
await handler._record_crc_errors_async()
|
||||
|
||||
handler.storage.record_crc_errors.assert_called_once_with(3)
|
||||
assert handler._last_crc_error_count == 20_003
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_record_crc_errors_async_pymc_modem_baselines_delayed_device_stats(self):
|
||||
config = _make_config(radio_type="pymc_tcp")
|
||||
dispatcher = _make_dispatcher()
|
||||
dispatcher.radio.crc_error_count = 0
|
||||
with (
|
||||
patch("repeater.engine.StorageCollector"),
|
||||
patch("repeater.engine.RepeaterHandler._start_background_tasks"),
|
||||
):
|
||||
from repeater.engine import RepeaterHandler
|
||||
|
||||
handler = RepeaterHandler(config, dispatcher, LOCAL_HASH)
|
||||
|
||||
await handler._record_crc_errors_async()
|
||||
handler.storage.record_crc_errors.assert_not_called()
|
||||
assert handler._last_crc_error_count is None
|
||||
|
||||
dispatcher.radio.crc_error_count = 145
|
||||
await handler._record_crc_errors_async()
|
||||
|
||||
handler.storage.record_crc_errors.assert_not_called()
|
||||
assert handler._last_crc_error_count == 145
|
||||
|
||||
dispatcher.radio.crc_error_count = 148
|
||||
await handler._record_crc_errors_async()
|
||||
|
||||
handler.storage.record_crc_errors.assert_called_once_with(3)
|
||||
assert handler._last_crc_error_count == 148
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_record_noise_floor_async_caches_and_persists(self, handler):
|
||||
with patch.object(handler, "get_noise_floor", return_value=-117.5):
|
||||
|
||||
Reference in New Issue
Block a user