Added channel example
This commit is contained in:
parent
fe3a3e22f7
commit
a61b15cf6a
|
@ -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…
Reference in New Issue