########################################################## # This RNS example demonstrates how to set up a link to # # a destination, and pass binary data over it using a # # channel buffer. # ########################################################## from __future__ import annotations import os import sys import time import argparse from datetime import datetime import RNS from RNS.vendor import umsgpack # 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 # A reference to the latest buffer object latest_buffer = 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 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, "bufferexample" ) # 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 requests or user input server_loop(server_destination) def server_loop(destination): # Let the user know that everything is ready RNS.log( "Link buffer 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(f"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, latest_buffer latest_client_link = link RNS.log("Client connected") link.set_link_closed_callback(client_disconnected) # If a new connection is received, the old reader # needs to be disconnected. if latest_buffer: latest_buffer.close() # Create buffer objects. # The stream_id parameter to these functions is # a bit like a file descriptor, except that it # is unique to the *receiver*. # # In this example, both the reader and the writer # use stream_id = 0, but there are actually two # separate unidirectional streams flowing in # opposite directions. # channel = link.get_channel() latest_buffer = RNS.Buffer.create_bidirectional_buffer(0, 0, channel, server_buffer_ready) def client_disconnected(link): RNS.log("Client disconnected") def server_buffer_ready(ready_bytes: int): """ Callback from buffer when buffer has data available :param ready_bytes: The number of bytes ready to read """ global latest_buffer data = latest_buffer.read(ready_bytes) data = data.decode("utf-8") RNS.log(f"Received data over the buffer: {data}") reply_message = f"I received \"{data}\" over the buffer" reply_message = reply_message.encode("utf-8") latest_buffer.write(reply_message) latest_buffer.flush() ########################################################## #### Client Part ######################################### ########################################################## # A reference to the server link server_link = None # A reference to the buffer object, needed to share the # object from the link connected callback to the client # loop. buffer = None # 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( f"Destination length is invalid, must be {dest_len} hexadecimal characters ({dest_len // 2} bytes)." ) destination_hash = bytes.fromhex(destination_hexhash) except: RNS.log("Invalid destination entered. Check your input!\n") exit() # 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, "bufferexample" ) # And create a link link = RNS.Link(server_destination) # We'll also 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: # Otherwise, encode the text and write it to the buffer. text = text.encode("utf-8") buffer.write(text) # Flush the buffer to force the data to be sent. buffer.flush() except Exception as e: RNS.log(f"Error while sending data over the link buffer: {e}") should_quit = True server_link.teardown() # 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, buffer server_link = link # Create buffer, see server_client_connected() for # more detail about setting up the buffer. channel = link.get_channel() buffer = RNS.Buffer.create_bidirectional_buffer(0, 0, channel, client_buffer_ready) # Inform the user that the server is # connected RNS.log("Link established with server, enter some text to send, or \"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") RNS.Reticulum.exit_handler() time.sleep(1.5) os._exit(0) # When the buffer has new data, read it and write it to the terminal. def client_buffer_ready(ready_bytes: int): global buffer data = buffer.read(ready_bytes) RNS.log(f"Received data over the link buffer: {data.decode('utf-8')}") print("> ", end=" ") sys.stdout.flush() ########################################################## #### 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 buffer example") parser.add_argument( "-s", "--server", action="store_true", help="wait for incoming link requests 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("") exit()