From afb0c0f8057f5ed524ef5caa69d7be116aead0eb Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 4 Nov 2025 13:53:48 +0000 Subject: [PATCH 1/4] updated TX timeout calculation --- src/pymc_core/hardware/sx1262_wrapper.py | 69 ++++++++++++++---------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/src/pymc_core/hardware/sx1262_wrapper.py b/src/pymc_core/hardware/sx1262_wrapper.py index 47287c5..625bf44 100644 --- a/src/pymc_core/hardware/sx1262_wrapper.py +++ b/src/pymc_core/hardware/sx1262_wrapper.py @@ -671,34 +671,49 @@ def begin(self) -> bool: raise RuntimeError(f"Failed to initialize SX1262 radio: {e}") from e def _calculate_tx_timeout(self, packet_length: int) -> tuple[int, int]: - """Calculate transmission timeout based on modulation parameters""" + """Calculate transmission timeout using correct LoRa formula""" + import math + sf = self.spreading_factor - bw = self.bandwidth - - # Realistic timeout calculation based on actual LoRa performance - if sf == 11 and bw == 250000: - # Your specific configuration: SF11/250kHz - base_tx_time_ms = 500 + (packet_length * 8) # ~500ms + 8ms per byte - elif sf == 7 and bw == 125000: - # Standard configuration - base_tx_time_ms = 100 + (packet_length * 2) # ~100ms + 2ms per byte - else: - # General formula for other configurations - sf_factor = 2 ** (sf - 7) - bw_factor = 125000.0 / bw - base_tx_time_ms = int(100 * sf_factor * bw_factor + (packet_length * sf_factor)) - - # Add reasonable safety margin (2x) for timeout - safety_margin = 2.0 - final_timeout_ms = int(base_tx_time_ms * safety_margin) - - # Reasonable limits: minimum 1 second, maximum 10 seconds - final_timeout_ms = max(1000, min(final_timeout_ms, 10000)) - - # Convert to driver timeout format - driver_timeout = final_timeout_ms * 64 # tOut = timeout * 64 - - return final_timeout_ms, driver_timeout + bw = self.bandwidth + cr = self.coding_rate + preamble_len = self.preamble_length + + # LoRa symbol duration (milliseconds) + t_symbol = (2 ** sf) / bw * 1000 + + # Preamble time + t_preamble = (preamble_len + 4.25) * t_symbol + + # Payload calculation + payload_bits = packet_length * 8 + header_bits = 20 # Explicit header + + # Payload symbols calculation + payload_symbol_count = 8 + max(0, + math.ceil((payload_bits + header_bits - 4*sf + 28 + 16) / (4*sf)) * cr + ) + + t_payload = payload_symbol_count * t_symbol + total_tx_time_ms = t_preamble + t_payload + + # safety margin (50%) + timeout_ms = int(total_tx_time_ms * 1.5) + + # Minimum 500ms, maximum 30s + timeout_ms = max(500, min(timeout_ms, 30000)) + + # Convert to driver format + driver_timeout = timeout_ms * 64 + + logger.debug( + f"TX timing SF{sf}/{bw/1000:.1f}kHz: " + f"symbol={t_symbol:.1f}ms, preamble={t_preamble:.0f}ms, " + f"payload={t_payload:.0f}ms, total={total_tx_time_ms:.0f}ms, " + f"timeout={timeout_ms}ms" + ) + + return timeout_ms, driver_timeout def _prepare_packet_transmission(self, data_list: list, length: int) -> None: """Prepare radio for packet transmission""" From 988c5d5a81f9bb728fc9cc428690b21531a2f639 Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 4 Nov 2025 15:13:00 +0000 Subject: [PATCH 2/4] Refactor TX timeout calculation using C++ MeshCore formula for improved accuracy --- src/pymc_core/hardware/sx1262_wrapper.py | 60 ++++++++++-------------- 1 file changed, 25 insertions(+), 35 deletions(-) diff --git a/src/pymc_core/hardware/sx1262_wrapper.py b/src/pymc_core/hardware/sx1262_wrapper.py index 625bf44..f2cca10 100644 --- a/src/pymc_core/hardware/sx1262_wrapper.py +++ b/src/pymc_core/hardware/sx1262_wrapper.py @@ -11,6 +11,7 @@ import asyncio import logging import time +import math from typing import Callable, Optional from gpiozero import Button, Device, OutputDevice @@ -671,46 +672,35 @@ def begin(self) -> bool: raise RuntimeError(f"Failed to initialize SX1262 radio: {e}") from e def _calculate_tx_timeout(self, packet_length: int) -> tuple[int, int]: - """Calculate transmission timeout using correct LoRa formula""" - import math + """Calculate transmission timeout using C++ MeshCore formula - simple and accurate""" + + symbol_time = float(1 << self.spreading_factor) / float(self.bandwidth) + preamble_time = (self.preamble_length + 4.25) * symbol_time + tmp = (8 * packet_length) - (4 * self.spreading_factor) + 28 + 16 + #CRC is enabled + tmp -= 16 - sf = self.spreading_factor - bw = self.bandwidth - cr = self.coding_rate - preamble_len = self.preamble_length - - # LoRa symbol duration (milliseconds) - t_symbol = (2 ** sf) / bw * 1000 - - # Preamble time - t_preamble = (preamble_len + 4.25) * t_symbol - - # Payload calculation - payload_bits = packet_length * 8 - header_bits = 20 # Explicit header - - # Payload symbols calculation - payload_symbol_count = 8 + max(0, - math.ceil((payload_bits + header_bits - 4*sf + 28 + 16) / (4*sf)) * cr - ) - - t_payload = payload_symbol_count * t_symbol - total_tx_time_ms = t_preamble + t_payload - - # safety margin (50%) - timeout_ms = int(total_tx_time_ms * 1.5) - - # Minimum 500ms, maximum 30s - timeout_ms = max(500, min(timeout_ms, 30000)) + if tmp > 0: + payload_symbols = 8.0 + math.ceil(float(tmp) / float(4 * self.spreading_factor)) * (self.coding_rate + 4) + else: + payload_symbols = 8.0 - # Convert to driver format + payload_time = payload_symbols * symbol_time + air_time_ms = (preamble_time + payload_time) * 1000.0 + timeout_ms = math.ceil(air_time_ms) + 1000 driver_timeout = timeout_ms * 64 logger.debug( - f"TX timing SF{sf}/{bw/1000:.1f}kHz: " - f"symbol={t_symbol:.1f}ms, preamble={t_preamble:.0f}ms, " - f"payload={t_payload:.0f}ms, total={total_tx_time_ms:.0f}ms, " - f"timeout={timeout_ms}ms" + f"TX timing SF{self.spreading_factor}/{self.bandwidth/1000:.1f}kHz " + f"CR4/{self.coding_rate} {packet_length}B: " + f"symbol={symbol_time*1000:.1f}ms, " + f"preamble={preamble_time*1000:.0f}ms, " + f"tmp={tmp}, " + f"payload_syms={payload_symbols:.1f}, " + f"payload={payload_time*1000:.0f}ms, " + f"air_time={air_time_ms:.0f}ms, " + f"timeout={timeout_ms}ms, " + f"driver_timeout={driver_timeout}" ) return timeout_ms, driver_timeout From 6e045ca5b3fc52f3cd6e1f1ee684b2e906a4d27b Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 4 Nov 2025 15:39:29 +0000 Subject: [PATCH 3/4] Tweak LBT backoff strategy --- src/pymc_core/hardware/sx1262_wrapper.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/pymc_core/hardware/sx1262_wrapper.py b/src/pymc_core/hardware/sx1262_wrapper.py index f2cca10..d71659c 100644 --- a/src/pymc_core/hardware/sx1262_wrapper.py +++ b/src/pymc_core/hardware/sx1262_wrapper.py @@ -12,8 +12,8 @@ import logging import time import math +import random from typing import Callable, Optional - from gpiozero import Button, Device, OutputDevice # Force gpiozero to use LGPIOFactory - no RPi.GPIO fallback @@ -758,18 +758,17 @@ async def _prepare_radio_for_tx(self) -> bool: else: lbt_attempts += 1 if lbt_attempts < max_lbt_attempts: - # Channel busy, wait random backoff before trying again - # this may conflict with dispatcher will need testing. - # Channel busy, wait backoff before trying again (MeshCore-inspired) - import random - - base_delay = random.randint(120, 240) - backoff_ms = base_delay + ( - lbt_attempts * 50 - ) # Progressive: 120-290ms, 170-340ms, etc. + + # Jitter (50-200ms) + base_delay = random.randint(50, 200) + # Exponential backoff: base * 2^attempts + backoff_ms = base_delay * (2 ** (lbt_attempts - 1)) + # Cap at 5 seconds maximum + backoff_ms = min(backoff_ms, 5000) + logger.debug( - f"Channel busy (CAD detected activity), backing off {backoff_ms}ms" - f" - >>>>>>> attempt {lbt_attempts} <<<<<<<", + f"Channel busy (CAD detected activity), backing off {backoff_ms}ms " + f"- attempt {lbt_attempts}/{max_lbt_attempts} (exponential backoff)" ) await asyncio.sleep(backoff_ms / 1000.0) else: From e1b9917d57e1049ebbc68d88bc89bccf33bfc88e Mon Sep 17 00:00:00 2001 From: Lloyd Date: Tue, 4 Nov 2025 22:14:41 +0000 Subject: [PATCH 4/4] Bump version to 1.0.4 --- pyproject.toml | 2 +- src/pymc_core/__init__.py | 2 +- tests/test_basic.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f03279c..75b5797 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pymc_core" -version = "1.0.3" +version = "1.0.4" authors = [ {name = "Lloyd Newton", email = "lloyd@rightup.co.uk"}, ] diff --git a/src/pymc_core/__init__.py b/src/pymc_core/__init__.py index 46fece3..1acef86 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.3" +__version__ = "1.0.4" # Core mesh functionality from .node.node import MeshNode diff --git a/tests/test_basic.py b/tests/test_basic.py index 7645b47..6ebb24b 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -2,7 +2,7 @@ def test_version(): - assert __version__ == "1.0.3" + assert __version__ == "1.0.4" def test_import():