Added ability to query physical layer stats on links

This commit is contained in:
Mark Qvist 2023-10-28 00:05:35 +02:00
parent a451b987aa
commit 798dfb1727
6 changed files with 89 additions and 22 deletions

View File

@ -169,6 +169,7 @@ class Link:
self.destination = destination self.destination = destination
self.attached_interface = None self.attached_interface = None
self.__remote_identity = None self.__remote_identity = None
self.__track_phy_stats = False
self._channel = None self._channel = None
if self.destination == None: if self.destination == None:
self.initiator = False self.initiator = False
@ -409,6 +410,38 @@ class Link:
RNS.log("Error occurred while processing RTT packet, tearing down link. The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Error occurred while processing RTT packet, tearing down link. The contained exception was: "+str(e), RNS.LOG_ERROR)
self.teardown() self.teardown()
def track_phy_stats(self, track):
"""
You can enable physical layer statistics on a per-link basis. If this is enabled,
and the link is running over an interface that supports reporting physical layer
statistics, you will be able to retrieve stats such as *RSSI*, *SNR* and physical
*Link Quality* for the link.
:param track: Whether or not to keep track of physical layer statistics. Value must be ``True`` or ``False``.
"""
if track:
self.__track_phy_stats = True
else:
self.__track_phy_stats = False
def get_rssi(self):
"""
:returns: The physical layer *Received Signal Strength Indication* if available, otherwise ``None``. Physical layer statistics must be enabled on the link for this method to return a value.
"""
return self.rssi
def get_snr(self):
"""
:returns: The physical layer *Signal-to-Noise Ratio* if available, otherwise ``None``. Physical layer statistics must be enabled on the link for this method to return a value.
"""
return self.rssi
def get_q(self):
"""
:returns: The physical layer *Link Quality* if available, otherwise ``None``. Physical layer statistics must be enabled on the link for this method to return a value.
"""
return self.rssi
def get_establishment_rate(self): def get_establishment_rate(self):
""" """
:returns: The data transfer rate at which the link establishment procedure ocurred, in bits per second. :returns: The data transfer rate at which the link establishment procedure ocurred, in bits per second.
@ -584,13 +617,20 @@ class Link:
sleep(sleep_time) sleep(sleep_time)
def __update_phy_stats(self, packet): def __update_phy_stats(self, packet, query_shared = True):
if packet.rssi != None: if self.__track_phy_stats:
self.rssi = packet.rssi if query_shared:
if packet.snr != None: reticulum = RNS.Reticulum.get_instance()
self.snr = packet.snr if packet.rssi == None: packet.rssi = reticulum.get_packet_rssi(packet.packet_hash)
if packet.q != None: if packet.snr == None: packet.snr = reticulum.get_packet_snr(packet.packet_hash)
self.q = packet.q if packet.q == None: packet.q = reticulum.get_packet_q(packet.packet_hash)
if packet.rssi != None:
self.rssi = packet.rssi
if packet.snr != None:
self.snr = packet.snr
if packet.q != None:
self.q = packet.q
def send_keepalive(self): def send_keepalive(self):
keepalive_packet = RNS.Packet(self, bytes([0xFF]), context=RNS.Packet.KEEPALIVE) keepalive_packet = RNS.Packet(self, bytes([0xFF]), context=RNS.Packet.KEEPALIVE)
@ -705,6 +745,7 @@ class Link:
self.status = Link.ACTIVE self.status = Link.ACTIVE
if packet.packet_type == RNS.Packet.DATA: if packet.packet_type == RNS.Packet.DATA:
should_query = False
if packet.context == RNS.Packet.NONE: if packet.context == RNS.Packet.NONE:
plaintext = self.decrypt(packet.data) plaintext = self.decrypt(packet.data)
if self.callbacks.packet != None: if self.callbacks.packet != None:
@ -714,15 +755,18 @@ class Link:
if self.destination.proof_strategy == RNS.Destination.PROVE_ALL: if self.destination.proof_strategy == RNS.Destination.PROVE_ALL:
packet.prove() packet.prove()
should_query = True
elif self.destination.proof_strategy == RNS.Destination.PROVE_APP: elif self.destination.proof_strategy == RNS.Destination.PROVE_APP:
if self.destination.callbacks.proof_requested: if self.destination.callbacks.proof_requested:
try: try:
self.destination.callbacks.proof_requested(packet) if self.destination.callbacks.proof_requested(packet):
packet.prove()
should_query = True
except Exception as e: except Exception as e:
RNS.log("Error while executing proof request callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Error while executing proof request callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
self.__update_phy_stats(packet) self.__update_phy_stats(packet, query_shared=should_query)
elif packet.context == RNS.Packet.LINKIDENTIFY: elif packet.context == RNS.Packet.LINKIDENTIFY:
plaintext = self.decrypt(packet.data) plaintext = self.decrypt(packet.data)
@ -742,7 +786,7 @@ class Link:
except Exception as e: except Exception as e:
RNS.log("Error while executing remote identified callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Error while executing remote identified callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
self.__update_phy_stats(packet) self.__update_phy_stats(packet, query_shared=True)
elif packet.context == RNS.Packet.REQUEST: elif packet.context == RNS.Packet.REQUEST:
try: try:
@ -750,7 +794,7 @@ class Link:
packed_request = self.decrypt(packet.data) packed_request = self.decrypt(packet.data)
unpacked_request = umsgpack.unpackb(packed_request) unpacked_request = umsgpack.unpackb(packed_request)
self.handle_request(request_id, unpacked_request) self.handle_request(request_id, unpacked_request)
self.__update_phy_stats(packet) self.__update_phy_stats(packet, query_shared=True)
except Exception as e: except Exception as e:
RNS.log("Error occurred while handling request. The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Error occurred while handling request. The contained exception was: "+str(e), RNS.LOG_ERROR)
@ -762,21 +806,22 @@ class Link:
response_data = unpacked_response[1] response_data = unpacked_response[1]
transfer_size = len(umsgpack.packb(response_data))-2 transfer_size = len(umsgpack.packb(response_data))-2
self.handle_response(request_id, response_data, transfer_size, transfer_size) self.handle_response(request_id, response_data, transfer_size, transfer_size)
self.__update_phy_stats(packet) self.__update_phy_stats(packet, query_shared=True)
except Exception as e: except Exception as e:
RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR)
elif packet.context == RNS.Packet.LRRTT: elif packet.context == RNS.Packet.LRRTT:
if not self.initiator: if not self.initiator:
self.rtt_packet(packet) self.rtt_packet(packet)
self.__update_phy_stats(packet) self.__update_phy_stats(packet, query_shared=True)
elif packet.context == RNS.Packet.LINKCLOSE: elif packet.context == RNS.Packet.LINKCLOSE:
self.teardown_packet(packet) self.teardown_packet(packet)
self.__update_phy_stats(packet, query_shared=True)
elif packet.context == RNS.Packet.RESOURCE_ADV: elif packet.context == RNS.Packet.RESOURCE_ADV:
packet.plaintext = self.decrypt(packet.data) packet.plaintext = self.decrypt(packet.data)
self.__update_phy_stats(packet) self.__update_phy_stats(packet, query_shared=True)
if RNS.ResourceAdvertisement.is_request(packet): if RNS.ResourceAdvertisement.is_request(packet):
RNS.Resource.accept(packet, callback=self.request_resource_concluded) RNS.Resource.accept(packet, callback=self.request_resource_concluded)
@ -804,7 +849,7 @@ class Link:
elif packet.context == RNS.Packet.RESOURCE_REQ: elif packet.context == RNS.Packet.RESOURCE_REQ:
plaintext = self.decrypt(packet.data) plaintext = self.decrypt(packet.data)
self.__update_phy_stats(packet) self.__update_phy_stats(packet, query_shared=True)
if ord(plaintext[:1]) == RNS.Resource.HASHMAP_IS_EXHAUSTED: if ord(plaintext[:1]) == RNS.Resource.HASHMAP_IS_EXHAUSTED:
resource_hash = plaintext[1+RNS.Resource.MAPHASH_LEN:RNS.Identity.HASHLENGTH//8+1+RNS.Resource.MAPHASH_LEN] resource_hash = plaintext[1+RNS.Resource.MAPHASH_LEN:RNS.Identity.HASHLENGTH//8+1+RNS.Resource.MAPHASH_LEN]
else: else:
@ -820,7 +865,7 @@ class Link:
elif packet.context == RNS.Packet.RESOURCE_HMU: elif packet.context == RNS.Packet.RESOURCE_HMU:
plaintext = self.decrypt(packet.data) plaintext = self.decrypt(packet.data)
self.__update_phy_stats(packet) self.__update_phy_stats(packet, query_shared=True)
resource_hash = plaintext[:RNS.Identity.HASHLENGTH//8] resource_hash = plaintext[:RNS.Identity.HASHLENGTH//8]
for resource in self.incoming_resources: for resource in self.incoming_resources:
if resource_hash == resource.hash: if resource_hash == resource.hash:
@ -878,7 +923,7 @@ class Link:
for resource in self.outgoing_resources: for resource in self.outgoing_resources:
if resource_hash == resource.hash: if resource_hash == resource.hash:
resource.validate_proof(packet.data) resource.validate_proof(packet.data)
self.__update_phy_stats(packet) self.__update_phy_stats(packet, query_shared=True)
self.watchdog_lock = False self.watchdog_lock = False

View File

@ -371,7 +371,7 @@ class PacketReceipt:
if packet.destination.type == RNS.Destination.LINK: if packet.destination.type == RNS.Destination.LINK:
self.timeout = packet.destination.rtt * packet.destination.traffic_timeout_factor self.timeout = packet.destination.rtt * packet.destination.traffic_timeout_factor
else: else:
self.timeout = RNS.Reticulum.get_instance().get_first_hop_timeout(destination.hash) self.timeout = RNS.Reticulum.get_instance().get_first_hop_timeout(self.destination.hash)
self.timeout += Packet.TIMEOUT_PER_HOP * RNS.Transport.hops_to(self.destination.hash) self.timeout += Packet.TIMEOUT_PER_HOP * RNS.Transport.hops_to(self.destination.hash)
def get_status(self): def get_status(self):

View File

@ -175,7 +175,7 @@ class Resource:
if not resource.link.has_incoming_resource(resource): if not resource.link.has_incoming_resource(resource):
resource.link.register_incoming_resource(resource) resource.link.register_incoming_resource(resource)
RNS.log("Accepting resource advertisement for "+RNS.prettyhexrep(resource.hash), RNS.LOG_DEBUG) RNS.log(f"Accepting resource advertisement for {RNS.prettyhexrep(resource.hash)}. Transfer size is {RNS.prettysize(resource.size)} in {resource.total_parts} parts.", RNS.LOG_DEBUG)
if resource.link.callbacks.resource_started != None: if resource.link.callbacks.resource_started != None:
try: try:
resource.link.callbacks.resource_started(resource) resource.link.callbacks.resource_started(resource)

View File

@ -1109,6 +1109,9 @@ class Reticulum:
if path == "packet_snr": if path == "packet_snr":
rpc_connection.send(self.get_packet_snr(call["packet_hash"])) rpc_connection.send(self.get_packet_snr(call["packet_hash"]))
if path == "packet_q":
rpc_connection.send(self.get_packet_q(call["packet_hash"]))
if "drop" in call: if "drop" in call:
path = call["drop"] path = call["drop"]
@ -1371,6 +1374,21 @@ class Reticulum:
return None return None
def get_packet_q(self, packet_hash):
if self.is_connected_to_shared_instance:
rpc_connection = multiprocessing.connection.Client(self.rpc_addr, authkey=self.rpc_key)
rpc_connection.send({"get": "packet_q", "packet_hash": packet_hash})
response = rpc_connection.recv()
return response
else:
for entry in RNS.Transport.local_client_q_cache:
if entry[0] == packet_hash:
return entry[1]
return None
@staticmethod @staticmethod
def should_use_implicit_proof(): def should_use_implicit_proof():
""" """

View File

@ -116,7 +116,7 @@ class Transport:
local_client_rssi_cache = [] local_client_rssi_cache = []
local_client_snr_cache = [] local_client_snr_cache = []
local_client_q_cache = [] local_client_q_cache = []
LOCAL_CLIENT_CACHE_MAXSIZE = 512 LOCAL_CLIENT_CACHE_MAXSIZE = 512
pending_local_path_requests = {} pending_local_path_requests = {}

View File

@ -31,7 +31,7 @@ import argparse
from RNS._version import __version__ from RNS._version import __version__
DEFAULT_PROBE_SIZE = 16 DEFAULT_PROBE_SIZE = 16
DEFAULT_TIMEOUT = 15 DEFAULT_TIMEOUT = 12
def program_setup(configdir, destination_hexhash, size=None, full_name = None, verbosity = 0, timeout=None): def program_setup(configdir, destination_hexhash, size=None, full_name = None, verbosity = 0, timeout=None):
if size == None: size = DEFAULT_PROBE_SIZE if size == None: size = DEFAULT_PROBE_SIZE
@ -73,7 +73,7 @@ def program_setup(configdir, destination_hexhash, size=None, full_name = None, v
print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ") print("Path to "+RNS.prettyhexrep(destination_hash)+" requested ", end=" ")
sys.stdout.flush() sys.stdout.flush()
_timeout = time.time() + (timeout or DEFAULT_TIMEOUT) _timeout = time.time() + (timeout or DEFAULT_TIMEOUT+reticulum.get_first_hop_timeout(destination_hash))
i = 0 i = 0
syms = "⢄⢂⢁⡁⡈⡐⡠" syms = "⢄⢂⢁⡁⡈⡐⡠"
while not RNS.Transport.has_path(destination_hash) and not time.time() > _timeout: while not RNS.Transport.has_path(destination_hash) and not time.time() > _timeout:
@ -149,12 +149,16 @@ def program_setup(configdir, destination_hexhash, size=None, full_name = None, v
if reticulum.is_connected_to_shared_instance: if reticulum.is_connected_to_shared_instance:
reception_rssi = reticulum.get_packet_rssi(receipt.proof_packet.packet_hash) reception_rssi = reticulum.get_packet_rssi(receipt.proof_packet.packet_hash)
reception_snr = reticulum.get_packet_snr(receipt.proof_packet.packet_hash) reception_snr = reticulum.get_packet_snr(receipt.proof_packet.packet_hash)
reception_q = reticulum.get_packet_q(receipt.proof_packet.packet_hash)
if reception_rssi != None: if reception_rssi != None:
reception_stats += " [RSSI "+str(reception_rssi)+" dBm]" reception_stats += " [RSSI "+str(reception_rssi)+" dBm]"
if reception_snr != None: if reception_snr != None:
reception_stats += " [SNR "+str(reception_snr)+" dB]" reception_stats += " [SNR "+str(reception_snr)+" dB]"
if reception_q != None:
reception_stats += " [Link Quality "+str(reception_q)+"%]"
else: else:
if receipt.proof_packet != None: if receipt.proof_packet != None: