From 5fb6abd01937cc9f0c41dbdfd3157b2f37945add Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Sat, 10 May 2025 15:32:06 +0200 Subject: [PATCH] Added resource example --- Examples/Request.py | 4 +- Examples/Resource.py | 289 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 Examples/Resource.py diff --git a/Examples/Request.py b/Examples/Request.py index 7a56d72..8011666 100644 --- a/Examples/Request.py +++ b/Examples/Request.py @@ -1,6 +1,6 @@ ########################################################## -# This RNS example demonstrates how to set perform # -# requests and receive responses over a link. # +# This RNS example demonstrates how to perform requests # +# and receive responses over a link. # ########################################################## import os diff --git a/Examples/Resource.py b/Examples/Resource.py new file mode 100644 index 0000000..2205d42 --- /dev/null +++ b/Examples/Resource.py @@ -0,0 +1,289 @@ +########################################################## +# This RNS example demonstrates how to transfer a # +# resource over an established link # +########################################################## + +import os +import sys +import time +import random +import argparse +import RNS + +# Let's define an app name. We'll use this for all +# destinations we create. Since this echo example +# is part of a range of example utilities, we'll put +# them all within the app namespace "example_utilities" +APP_NAME = "example_utilities" + +########################################################## +#### Server Part ######################################### +########################################################## + +# A reference to the latest client link that connected +latest_client_link = None + +# This initialisation is executed when the users chooses +# to run as a server +def server(configpath): + # We must first initialise Reticulum + reticulum = RNS.Reticulum(configpath) + + # Randomly create a new identity for our link example + server_identity = RNS.Identity() + + # We create a destination that clients can connect to. We + # want clients to create links to this destination, so we + # need to create a "single" destination type. + server_destination = RNS.Destination( + server_identity, + RNS.Destination.IN, + RNS.Destination.SINGLE, + APP_NAME, + "resourceexample" + ) + + # We configure a function that will get called every time + # a new client creates a link to this destination. + server_destination.set_link_established_callback(client_connected) + + # Everything's ready! + # Let's Wait for client resources or user input + server_loop(server_destination) + +def server_loop(destination): + # Let the user know that everything is ready + RNS.log( + "Resource example "+ + RNS.prettyhexrep(destination.hash)+ + " running, waiting for a connection." + ) + + RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)") + + # We enter a loop that runs until the users exits. + # If the user hits enter, we will announce our server + # destination on the network, which will let clients + # know how to create messages directed towards it. + while True: + entered = input() + destination.announce() + RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) + +# When a client establishes a link to our server +# destination, this function will be called with +# a reference to the link. +def client_connected(link): + global latest_client_link + RNS.log("Client connected") + + # We configure the link to accept all resources + # and set a callback for completed resources + link.set_resource_strategy(RNS.Link.ACCEPT_ALL) + link.set_resource_concluded_callback(resource_concluded) + + link.set_link_closed_callback(client_disconnected) + latest_client_link = link + +def client_disconnected(link): + RNS.log("Client disconnected") + +def resource_concluded(resource): + if resource.status == RNS.Resource.COMPLETE: + RNS.log(f"Resource {resource} received") + RNS.log(f"Metadata: {resource.metadata}") + RNS.log(f"Data can be read from: {resource.data}") + else: + RNS.log(f"Receiving resource {resource} failed") + + + +########################################################## +#### Client Part ######################################### +########################################################## + +# A reference to the server link +server_link = None + +def random_text_generator(): + texts = ["They looked up", "On each full moon", "Becky was upset", "I’ll stay away from it", "The pet shop stocks everything"] + return texts[random.randint(0, len(texts)-1)] + +# This initialisation is executed when the users chooses +# to run as a client +def client(destination_hexhash, configpath): + # We need a binary representation of the destination + # hash that was entered on the command line + try: + dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 + if len(destination_hexhash) != dest_len: + raise ValueError( + "Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2) + ) + + destination_hash = bytes.fromhex(destination_hexhash) + except: + RNS.log("Invalid destination entered. Check your input!\n") + sys.exit(0) + + # We must first initialise Reticulum + reticulum = RNS.Reticulum(configpath) + + # Check if we know a path to the destination + if not RNS.Transport.has_path(destination_hash): + RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...") + RNS.Transport.request_path(destination_hash) + while not RNS.Transport.has_path(destination_hash): + time.sleep(0.1) + + # Recall the server identity + server_identity = RNS.Identity.recall(destination_hash) + + # Inform the user that we'll begin connecting + RNS.log("Establishing link with server...") + + # When the server identity is known, we set + # up a destination + server_destination = RNS.Destination( + server_identity, + RNS.Destination.OUT, + RNS.Destination.SINGLE, + APP_NAME, + "resourceexample" + ) + + # And create a link + link = RNS.Link(server_destination) + + # We'll set up functions to inform the + # user when the link is established or closed + link.set_link_established_callback(link_established) + link.set_link_closed_callback(link_closed) + + # Everything is set up, so let's enter a loop + # for the user to interact with the example + client_loop() + +def client_loop(): + global server_link + + # Wait for the link to become active + while not server_link: + time.sleep(0.1) + + should_quit = False + while not should_quit: + try: + print("> ", end=" ") + text = input() + + # Check if we should quit the example + if text == "quit" or text == "q" or text == "exit": + should_quit = True + server_link.teardown() + + else: + # Generate 32 megabytes of random data + data = os.urandom(32*1024*1024) + + # Generate some metadata + metadata = {"text": random_text_generator(), "numbers": [1,2,3,4], "blob": os.urandom(16)} + + # Send the resource + resource = RNS.Resource(data, server_link, metadata=metadata, callback=resource_concluded_sending, auto_compress=False) + + # Alternatively, you can stream data + # directly from an open file descriptor + + # with open("/path/to/file", "rb") as data_file: + # resource = RNS.Resource(data_file, server_link, metadata=metadata, callback=resource_concluded_sending, auto_compress=False) + + except Exception as e: + RNS.log("Error while sending resource over the link: "+str(e)) + should_quit = True + server_link.teardown() + +def resource_concluded_sending(resource): + if resource.status == RNS.Resource.COMPLETE: RNS.log(f"The resource {resource} was sent successfully") + else: RNS.log(f"Sending the resource {resource} failed") + +# This function is called when a link +# has been established with the server +def link_established(link): + # We store a reference to the link + # instance for later use + global server_link + server_link = link + + # Inform the user that the server is + # connected + RNS.log("Link established with server, hit enter to sand a resource, or type in \"quit\" to quit") + +# When a link is closed, we'll inform the +# user, and exit the program +def link_closed(link): + if link.teardown_reason == RNS.Link.TIMEOUT: + RNS.log("The link timed out, exiting now") + elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: + RNS.log("The link was closed by the server, exiting now") + else: + RNS.log("Link closed, exiting now") + + time.sleep(1.5) + sys.exit(0) + + +########################################################## +#### Program Startup ##################################### +########################################################## + +# This part of the program runs at startup, +# and parses input of from the user, and then +# starts up the desired program mode. +if __name__ == "__main__": + try: + parser = argparse.ArgumentParser(description="Simple resource example") + + parser.add_argument( + "-s", + "--server", + action="store_true", + help="wait for incoming resources from clients" + ) + + parser.add_argument( + "--config", + action="store", + default=None, + help="path to alternative Reticulum config directory", + type=str + ) + + parser.add_argument( + "destination", + nargs="?", + default=None, + help="hexadecimal hash of the server destination", + type=str + ) + + args = parser.parse_args() + + if args.config: + configarg = args.config + else: + configarg = None + + if args.server: + server(configarg) + else: + if (args.destination == None): + print("") + parser.print_help() + print("") + else: + client(args.destination, configarg) + + except KeyboardInterrupt: + print("") + sys.exit(0) \ No newline at end of file