/ Examples / Speedtest.py
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)