From 7002f5aee7007ca3a35d57d927b7ba88fbda45f9 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Sun, 26 Oct 2025 23:24:56 +0000 Subject: [PATCH 1/5] Fix LDRO (Low Data Rate Optimization) configuration for SF8+ with narrow bandwidth Add automatic LDRO calculation and configuration based on symbol duration. LDRO must be enabled when symbol duration > 16ms to prevent timing drift and CRC errors during reception. - Calculate symbol duration: 2^SF / (BW / 1000) milliseconds - Enable LDRO when symbol_duration_ms > 16.0 - Pass ldro parameter to setLoRaModulation() in both Waveshare and standard init paths - Add debug logging to show LDRO state and symbol duration at startup Fixes CRC errors that occurred with SF11/62.5kHz (32.768ms symbols) where LDRO was incorrectly disabled, causing receiver timing drift and packet corruption despite successful transmission. Also improves configuration logging by adding bandwidth to startup message. --- src/pymc_core/hardware/sx1262_wrapper.py | 32 +++++++++++++++--------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/pymc_core/hardware/sx1262_wrapper.py b/src/pymc_core/hardware/sx1262_wrapper.py index e81df48..cfc92bc 100644 --- a/src/pymc_core/hardware/sx1262_wrapper.py +++ b/src/pymc_core/hardware/sx1262_wrapper.py @@ -195,7 +195,7 @@ def __init__( logger.info( f"SX1262Radio configured: freq={frequency/1e6:.1f}MHz, " - f"power={tx_power}dBm, sf={spreading_factor}, pre={preamble_length}" + f"power={tx_power}dBm, sf={spreading_factor}, bw={bandwidth/1000:.1f}kHz, pre={preamble_length}" ) # Register this instance as the active radio for IRQ callback access SX1262Radio._active_instance = self @@ -315,7 +315,7 @@ def set_rx_callback(self, callback): async def _rx_irq_background_task(self): """Background task: waits for RX_DONE IRQ and processes received packets automatically.""" - logger.info("[RX] Starting RX IRQ background task") + logger.debug("[RX] Starting RX IRQ background task") rx_check_count = 0 last_preamble_time = 0 preamble_timeout = 5.0 # 5 seconds timeout for incomplete preamble detection @@ -456,14 +456,14 @@ async def _rx_irq_background_task(self): raw_rssi = self.lora.getRssiInst() if raw_rssi is not None: noise_floor_dbm = -(float(raw_rssi) / 2) - logger.info( + logger.debug( f"[RX Task] Status check #{rx_check_count}, " f"Noise: {noise_floor_dbm:.1f}dBm" ) else: - logger.info(f"[RX Task] Status check #{rx_check_count}") + logger.debug(f"[RX Task] Status check #{rx_check_count}") except Exception: - logger.info(f"[RX Task] Status check #{rx_check_count}") + logger.debug(f"[RX Task] Status check #{rx_check_count}") else: await asyncio.sleep(0.1) # Longer delay when interrupts not set up @@ -484,7 +484,7 @@ def begin(self) -> bool: self.irq_pin = Button(self.irq_pin_number, pull_up=False) self.irq_pin.when_activated = self._handle_interrupt self._interrupt_setup = True - logger.info(f"[RX] IRQ setup successful on pin {self.irq_pin_number}") + logger.debug(f"[RX] IRQ setup successful on pin {self.irq_pin_number}") except Exception as e: logger.error(f"IRQ setup failed: {e}") raise RuntimeError(f"Failed to set up IRQ pin {self.irq_pin_number}: {e}") @@ -525,7 +525,11 @@ def begin(self) -> bool: self.lora.setBufferBaseAddress(0x00, 0x80) # TX=0x00, RX=0x80 - self.lora.setLoRaModulation(self.spreading_factor, self.bandwidth, self.coding_rate) + # Enable LDRO if symbol duration > 16ms (SF11/62.5kHz = 32.768ms) + symbol_duration_ms = (2 ** self.spreading_factor) / (self.bandwidth / 1000) + ldro = symbol_duration_ms > 16.0 + logger.info(f"LDRO {'enabled' if ldro else 'disabled'} (symbol duration: {symbol_duration_ms:.3f}ms)") + self.lora.setLoRaModulation(self.spreading_factor, self.bandwidth, self.coding_rate, ldro) self.lora.setLoRaPacket( self.lora.HEADER_EXPLICIT, @@ -564,7 +568,11 @@ def begin(self) -> bool: self.lora.setTxParams(self.tx_power, self.lora.PA_RAMP_200U) # Configure modulation and packet parameters - self.lora.setLoRaModulation(self.spreading_factor, self.bandwidth, self.coding_rate) + # Enable LDRO if symbol duration > 16ms (SF11/62.5kHz = 32.768ms) + symbol_duration_ms = (2 ** self.spreading_factor) / (self.bandwidth / 1000) + ldro = symbol_duration_ms > 16.0 + logger.info(f"LDRO {'enabled' if ldro else 'disabled'} (symbol duration: {symbol_duration_ms:.3f}ms)") + self.lora.setLoRaModulation(self.spreading_factor, self.bandwidth, self.coding_rate, ldro) self.lora.setPacketParamsLoRa( self.preamble_length, self.lora.HEADER_EXPLICIT, @@ -600,9 +608,9 @@ def begin(self) -> bool: return True self._rx_irq_task = loop.create_task(self._rx_irq_background_task()) - logger.info("[RX] RX IRQ background task started") + logger.debug("[RX] RX IRQ background task started") else: - logger.info("[RX] RX IRQ background task already running") + logger.debug("[RX] RX IRQ background task already running") except Exception as e: logger.warning(f"Failed to start RX IRQ background handler: {e}") return True @@ -759,13 +767,13 @@ async def _execute_transmission(self, driver_timeout: int) -> bool: async def _wait_for_transmission_complete(self, timeout_seconds: float) -> bool: """Wait for transmission to complete using interrupts. Returns True if successful.""" - logger.info(f"[TX] Waiting for TX completion (timeout: {timeout_seconds}s)") + logger.debug(f"[TX] Waiting for TX completion (timeout: {timeout_seconds}s)") start_time = time.time() # IRQ setup is required try: await asyncio.wait_for(self._tx_done_event.wait(), timeout=timeout_seconds) - logger.info("[TX] TX completion interrupt received!") + logger.debug("[TX] TX completion interrupt received!") return True except asyncio.TimeoutError: logger.error("[TX] TX completion timeout - no interrupt received!") From de3bd5b297c6ada41e5d5b82e1316e6d20eed8b4 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Mon, 27 Oct 2025 20:56:38 +0000 Subject: [PATCH 2/5] Add method to retrieve current noise floor in dBm --- src/pymc_core/hardware/sx1262_wrapper.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/pymc_core/hardware/sx1262_wrapper.py b/src/pymc_core/hardware/sx1262_wrapper.py index cfc92bc..4efb64c 100644 --- a/src/pymc_core/hardware/sx1262_wrapper.py +++ b/src/pymc_core/hardware/sx1262_wrapper.py @@ -926,6 +926,24 @@ def get_last_snr(self) -> float: """Return last received SNR in dB""" return self.last_snr + def get_noise_floor(self) -> Optional[float]: + """ + Get current noise floor (instantaneous RSSI) in dBm. + Returns None if radio is not initialized or if reading fails. + """ + if not self._initialized or self.lora is None: + return None + + try: + raw_rssi = self.lora.getRssiInst() + if raw_rssi is not None: + noise_floor_dbm = -(float(raw_rssi) / 2) + return noise_floor_dbm + return None + except Exception as e: + logger.debug(f"Failed to read noise floor: {e}") + return None + def set_frequency(self, frequency: int) -> bool: """Set operating frequency""" From b60309c550e293798a5f722c8d6785165e0460d2 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Mon, 27 Oct 2025 21:48:19 +0000 Subject: [PATCH 3/5] Bump version to 1.0.2 --- src/pymc_core/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymc_core/__init__.py b/src/pymc_core/__init__.py index 25ed2b5..a4882a5 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.1" +__version__ = "1.0.2" # Core mesh functionality from .node.node import MeshNode From 1d354b73a8a3332c073e0bd0f9161acf10aac9ea Mon Sep 17 00:00:00 2001 From: Lloyd Date: Mon, 27 Oct 2025 21:54:05 +0000 Subject: [PATCH 4/5] Update version in test to match 1.0.2 --- tests/test_basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_basic.py b/tests/test_basic.py index f11740a..17e1406 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -2,7 +2,7 @@ def test_version(): - assert __version__ == "1.0.1" + assert __version__ == "1.0.2" def test_import(): From aa32c6ea915c40bb798f4a98f7b4b10575197360 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Mon, 27 Oct 2025 21:55:50 +0000 Subject: [PATCH 5/5] Update src/pymc_core/hardware/sx1262_wrapper.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/pymc_core/hardware/sx1262_wrapper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pymc_core/hardware/sx1262_wrapper.py b/src/pymc_core/hardware/sx1262_wrapper.py index 4efb64c..527087e 100644 --- a/src/pymc_core/hardware/sx1262_wrapper.py +++ b/src/pymc_core/hardware/sx1262_wrapper.py @@ -933,7 +933,6 @@ def get_noise_floor(self) -> Optional[float]: """ if not self._initialized or self.lora is None: return None - try: raw_rssi = self.lora.getRssiInst() if raw_rssi is not None: