Speedtest.py
1 ########################################################## 2 # This RNS example demonstrates a simple speedtest # 3 # program to measure link throughput. # 4 # # 5 # The current configuration is suited for testing fast # 6 # links. If you want to measure slow links like LoRa or # 7 # packet radio, you must significantly lower the # 8 # data_cap variable, which defines how much data is sent # 9 # for each test. # 10 ########################################################## 11 12 import os 13 import sys 14 import time 15 import argparse 16 import RNS 17 18 # Let's define an app name. We'll use this for all 19 # destinations we create. 20 APP_NAME = "example_utilities" 21 22 ########################################################## 23 #### Server Part ######################################### 24 ########################################################## 25 26 latest_client_link = None 27 first_packet_at = None 28 last_packet_at = None 29 received_data = 0 30 rc = 0 31 data_cap = 2*1024*1024 32 printed = False 33 34 # This initialisation is executed when the users chooses 35 # to run as a server 36 def server(configpath): 37 # We must first initialise Reticulum 38 reticulum = RNS.Reticulum(configpath) 39 40 # Randomly create a new identity for our link example 41 server_identity = RNS.Identity() 42 43 # We create a destination that clients can connect to. We 44 # want clients to create links to this destination, so we 45 # need to create a "single" destination type. 46 server_destination = RNS.Destination( 47 server_identity, 48 RNS.Destination.IN, 49 RNS.Destination.SINGLE, 50 APP_NAME, 51 "speedtest" 52 ) 53 54 # We configure a function that will get called every time 55 # a new client creates a link to this destination. 56 server_destination.set_link_established_callback(client_connected) 57 58 # Everything's ready! 59 # Let's Wait for client requests or user input 60 server_loop(server_destination) 61 62 def server_loop(destination): 63 # Let the user know that everything is ready 64 RNS.log( 65 "Speedtest "+ 66 RNS.prettyhexrep(destination.hash)+ 67 " running, waiting for a connection." 68 ) 69 70 RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)") 71 72 # We enter a loop that runs until the users exits. 73 # If the user hits enter, we will announce our server 74 # destination on the network, which will let clients 75 # know how to create messages directed towards it. 76 while True: 77 entered = input() 78 destination.announce() 79 RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) 80 81 # When a client establishes a link to our server 82 # destination, this function will be called with 83 # a reference to the link. 84 def client_connected(link): 85 global latest_client_link, first_packet_at, rc 86 87 RNS.log("Client connected") 88 first_packet_at = time.time() 89 rc = 0 90 link.set_link_closed_callback(client_disconnected) 91 link.set_packet_callback(server_packet_received) 92 latest_client_link = link 93 94 def client_disconnected(link): 95 RNS.log("Client disconnected") 96 97 98 # A convenience function for printing a human- 99 # readable file size 100 def size_str(num, suffix='B'): 101 units = ['','Ki','Mi','Gi','Ti','Pi','Ei','Zi'] 102 last_unit = 'Yi' 103 104 if suffix == 'b': 105 num *= 8 106 units = ['','K','M','G','T','P','E','Z'] 107 last_unit = 'Y' 108 109 for unit in units: 110 if abs(num) < 1024.0: 111 return "%3.2f %s%s" % (num, unit, suffix) 112 num /= 1024.0 113 return "%.2f %s%s" % (num, last_unit, suffix) 114 115 116 def server_packet_received(message, packet): 117 global latest_client_link, first_packet_at, last_packet_at, received_data, rc, data_cap 118 119 received_data += len(packet.data) 120 121 rc += 1 122 if rc >= 50: 123 RNS.log(size_str(received_data)) 124 rc = 0 125 126 if received_data > data_cap: 127 rcv_d = received_data 128 received_data = 0 129 rc = 0 130 131 last_packet_at = time.time() 132 133 # Print statistics 134 download_time = last_packet_at-first_packet_at 135 hours, rem = divmod(download_time, 3600) 136 minutes, seconds = divmod(rem, 60) 137 timestring = "{:0>2}:{:0>2}:{:05.2f}".format(int(hours),int(minutes),seconds) 138 139 print("") 140 print("") 141 print("--- Statistics -----") 142 print("\tTime taken : "+timestring) 143 print("\tData transferred : "+size_str(rcv_d)) 144 print("\tTransfer rate : "+size_str(rcv_d/download_time, suffix='b')+"/s") 145 print("") 146 147 sys.stdout.flush() 148 latest_client_link.teardown() 149 time.sleep(0.2) 150 rc = 0 151 received_data = 0 152 153 154 ########################################################## 155 #### Client Part ######################################### 156 ########################################################## 157 158 # A reference to the server link 159 server_link = None 160 should_quit = False 161 162 # This initialisation is executed when the users chooses 163 # to run as a client 164 def client(destination_hexhash, configpath): 165 # We need a binary representation of the destination 166 # hash that was entered on the command line 167 try: 168 dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 169 if len(destination_hexhash) != dest_len: 170 raise ValueError( 171 "Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2) 172 ) 173 174 destination_hash = bytes.fromhex(destination_hexhash) 175 except: 176 RNS.log("Invalid destination entered. Check your input!\n") 177 sys.exit(0) 178 179 # We must first initialise Reticulum 180 reticulum = RNS.Reticulum(configpath) 181 182 # Check if we know a path to the destination 183 if not RNS.Transport.has_path(destination_hash): 184 RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...") 185 RNS.Transport.request_path(destination_hash) 186 while not RNS.Transport.has_path(destination_hash): 187 time.sleep(0.1) 188 189 # Recall the server identity 190 server_identity = RNS.Identity.recall(destination_hash) 191 192 # Inform the user that we'll begin connecting 193 RNS.log("Establishing link with server...") 194 195 # When the server identity is known, we set 196 # up a destination 197 server_destination = RNS.Destination( 198 server_identity, 199 RNS.Destination.OUT, 200 RNS.Destination.SINGLE, 201 APP_NAME, 202 "speedtest" 203 ) 204 205 # And create a link 206 link = RNS.Link(server_destination) 207 208 # We'll also set up functions to inform the 209 # user when the link is established or closed 210 link.set_link_established_callback(link_established) 211 link.set_link_closed_callback(link_closed) 212 213 # Everything is set up, so let's enter a loop 214 # for the user to interact with the example 215 client_loop() 216 217 def client_loop(): 218 global server_link, should_quit 219 220 # Wait for the link to become active 221 while not server_link: 222 time.sleep(0.1) 223 224 should_quit = False 225 while not should_quit: 226 time.sleep(0.2) 227 228 # This function is called when a link 229 # has been established with the server 230 def link_established(link): 231 # We store a reference to the link 232 # instance for later use 233 global server_link, data_cap, printed 234 server_link = link 235 data_sent = 0 236 237 # Inform the user that the server is 238 # connected 239 RNS.log("Link established with server, sending...") 240 rd = os.urandom(link.mdu) 241 started = time.time() 242 while link.status == RNS.Link.ACTIVE and data_sent < data_cap*1.25: 243 RNS.Packet(server_link, rd, create_receipt=False).send() 244 data_sent += len(rd) 245 246 if data_sent > data_cap and not printed: 247 printed = True 248 ended = time.time() 249 # Print statistics 250 download_time = ended-started 251 hours, rem = divmod(download_time, 3600) 252 minutes, seconds = divmod(rem, 60) 253 timestring = "{:0>2}:{:0>2}:{:05.2f}".format(int(hours),int(minutes),seconds) 254 print("") 255 print("") 256 print("--- Statistics -----") 257 print("\tTime taken : "+timestring) 258 print("\tData transferred : "+size_str(data_sent)) 259 print("\tTransfer rate : "+size_str(data_sent/download_time, suffix='b')+"/s") 260 print("") 261 262 sys.stdout.flush() 263 time.sleep(0.1) 264 265 266 # When a link is closed, we'll inform the 267 # user, and exit the program 268 def link_closed(link): 269 global should_quit 270 if link.teardown_reason == RNS.Link.TIMEOUT: 271 RNS.log("The link timed out, exiting now") 272 elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: 273 RNS.log("The link was closed by the server, exiting now") 274 else: 275 RNS.log("Link closed, exiting now") 276 277 should_quit = True 278 time.sleep(1.5) 279 sys.exit(0) 280 281 def client_packet_received(message, packet): 282 pass 283 284 ########################################################## 285 #### Program Startup ##################################### 286 ########################################################## 287 288 # This part of the program runs at startup, 289 # and parses input of from the user, and then 290 # starts up the desired program mode. 291 if __name__ == "__main__": 292 try: 293 parser = argparse.ArgumentParser(description="Speedtest example") 294 295 parser.add_argument( 296 "-s", 297 "--server", 298 action="store_true", 299 help="wait for incoming requests from clients" 300 ) 301 302 parser.add_argument( 303 "--config", 304 action="store", 305 default=None, 306 help="path to alternative Reticulum config directory", 307 type=str 308 ) 309 310 parser.add_argument( 311 "destination", 312 nargs="?", 313 default=None, 314 help="hexadecimal hash of the server destination", 315 type=str 316 ) 317 318 args = parser.parse_args() 319 320 if args.config: 321 configarg = args.config 322 else: 323 configarg = None 324 325 if args.server: 326 server(configarg) 327 else: 328 if (args.destination == None): 329 print("") 330 parser.print_help() 331 print("") 332 else: 333 client(args.destination, configarg) 334 335 except KeyboardInterrupt: 336 print("") 337 sys.exit(0)