########################################################## # This RNS example demonstrates how to set up a link to # # a destination, and pass structured messages over it # # using a channel. # ########################################################## 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" ########################################################## #### Shared Objects ###################################### ########################################################## # Channel data must be structured in a subclass of # MessageBase. This ensures that the channel will be able # to serialize and deserialize the object and multiplex it # with other objects. Both ends of a link will need the # same object definitions to be able to communicate over # a channel. # # Note: The objects we wish to use over the channel must # be registered with the channel, and each link has a # different channel instance. See the client_connected # and link_established functions in this example to see # how message types are registered. # Let's make a simple message class called StringMessage # that will convey a string with a timestamp. class StringMessage(RNS.MessageBase): # The MSGTYPE class variable needs to be assigned a # 2 byte integer value. This identifier allows the # channel to look up your message's constructor when a # message arrives over the channel. # # MSGTYPE must be unique across all message types we # register with the channel. MSGTYPEs >= 0xf000 are # reserved for the system. MSGTYPE = 0x0101 # The constructor of our object must be callable with # no arguments. We can have parameters, but they must # have a default assignment. # # This is needed so the channel can create an empty # version of our message into which the incoming # message can be unpacked. def __init__(self, data=None): self.data = data self.timestamp = datetime.now() # Finally, our message needs to implement functions # the channel can call to pack and unpack our message # to/from the raw packet payload. We'll use the # umsgpack package bundled with RNS. We could also use # the struct package bundled with Python if we wanted # more control over the structure of the packed bytes. # # Also note that packed message objects must fit # entirely in one packet. The number of bytes # available for message payloads can be queried from # the channel using the Channel.MDU property. The # channel MDU is slightly less than the link MDU due # to encoding the message header. # The pack function encodes the message contents into # a byte stream. def pack(self) -> bytes: return umsgpack.packb((self.data, self.timestamp)) # And the unpack function decodes a byte stream into # the message contents. def unpack(self, raw): self.data, self.timestamp = umsgpack.unpackb(raw) ########################################################## #### 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, "channelexample" ) # 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( f"Link 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_client_link = link RNS.log("Client connected") link.set_link_closed_callback(client_disconnected) # Register message types and add callback to channel channel = link.get_channel() channel.register_message_type(StringMessage) channel.add_message_handler(server_message_received) def client_disconnected(link): RNS.log("Client disconnected") def server_message_received(message): """ A message handler @param message: An instance of a subclass of MessageBase @return: True if message was handled """ global latest_client_link # When a message is received over any active link, # the replies will all be directed to the last client # that connected. # In a message handler, any deserializable message # that arrives over the link's channel will be passed # to all message handlers, unless a preceding handler indicates it # has handled the message. # # if isinstance(message, StringMessage): RNS.log(f"Received data on the link: {message.data} (message created at {message.timestamp})") reply_message = StringMessage(f"I received \"{message.data}\" over the link") latest_client_link.get_channel().send(reply_message) # Incoming messages are sent to each message # handler added to the channel, in the order they # were added. # If any message handler returns True, the message # is considered handled and any subsequent # handlers are skipped. return True ########################################################## #### Client Part ######################################### ########################################################## # A reference to the server link server_link = 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, "channelexample" ) # 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() # If not, send the entered text over the link if text != "": message = StringMessage(text) packed_size = len(message.pack()) channel = server_link.get_channel() if channel.is_ready_to_send(): if packed_size <= channel.MDU: channel.send(message) else: RNS.log( f"Cannot send this packet, the data size of {packed_size} bytes exceeds the link packet MDU of {channel.MDU} bytes", RNS.LOG_ERROR ) else: RNS.log(f"Channel is not ready to send, please wait for pending messages to complete.", RNS.LOG_ERROR) except Exception as e: RNS.log(f"Error while sending data over the link: {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 server_link = link # Register messages and add handler to channel channel = link.get_channel() channel.register_message_type(StringMessage) channel.add_message_handler(client_message_received) # 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 a packet is received over the channel, we # simply print out the data. def client_message_received(message): if isinstance(message, StringMessage): RNS.log(f"Received data on the link: {message.data} (message created at {message.timestamp})") 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 channel 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()