Added channel example
This commit is contained in:
		
							parent
							
								
									fe3a3e22f7
								
							
						
					
					
						commit
						a61b15cf6a
					
				
							
								
								
									
										375
									
								
								Examples/Channel.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										375
									
								
								Examples/Channel.py
									
									
									
									
									
										Normal file
									
								
							| @ -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() | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user