diff --git a/Examples/Channel.py b/Examples/Channel.py new file mode 100644 index 0000000..dfad943 --- /dev/null +++ b/Examples/Channel.py @@ -0,0 +1,375 @@ +########################################################## +# This RNS example demonstrates how to set up a link to # +# a destination, and pass structuredmessages 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 + 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( + "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("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_callback(server_message_received) + +def client_disconnected(link): + RNS.log("Client disconnected") + +def server_message_received(message): + 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. + if isinstance(message, StringMessage): + RNS.log("Received data on the link: " + message.data + " (message created at " + str(message.timestamp) + ")") + + reply_message = StringMessage("I received \""+message.data+"\" over the link") + latest_client_link.get_channel().send(reply_message) + + +########################################################## +#### 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( + "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") + 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 set a callback that will get executed + # every time a packet is received over the + # link + link.set_packet_callback(client_message_received) + + # 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( + "Cannot send this packet, the data size of "+ + str(packed_size)+" bytes exceeds the link packet MDU of "+ + str(channel.MDU)+" bytes", + RNS.LOG_ERROR + ) + else: + RNS.log("Channel is not ready to send, please wait for " + + "pending messages to complete.", RNS.LOG_ERROR) + + except Exception as e: + RNS.log("Error while sending data over the link: "+str(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_callback(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 link, we +# simply print out the data. +def client_message_received(message): + if isinstance(message, StringMessage): + RNS.log("Received data on the link: " + message.data + " (message created at " + str(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 link 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() \ No newline at end of file