Files
MeshChatX/meshchatx/src/backend/rnprobe_handler.py
2026-01-01 15:05:29 -06:00

138 lines
5.0 KiB
Python

import asyncio
import os
import time
import RNS
class RNProbeHandler:
DEFAULT_PROBE_SIZE = 16
DEFAULT_TIMEOUT = 12
def __init__(self, reticulum_instance, identity):
self.reticulum = reticulum_instance
self.identity = identity
async def probe_destination(
self,
destination_hash: bytes,
full_name: str,
size: int = DEFAULT_PROBE_SIZE,
timeout: float | None = None,
wait: float = 0,
probes: int = 1,
):
try:
app_name, aspects = RNS.Destination.app_and_aspects_from_name(full_name)
except Exception as e:
msg = f"Invalid destination name: {e}"
raise ValueError(msg)
if not RNS.Transport.has_path(destination_hash):
RNS.Transport.request_path(destination_hash)
timeout_after = time.time() + (timeout or self.DEFAULT_TIMEOUT + self.reticulum.get_first_hop_timeout(destination_hash))
while not RNS.Transport.has_path(destination_hash) and time.time() < timeout_after:
await asyncio.sleep(0.1)
if not RNS.Transport.has_path(destination_hash):
msg = "Path request timed out"
raise TimeoutError(msg)
server_identity = RNS.Identity.recall(destination_hash)
request_destination = RNS.Destination(
server_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
app_name,
*aspects,
)
results = []
sent = 0
while probes > 0:
if sent > 0:
await asyncio.sleep(wait)
try:
probe = RNS.Packet(request_destination, os.urandom(size))
probe.pack()
except OSError:
msg = f"Probe packet size of {len(probe.raw)} bytes exceeds MTU of {RNS.Reticulum.MTU} bytes"
raise ValueError(msg)
receipt = probe.send()
sent += 1
next_hop = self.reticulum.get_next_hop(destination_hash)
via_str = f" via {RNS.prettyhexrep(next_hop)}" if next_hop else ""
if_name = self.reticulum.get_next_hop_if_name(destination_hash)
if_str = f" on {if_name}" if if_name and if_name != "None" else ""
timeout_after = time.time() + (timeout or self.DEFAULT_TIMEOUT + self.reticulum.get_first_hop_timeout(destination_hash))
while receipt.status == RNS.PacketReceipt.SENT and time.time() < timeout_after:
await asyncio.sleep(0.1)
result: dict = {
"probe_number": sent,
"size": size,
"destination": RNS.prettyhexrep(destination_hash),
"via": via_str,
"interface": if_str,
"status": "timeout",
}
if time.time() > timeout_after:
result["status"] = "timeout"
elif receipt.status == RNS.PacketReceipt.DELIVERED:
hops = RNS.Transport.hops_to(destination_hash)
rtt = receipt.get_rtt()
if rtt >= 1:
rtt_str = f"{round(rtt, 3)} seconds"
else:
rtt_str = f"{round(rtt * 1000, 3)} milliseconds"
reception_stats = {}
if self.reticulum.is_connected_to_shared_instance:
reception_rssi = self.reticulum.get_packet_rssi(receipt.proof_packet.packet_hash)
reception_snr = self.reticulum.get_packet_snr(receipt.proof_packet.packet_hash)
reception_q = self.reticulum.get_packet_q(receipt.proof_packet.packet_hash)
if reception_rssi is not None:
reception_stats["rssi"] = reception_rssi
if reception_snr is not None:
reception_stats["snr"] = reception_snr
if reception_q is not None:
reception_stats["quality"] = reception_q
elif receipt.proof_packet:
if receipt.proof_packet.rssi is not None:
reception_stats["rssi"] = receipt.proof_packet.rssi
if receipt.proof_packet.snr is not None:
reception_stats["snr"] = receipt.proof_packet.snr
result.update(
{
"status": "delivered",
"hops": hops,
"rtt": rtt,
"rtt_string": rtt_str,
"reception_stats": reception_stats,
},
)
else:
result["status"] = "failed"
results.append(result)
probes -= 1
return {
"results": results,
"sent": sent,
"delivered": sum(1 for r in results if r["status"] == "delivered"),
"timeouts": sum(1 for r in results if r["status"] == "timeout"),
"failed": sum(1 for r in results if r["status"] == "failed"),
}