From 798dfb1727c69a1949702452712bc46cfa6d893f Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 28 Oct 2023 00:05:35 +0200 Subject: [PATCH] Added ability to query physical layer stats on links --- RNS/Link.py | 79 +++++++++++++++++++++++++++++++--------- RNS/Packet.py | 2 +- RNS/Resource.py | 2 +- RNS/Reticulum.py | 18 +++++++++ RNS/Transport.py | 2 +- RNS/Utilities/rnprobe.py | 8 +++- 6 files changed, 89 insertions(+), 22 deletions(-) diff --git a/RNS/Link.py b/RNS/Link.py index e34e994..07dd1e1 100644 --- a/RNS/Link.py +++ b/RNS/Link.py @@ -169,6 +169,7 @@ class Link: self.destination = destination self.attached_interface = None self.__remote_identity = None + self.__track_phy_stats = False self._channel = None if self.destination == None: 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) 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): """ :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) - def __update_phy_stats(self, packet): - 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 __update_phy_stats(self, packet, query_shared = True): + if self.__track_phy_stats: + if query_shared: + reticulum = RNS.Reticulum.get_instance() + if packet.rssi == None: packet.rssi = reticulum.get_packet_rssi(packet.packet_hash) + if packet.snr == None: packet.snr = reticulum.get_packet_snr(packet.packet_hash) + 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): keepalive_packet = RNS.Packet(self, bytes([0xFF]), context=RNS.Packet.KEEPALIVE) @@ -705,6 +745,7 @@ class Link: self.status = Link.ACTIVE if packet.packet_type == RNS.Packet.DATA: + should_query = False if packet.context == RNS.Packet.NONE: plaintext = self.decrypt(packet.data) if self.callbacks.packet != None: @@ -714,15 +755,18 @@ class Link: if self.destination.proof_strategy == RNS.Destination.PROVE_ALL: packet.prove() + should_query = True elif self.destination.proof_strategy == RNS.Destination.PROVE_APP: if self.destination.callbacks.proof_requested: try: - self.destination.callbacks.proof_requested(packet) + if self.destination.callbacks.proof_requested(packet): + packet.prove() + should_query = True except Exception as e: 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: plaintext = self.decrypt(packet.data) @@ -742,7 +786,7 @@ class Link: except Exception as e: 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: try: @@ -750,7 +794,7 @@ class Link: packed_request = self.decrypt(packet.data) unpacked_request = umsgpack.unpackb(packed_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: 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] transfer_size = len(umsgpack.packb(response_data))-2 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: RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR) elif packet.context == RNS.Packet.LRRTT: if not self.initiator: self.rtt_packet(packet) - self.__update_phy_stats(packet) + self.__update_phy_stats(packet, query_shared=True) elif packet.context == RNS.Packet.LINKCLOSE: self.teardown_packet(packet) + self.__update_phy_stats(packet, query_shared=True) elif packet.context == RNS.Packet.RESOURCE_ADV: 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): RNS.Resource.accept(packet, callback=self.request_resource_concluded) @@ -804,7 +849,7 @@ class Link: elif packet.context == RNS.Packet.RESOURCE_REQ: 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: resource_hash = plaintext[1+RNS.Resource.MAPHASH_LEN:RNS.Identity.HASHLENGTH//8+1+RNS.Resource.MAPHASH_LEN] else: @@ -820,7 +865,7 @@ class Link: elif packet.context == RNS.Packet.RESOURCE_HMU: 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] for resource in self.incoming_resources: if resource_hash == resource.hash: @@ -878,7 +923,7 @@ class Link: for resource in self.outgoing_resources: if resource_hash == resource.hash: resource.validate_proof(packet.data) - self.__update_phy_stats(packet) + self.__update_phy_stats(packet, query_shared=True) self.watchdog_lock = False diff --git a/RNS/Packet.py b/RNS/Packet.py index c68e2b4..e47f05b 100755 --- a/RNS/Packet.py +++ b/RNS/Packet.py @@ -371,7 +371,7 @@ class PacketReceipt: if packet.destination.type == RNS.Destination.LINK: self.timeout = packet.destination.rtt * packet.destination.traffic_timeout_factor 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) def get_status(self): diff --git a/RNS/Resource.py b/RNS/Resource.py index c0e0f7e..e8e84f2 100644 --- a/RNS/Resource.py +++ b/RNS/Resource.py @@ -175,7 +175,7 @@ class Resource: if not resource.link.has_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: try: resource.link.callbacks.resource_started(resource) diff --git a/RNS/Reticulum.py b/RNS/Reticulum.py index 88d5c8d..06a70b5 100755 --- a/RNS/Reticulum.py +++ b/RNS/Reticulum.py @@ -1109,6 +1109,9 @@ class Reticulum: if path == "packet_snr": 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: path = call["drop"] @@ -1371,6 +1374,21 @@ class Reticulum: 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 def should_use_implicit_proof(): """ diff --git a/RNS/Transport.py b/RNS/Transport.py index 2da8d16..1cff195 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -116,7 +116,7 @@ class Transport: local_client_rssi_cache = [] local_client_snr_cache = [] - local_client_q_cache = [] + local_client_q_cache = [] LOCAL_CLIENT_CACHE_MAXSIZE = 512 pending_local_path_requests = {} diff --git a/RNS/Utilities/rnprobe.py b/RNS/Utilities/rnprobe.py index 8a299c6..ae628a3 100644 --- a/RNS/Utilities/rnprobe.py +++ b/RNS/Utilities/rnprobe.py @@ -31,7 +31,7 @@ import argparse from RNS._version import __version__ 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): 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=" ") 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 syms = "⢄⢂⢁⡁⡈⡐⡠" 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: reception_rssi = reticulum.get_packet_rssi(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: reception_stats += " [RSSI "+str(reception_rssi)+" dBm]" if reception_snr != None: reception_stats += " [SNR "+str(reception_snr)+" dB]" + + if reception_q != None: + reception_stats += " [Link Quality "+str(reception_q)+"%]" else: if receipt.proof_packet != None: