Skip to content

Commit e0ccf6a

Browse files
authored
Merge pull request #13 from rightup/dev
This PR bumps the version to 1.0.4 and refactors the LoRa transmission timeout calculation and channel busy backoff logic in the SX1262 radio driver. The changes align the Python implementation with a C++ MeshCore formula for more accurate timing calculations. - Version updated from 1.0.3 to 1.0.4 across all relevant files - Replaced approximate timeout calculation with precise LoRa airtime formula - Improved channel busy backoff with exponential backoff strategy
2 parents db2bafe + e1b9917 commit e0ccf6a

File tree

4 files changed

+46
-42
lines changed

4 files changed

+46
-42
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "pymc_core"
7-
version = "1.0.3"
7+
version = "1.0.4"
88
authors = [
99
{name = "Lloyd Newton", email = "lloyd@rightup.co.uk"},
1010
]

src/pymc_core/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Clean, simple API for building mesh network applications.
44
"""
55

6-
__version__ = "1.0.3"
6+
__version__ = "1.0.4"
77

88
# Core mesh functionality
99
from .node.node import MeshNode

src/pymc_core/hardware/sx1262_wrapper.py

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
import asyncio
1212
import logging
1313
import time
14+
import math
15+
import random
1416
from typing import Callable, Optional
15-
1617
from gpiozero import Button, Device, OutputDevice
1718

1819
# Force gpiozero to use LGPIOFactory - no RPi.GPIO fallback
@@ -671,34 +672,38 @@ def begin(self) -> bool:
671672
raise RuntimeError(f"Failed to initialize SX1262 radio: {e}") from e
672673

673674
def _calculate_tx_timeout(self, packet_length: int) -> tuple[int, int]:
674-
"""Calculate transmission timeout based on modulation parameters"""
675-
sf = self.spreading_factor
676-
bw = self.bandwidth
677-
678-
# Realistic timeout calculation based on actual LoRa performance
679-
if sf == 11 and bw == 250000:
680-
# Your specific configuration: SF11/250kHz
681-
base_tx_time_ms = 500 + (packet_length * 8) # ~500ms + 8ms per byte
682-
elif sf == 7 and bw == 125000:
683-
# Standard configuration
684-
base_tx_time_ms = 100 + (packet_length * 2) # ~100ms + 2ms per byte
675+
"""Calculate transmission timeout using C++ MeshCore formula - simple and accurate"""
676+
677+
symbol_time = float(1 << self.spreading_factor) / float(self.bandwidth)
678+
preamble_time = (self.preamble_length + 4.25) * symbol_time
679+
tmp = (8 * packet_length) - (4 * self.spreading_factor) + 28 + 16
680+
#CRC is enabled
681+
tmp -= 16
682+
683+
if tmp > 0:
684+
payload_symbols = 8.0 + math.ceil(float(tmp) / float(4 * self.spreading_factor)) * (self.coding_rate + 4)
685685
else:
686-
# General formula for other configurations
687-
sf_factor = 2 ** (sf - 7)
688-
bw_factor = 125000.0 / bw
689-
base_tx_time_ms = int(100 * sf_factor * bw_factor + (packet_length * sf_factor))
690-
691-
# Add reasonable safety margin (2x) for timeout
692-
safety_margin = 2.0
693-
final_timeout_ms = int(base_tx_time_ms * safety_margin)
694-
695-
# Reasonable limits: minimum 1 second, maximum 10 seconds
696-
final_timeout_ms = max(1000, min(final_timeout_ms, 10000))
697-
698-
# Convert to driver timeout format
699-
driver_timeout = final_timeout_ms * 64 # tOut = timeout * 64
700-
701-
return final_timeout_ms, driver_timeout
686+
payload_symbols = 8.0
687+
688+
payload_time = payload_symbols * symbol_time
689+
air_time_ms = (preamble_time + payload_time) * 1000.0
690+
timeout_ms = math.ceil(air_time_ms) + 1000
691+
driver_timeout = timeout_ms * 64
692+
693+
logger.debug(
694+
f"TX timing SF{self.spreading_factor}/{self.bandwidth/1000:.1f}kHz "
695+
f"CR4/{self.coding_rate} {packet_length}B: "
696+
f"symbol={symbol_time*1000:.1f}ms, "
697+
f"preamble={preamble_time*1000:.0f}ms, "
698+
f"tmp={tmp}, "
699+
f"payload_syms={payload_symbols:.1f}, "
700+
f"payload={payload_time*1000:.0f}ms, "
701+
f"air_time={air_time_ms:.0f}ms, "
702+
f"timeout={timeout_ms}ms, "
703+
f"driver_timeout={driver_timeout}"
704+
)
705+
706+
return timeout_ms, driver_timeout
702707

703708
def _prepare_packet_transmission(self, data_list: list, length: int) -> None:
704709
"""Prepare radio for packet transmission"""
@@ -753,18 +758,17 @@ async def _prepare_radio_for_tx(self) -> bool:
753758
else:
754759
lbt_attempts += 1
755760
if lbt_attempts < max_lbt_attempts:
756-
# Channel busy, wait random backoff before trying again
757-
# this may conflict with dispatcher will need testing.
758-
# Channel busy, wait backoff before trying again (MeshCore-inspired)
759-
import random
760-
761-
base_delay = random.randint(120, 240)
762-
backoff_ms = base_delay + (
763-
lbt_attempts * 50
764-
) # Progressive: 120-290ms, 170-340ms, etc.
761+
762+
# Jitter (50-200ms)
763+
base_delay = random.randint(50, 200)
764+
# Exponential backoff: base * 2^attempts
765+
backoff_ms = base_delay * (2 ** (lbt_attempts - 1))
766+
# Cap at 5 seconds maximum
767+
backoff_ms = min(backoff_ms, 5000)
768+
765769
logger.debug(
766-
f"Channel busy (CAD detected activity), backing off {backoff_ms}ms"
767-
f" - >>>>>>> attempt {lbt_attempts} <<<<<<<",
770+
f"Channel busy (CAD detected activity), backing off {backoff_ms}ms "
771+
f"- attempt {lbt_attempts}/{max_lbt_attempts} (exponential backoff)"
768772
)
769773
await asyncio.sleep(backoff_ms / 1000.0)
770774
else:

tests/test_basic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
def test_version():
5-
assert __version__ == "1.0.3"
5+
assert __version__ == "1.0.4"
66

77

88
def test_import():

0 commit comments

Comments
 (0)