Add LoRa support with background sync and settings UI
- Progressive retry in rns_client.py: fast timeout (15s) then slow (60s+) for LoRa/multi-hop links, with automatic fallback - Background sync threads so subscriptions page returns immediately with syncing/error status indicators per subscription - LoRa RNode configuration in settings page with serial port and expandable advanced radio settings (frequency, bandwidth, etc.) - Internet transport now toggleable alongside LoRa — users can enable one, the other, or both - Reticulum config auto-generated from settings on startup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6ffd38d58c
commit
ce50150363
3 changed files with 227 additions and 96 deletions
|
|
@ -1,9 +1,15 @@
|
|||
import json
|
||||
import time
|
||||
import RNS
|
||||
|
||||
APP_NAME = "tinyweb"
|
||||
ASPECTS = ["server"]
|
||||
REQUEST_TIMEOUT = 30
|
||||
|
||||
# Two-tier timeout profiles: fast first, then slow for LoRa/multi-hop links
|
||||
_TIMEOUT_TIERS = [
|
||||
{"path": 15, "link": 15, "request": 30, "poll": 0.25},
|
||||
{"path": 60, "link": 60, "request": 120, "poll": 1.0},
|
||||
]
|
||||
|
||||
|
||||
def fetch_remote_sites(dest_hash_hex, since=""):
|
||||
|
|
@ -11,18 +17,40 @@ def fetch_remote_sites(dest_hash_hex, since=""):
|
|||
Connect to a remote TinyWeb instance over Reticulum and fetch its
|
||||
shared sites. Returns the response dict from /api/sites, or raises
|
||||
an exception on failure. Pass `since` as ISO timestamp for delta sync.
|
||||
|
||||
Uses progressive timeouts: tries fast first, then retries with longer
|
||||
timeouts for slow links (LoRa, multi-hop).
|
||||
"""
|
||||
last_error = None
|
||||
for tier in _TIMEOUT_TIERS:
|
||||
try:
|
||||
return _fetch(dest_hash_hex, since, tier)
|
||||
except PermissionError:
|
||||
raise # Don't retry permission errors
|
||||
except Exception as e:
|
||||
last_error = e
|
||||
continue
|
||||
raise ConnectionError(
|
||||
f"Could not reach {dest_hash_hex} after {len(_TIMEOUT_TIERS)} attempts: {last_error}"
|
||||
)
|
||||
|
||||
|
||||
def _fetch(dest_hash_hex, since, timeouts):
|
||||
"""Single fetch attempt with the given timeout profile."""
|
||||
dest_hash = bytes.fromhex(dest_hash_hex)
|
||||
poll = timeouts["poll"]
|
||||
|
||||
# Resolve path if needed
|
||||
if not RNS.Transport.has_path(dest_hash):
|
||||
RNS.Transport.request_path(dest_hash)
|
||||
elapsed = 0
|
||||
while not RNS.Transport.has_path(dest_hash) and elapsed < 15:
|
||||
time.sleep(0.5)
|
||||
elapsed += 0.5
|
||||
while not RNS.Transport.has_path(dest_hash) and elapsed < timeouts["path"]:
|
||||
time.sleep(poll)
|
||||
elapsed += poll
|
||||
if not RNS.Transport.has_path(dest_hash):
|
||||
raise ConnectionError(f"Could not find path to {dest_hash_hex}")
|
||||
raise ConnectionError(
|
||||
f"Could not find path to {dest_hash_hex} ({timeouts['path']}s timeout)"
|
||||
)
|
||||
|
||||
server_identity = RNS.Identity.recall(dest_hash)
|
||||
if server_identity is None:
|
||||
|
|
@ -39,15 +67,16 @@ def fetch_remote_sites(dest_hash_hex, since=""):
|
|||
# Establish link
|
||||
link = RNS.Link(destination)
|
||||
elapsed = 0
|
||||
while link.status == RNS.Link.PENDING and elapsed < 15:
|
||||
time.sleep(0.25)
|
||||
elapsed += 0.25
|
||||
while link.status == RNS.Link.PENDING and elapsed < timeouts["link"]:
|
||||
time.sleep(poll)
|
||||
elapsed += poll
|
||||
|
||||
if link.status != RNS.Link.ACTIVE:
|
||||
raise ConnectionError(f"Could not establish link to {dest_hash_hex}")
|
||||
raise ConnectionError(
|
||||
f"Could not establish link to {dest_hash_hex} ({timeouts['link']}s timeout)"
|
||||
)
|
||||
|
||||
try:
|
||||
# Request /api/sites
|
||||
query = {"since": [since]} if since else {}
|
||||
request_data = {
|
||||
"method": "GET",
|
||||
|
|
@ -57,13 +86,14 @@ def fetch_remote_sites(dest_hash_hex, since=""):
|
|||
"gateway_host": "",
|
||||
}
|
||||
|
||||
receipt = link.request("/tinyweb", data=request_data, timeout=REQUEST_TIMEOUT)
|
||||
req_timeout = timeouts["request"]
|
||||
receipt = link.request("/tinyweb", data=request_data, timeout=req_timeout)
|
||||
|
||||
elapsed = 0
|
||||
done = (RNS.RequestReceipt.READY, RNS.RequestReceipt.DELIVERED, RNS.RequestReceipt.FAILED)
|
||||
while receipt.get_status() not in done and elapsed < REQUEST_TIMEOUT:
|
||||
time.sleep(0.5)
|
||||
elapsed += 0.5
|
||||
while receipt.get_status() not in done and elapsed < req_timeout:
|
||||
time.sleep(poll)
|
||||
elapsed += poll
|
||||
|
||||
if receipt.get_status() in (RNS.RequestReceipt.READY, RNS.RequestReceipt.DELIVERED):
|
||||
resp = receipt.get_response()
|
||||
|
|
@ -71,9 +101,10 @@ def fetch_remote_sites(dest_hash_hex, since=""):
|
|||
raise PermissionError("That instance has sharing disabled.")
|
||||
if resp["status"] != 200:
|
||||
raise ConnectionError(f"Remote returned status {resp['status']}")
|
||||
import json
|
||||
return json.loads(resp["body"])
|
||||
else:
|
||||
raise ConnectionError(f"Request failed or timed out")
|
||||
raise ConnectionError(
|
||||
f"Request failed or timed out ({req_timeout}s timeout)"
|
||||
)
|
||||
finally:
|
||||
link.teardown()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue