diff --git a/pyproject.toml b/pyproject.toml index ffc5384..90df1a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta" [project] name = "pymc_core" -version = "1.0.11" +version = "1.0.12" authors = [ - {name = "Lloyd Newton", email = "lloyd@rightup.co.uk"}, + {name = "Rightup", email = "rightup@pymc.dev"}, ] description = "A Python MeshCore library with SPI LoRa radio support" readme = "README.md" @@ -66,10 +66,10 @@ docs = [ all = ["pymc_core[hardware,websocket,dev,docs]"] [project.urls] -Homepage = "https://github.com/rightup/pyMC_core" -Documentation = "https://rightup.github.io/rightup/pyMC_core" -Repository = "https://github.com/rightup/pyMC_core.git" -Issues = "https://github.com/rightup/pyMC_core/issues" +Homepage = "https://github.com/pyMC-dev/pyMC_core/" +Documentation = "https://rightup.github.io/pymc_dev/pyMC_core" +Repository = "https://github.com/pyMC-dev/pyMC_core.git" +Issues = "https://github.com/pyMC-dev/pyMC_core/issues" [tool.setuptools] package-dir = {"" = "src"} diff --git a/src/pymc_core/__init__.py b/src/pymc_core/__init__.py index 27efc05..6313b9c 100644 --- a/src/pymc_core/__init__.py +++ b/src/pymc_core/__init__.py @@ -3,7 +3,7 @@ Clean, simple API for building mesh network applications. """ -__version__ = "1.0.11" +__version__ = "1.0.12" # Core mesh functionality from .node.node import MeshNode diff --git a/src/pymc_core/hardware/tcp_radio.py b/src/pymc_core/hardware/tcp_radio.py index 71bbd6a..6752edc 100644 --- a/src/pymc_core/hardware/tcp_radio.py +++ b/src/pymc_core/hardware/tcp_radio.py @@ -151,6 +151,8 @@ def __init__( # Stats self._tx_count = 0 self._rx_count = 0 + self._crc_errors = 0 + self.crc_error_count = 0 logger.info( f"TCPLoRaRadio configured: {host}:{port} " @@ -359,7 +361,9 @@ def check_radio_health(self) -> bool: if self._event_loop: self._event_loop.call_soon_threadsafe( - lambda: self._event_loop.create_task(self.refresh_noise_floor()) + lambda: self._event_loop.create_task( + self._refresh_background_metrics() + ) ) return True @@ -380,6 +384,8 @@ def get_status(self) -> dict: "port": self.port, "tx_count": self._tx_count, "rx_count": self._rx_count, + "crc_errors": self._crc_errors, + "crc_error_count": self.crc_error_count, } async def get_modem_status(self) -> Optional[dict]: @@ -390,7 +396,7 @@ async def get_modem_status(self) -> Optional[dict]: ) if resp and len(resp) >= STATUS_RESP_SIZE: fields = struct.unpack(STATUS_RESP_FMT, resp[:STATUS_RESP_SIZE]) - return { + status = { "uptime_sec": fields[0], "rx_count": fields[1], "tx_count": fields[2], @@ -401,8 +407,21 @@ async def get_modem_status(self) -> Optional[dict]: "temp_c": fields[7], "radio_state": ["idle/rx", "tx", "error"][min(fields[8], 2)], } + self._crc_errors = status["crc_errors"] + self.crc_error_count = status["crc_errors"] + return status return None + async def _refresh_background_metrics(self) -> None: + try: + await self.refresh_noise_floor() + except Exception as e: + logger.debug(f"Noise-floor refresh failed: {e}") + try: + await self.get_modem_status() + except Exception as e: + logger.debug(f"Status refresh failed: {e}") + def get_noise_floor(self) -> Optional[float]: if not self._initialized: return 0.0 diff --git a/src/pymc_core/hardware/usb_radio.py b/src/pymc_core/hardware/usb_radio.py index c22c754..450a7d1 100644 --- a/src/pymc_core/hardware/usb_radio.py +++ b/src/pymc_core/hardware/usb_radio.py @@ -149,6 +149,8 @@ def __init__( # Stats self._tx_count = 0 self._rx_count = 0 + self._crc_errors = 0 + self.crc_error_count = 0 logger.info( f"USBLoRaRadio configured: port={port}, freq={frequency/1e6:.1f}MHz, " @@ -353,7 +355,7 @@ def check_radio_health(self) -> bool: """Health check — verify RX thread is alive, restart if dead. Called by Dispatcher.run_forever() every 60 seconds. - Also triggers a noise floor refresh from the modem. + Also refreshes cached modem metrics from the modem. """ if not self._initialized: return False @@ -368,10 +370,12 @@ def check_radio_health(self) -> bool: self._rx_thread.start() return False - # Schedule noise floor refresh (non-blocking) + # Schedule modem metric refresh (non-blocking) if self._event_loop: self._event_loop.call_soon_threadsafe( - lambda: self._event_loop.create_task(self.refresh_noise_floor()) + lambda: self._event_loop.create_task( + self._refresh_background_metrics() + ) ) return True @@ -393,6 +397,8 @@ def get_status(self) -> dict: "port": self.port, "tx_count": self._tx_count, "rx_count": self._rx_count, + "crc_errors": self._crc_errors, + "crc_error_count": self.crc_error_count, } async def get_modem_status(self) -> Optional[dict]: @@ -404,7 +410,7 @@ async def get_modem_status(self) -> Optional[dict]: ) if resp and len(resp) >= STATUS_RESP_SIZE: fields = struct.unpack(STATUS_RESP_FMT, resp[:STATUS_RESP_SIZE]) - return { + status = { "uptime_sec": fields[0], "rx_count": fields[1], "tx_count": fields[2], @@ -417,8 +423,21 @@ async def get_modem_status(self) -> Optional[dict]: min(fields[8], 2) ], } + self._crc_errors = status["crc_errors"] + self.crc_error_count = status["crc_errors"] + return status return None + async def _refresh_background_metrics(self) -> None: + try: + await self.refresh_noise_floor() + except Exception as e: + logger.debug(f"Noise-floor refresh failed: {e}") + try: + await self.get_modem_status() + except Exception as e: + logger.debug(f"Status refresh failed: {e}") + def get_noise_floor(self) -> Optional[float]: """Get current noise floor in dBm. diff --git a/tests/test_basic.py b/tests/test_basic.py index 6aa9617..37ba539 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -2,7 +2,7 @@ def test_version(): - assert __version__ == "1.0.11" + assert __version__ == "1.0.12" def test_import():