/ Examples / Channel.py
Channel.py
  1  ##########################################################
  2  # This RNS example demonstrates how to set up a link to  #
  3  # a destination, and pass structured messages over it    #
  4  # using a channel.                                       #
  5  ##########################################################
  6  
  7  import os
  8  import sys
  9  import time
 10  import argparse
 11  from datetime import datetime
 12  
 13  import RNS
 14  from RNS.vendor import umsgpack
 15  
 16  # Let's define an app name. We'll use this for all
 17  # destinations we create. Since this echo example
 18  # is part of a range of example utilities, we'll put
 19  # them all within the app namespace "example_utilities"
 20  APP_NAME = "example_utilities"
 21  
 22  ##########################################################
 23  #### Shared Objects ######################################
 24  ##########################################################
 25  
 26  # Channel data must be structured in a subclass of
 27  # MessageBase. This ensures that the channel will be able
 28  # to serialize and deserialize the object and multiplex it
 29  # with other objects. Both ends of a link will need the
 30  # same object definitions to be able to communicate over
 31  # a channel.
 32  #
 33  # Note: The objects we wish to use over the channel must
 34  # be registered with the channel, and each link has a
 35  # different channel instance. See the client_connected
 36  # and link_established functions in this example to see
 37  # how message types are registered.
 38  
 39  # Let's make a simple message class called StringMessage
 40  # that will convey a string with a timestamp.
 41  
 42  class StringMessage(RNS.MessageBase):
 43      # The MSGTYPE class variable needs to be assigned a
 44      # 2 byte integer value. This identifier allows the
 45      # channel to look up your message's constructor when a
 46      # message arrives over the channel.
 47      #
 48      # MSGTYPE must be unique across all message types we
 49      # register with the channel. MSGTYPEs >= 0xf000 are
 50      # reserved for the system.
 51      MSGTYPE = 0x0101
 52  
 53      # The constructor of our object must be callable with
 54      # no arguments. We can have parameters, but they must
 55      # have a default assignment.
 56      #
 57      # This is needed so the channel can create an empty
 58      # version of our message into which the incoming
 59      # message can be unpacked.
 60      def __init__(self, data=None):
 61          self.data = data
 62          self.timestamp = datetime.now()
 63  
 64      # Finally, our message needs to implement functions
 65      # the channel can call to pack and unpack our message
 66      # to/from the raw packet payload. We'll use the
 67      # umsgpack package bundled with RNS. We could also use
 68      # the struct package bundled with Python if we wanted
 69      # more control over the structure of the packed bytes.
 70      #
 71      # Also note that packed message objects must fit
 72      # entirely in one packet. The number of bytes
 73      # available for message payloads can be queried from
 74      # the channel using the Channel.MDU property. The
 75      # channel MDU is slightly less than the link MDU due
 76      # to encoding the message header.
 77  
 78      # The pack function encodes the message contents into
 79      # a byte stream.
 80      def pack(self) -> bytes:
 81          return umsgpack.packb((self.data, self.timestamp))
 82  
 83      # And the unpack function decodes a byte stream into
 84      # the message contents.
 85      def unpack(self, raw):
 86          self.data, self.timestamp = umsgpack.unpackb(raw)
 87  
 88  
 89  ##########################################################
 90  #### Server Part #########################################
 91  ##########################################################
 92  
 93  # A reference to the latest client link that connected
 94  latest_client_link = None
 95  
 96  # This initialisation is executed when the users chooses
 97  # to run as a server
 98  def server(configpath):
 99      # We must first initialise Reticulum
100      reticulum = RNS.Reticulum(configpath)
101      
102      # Randomly create a new identity for our link example
103      server_identity = RNS.Identity()
104  
105      # We create a destination that clients can connect to. We
106      # want clients to create links to this destination, so we
107      # need to create a "single" destination type.
108      server_destination = RNS.Destination(
109          server_identity,
110          RNS.Destination.IN,
111          RNS.Destination.SINGLE,
112          APP_NAME,
113          "channelexample"
114      )
115  
116      # We configure a function that will get called every time
117      # a new client creates a link to this destination.
118      server_destination.set_link_established_callback(client_connected)
119  
120      # Everything's ready!
121      # Let's Wait for client requests or user input
122      server_loop(server_destination)
123  
124  def server_loop(destination):
125      # Let the user know that everything is ready
126      RNS.log(
127          "Channel example "+
128          RNS.prettyhexrep(destination.hash)+
129          " running, waiting for a connection."
130      )
131  
132      RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)")
133  
134      # We enter a loop that runs until the users exits.
135      # If the user hits enter, we will announce our server
136      # destination on the network, which will let clients
137      # know how to create messages directed towards it.
138      while True:
139          entered = input()
140          destination.announce()
141          RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash))
142  
143  # When a client establishes a link to our server
144  # destination, this function will be called with
145  # a reference to the link.
146  def client_connected(link):
147      global latest_client_link
148      latest_client_link = link
149  
150      RNS.log("Client connected")
151      link.set_link_closed_callback(client_disconnected)
152  
153      # Register message types and add callback to channel
154      channel = link.get_channel()
155      channel.register_message_type(StringMessage)
156      channel.add_message_handler(server_message_received)
157  
158  def client_disconnected(link):
159      RNS.log("Client disconnected")
160  
161  def server_message_received(message):
162      """
163      A message handler
164      @param message: An instance of a subclass of MessageBase
165      @return: True if message was handled
166      """
167      global latest_client_link
168      # When a message is received over any active link,
169      # the replies will all be directed to the last client
170      # that connected.
171  
172      # In a message handler, any deserializable message
173      # that arrives over the link's channel will be passed
174      # to all message handlers, unless a preceding handler indicates it
175      # has handled the message.
176      #
177      #
178      if isinstance(message, StringMessage):
179          RNS.log("Received data on the link: " + message.data + " (message created at " + str(message.timestamp) + ")")
180  
181          reply_message = StringMessage("I received \""+message.data+"\" over the link")
182          latest_client_link.get_channel().send(reply_message)
183  
184          # Incoming messages are sent to each message
185          # handler added to the channel, in the order they
186          # were added.
187          # If any message handler returns True, the message
188          # is considered handled and any subsequent
189          # handlers are skipped.
190          return True
191  
192  
193  ##########################################################
194  #### Client Part #########################################
195  ##########################################################
196  
197  # A reference to the server link
198  server_link = None
199  
200  # This initialisation is executed when the users chooses
201  # to run as a client
202  def client(destination_hexhash, configpath):
203      # We need a binary representation of the destination
204      # hash that was entered on the command line
205      try:
206          dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2
207          if len(destination_hexhash) != dest_len:
208              raise ValueError(
209                  "Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2)
210              )
211              
212          destination_hash = bytes.fromhex(destination_hexhash)
213      except:
214          RNS.log("Invalid destination entered. Check your input!\n")
215          sys.exit(0)
216  
217      # We must first initialise Reticulum
218      reticulum = RNS.Reticulum(configpath)
219  
220      # Check if we know a path to the destination
221      if not RNS.Transport.has_path(destination_hash):
222          RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...")
223          RNS.Transport.request_path(destination_hash)
224          while not RNS.Transport.has_path(destination_hash):
225              time.sleep(0.1)
226  
227      # Recall the server identity
228      server_identity = RNS.Identity.recall(destination_hash)
229  
230      # Inform the user that we'll begin connecting
231      RNS.log("Establishing link with server...")
232  
233      # When the server identity is known, we set
234      # up a destination
235      server_destination = RNS.Destination(
236          server_identity,
237          RNS.Destination.OUT,
238          RNS.Destination.SINGLE,
239          APP_NAME,
240          "channelexample"
241      )
242  
243      # And create a link
244      link = RNS.Link(server_destination)
245  
246      # We'll also set up functions to inform the
247      # user when the link is established or closed
248      link.set_link_established_callback(link_established)
249      link.set_link_closed_callback(link_closed)
250  
251      # Everything is set up, so let's enter a loop
252      # for the user to interact with the example
253      client_loop()
254  
255  def client_loop():
256      global server_link
257  
258      # Wait for the link to become active
259      while not server_link:
260          time.sleep(0.1)
261  
262      should_quit = False
263      while not should_quit:
264          try:
265              print("> ", end=" ")
266              text = input()
267  
268              # Check if we should quit the example
269              if text == "quit" or text == "q" or text == "exit":
270                  should_quit = True
271                  server_link.teardown()
272  
273              # If not, send the entered text over the link
274              if text != "":
275                  message = StringMessage(text)
276                  packed_size = len(message.pack())
277                  channel = server_link.get_channel()
278                  if channel.is_ready_to_send():
279                      if packed_size <= channel.mdu:
280                          channel.send(message)
281                      else:
282                          RNS.log(
283                              "Cannot send this packet, the data size of "+
284                              str(packed_size)+" bytes exceeds the link packet MDU of "+
285                              str(channel.MDU)+" bytes",
286                              RNS.LOG_ERROR
287                          )
288                  else:
289                      RNS.log("Channel is not ready to send, please wait for " +
290                              "pending messages to complete.", RNS.LOG_ERROR)
291  
292          except Exception as e:
293              RNS.log("Error while sending data over the link: "+str(e))
294              should_quit = True
295              server_link.teardown()
296  
297  # This function is called when a link
298  # has been established with the server
299  def link_established(link):
300      # We store a reference to the link
301      # instance for later use
302      global server_link
303      server_link = link
304  
305      # Register messages and add handler to channel
306      channel = link.get_channel()
307      channel.register_message_type(StringMessage)
308      channel.add_message_handler(client_message_received)
309  
310      # Inform the user that the server is
311      # connected
312      RNS.log("Link established with server, enter some text to send, or \"quit\" to quit")
313  
314  # When a link is closed, we'll inform the
315  # user, and exit the program
316  def link_closed(link):
317      if link.teardown_reason == RNS.Link.TIMEOUT:
318          RNS.log("The link timed out, exiting now")
319      elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED:
320          RNS.log("The link was closed by the server, exiting now")
321      else:
322          RNS.log("Link closed, exiting now")
323      
324      time.sleep(1.5)
325      sys.exit(0)
326  
327  # When a packet is received over the channel, we
328  # simply print out the data.
329  def client_message_received(message):
330      if isinstance(message, StringMessage):
331          RNS.log("Received data on the link: " + message.data + " (message created at " + str(message.timestamp) + ")")
332          print("> ", end=" ")
333          sys.stdout.flush()
334  
335  
336  ##########################################################
337  #### Program Startup #####################################
338  ##########################################################
339  
340  # This part of the program runs at startup,
341  # and parses input of from the user, and then
342  # starts up the desired program mode.
343  if __name__ == "__main__":
344      try:
345          parser = argparse.ArgumentParser(description="Simple channel example")
346  
347          parser.add_argument(
348              "-s",
349              "--server",
350              action="store_true",
351              help="wait for incoming link requests from clients"
352          )
353  
354          parser.add_argument(
355              "--config",
356              action="store",
357              default=None,
358              help="path to alternative Reticulum config directory",
359              type=str
360          )
361  
362          parser.add_argument(
363              "destination",
364              nargs="?",
365              default=None,
366              help="hexadecimal hash of the server destination",
367              type=str
368          )
369  
370          args = parser.parse_args()
371  
372          if args.config:
373              configarg = args.config
374          else:
375              configarg = None
376  
377          if args.server:
378              server(configarg)
379          else:
380              if (args.destination == None):
381                  print("")
382                  parser.print_help()
383                  print("")
384              else:
385                  client(args.destination, configarg)
386  
387      except KeyboardInterrupt:
388          print("")
389          sys.exit(0)