From 890846fa8d4f36ffa73cdf5cb50e2790dc3eed93 Mon Sep 17 00:00:00 2001
From: Mark Qvist
Date: Fri, 22 Nov 2024 14:16:53 +0100
Subject: [PATCH] Added custom interfaces to documentation and readme
---
README.md | 5 +-
docs/manual/_sources/examples.rst.txt | 16 +-
docs/manual/_sources/interfaces.rst.txt | 137 ++++++-----
docs/manual/examples.html | 310 ++++++++++++++++++++++++
docs/manual/index.html | 1 +
docs/manual/interfaces.html | 136 ++++++-----
docs/manual/objects.inv | Bin 2638 -> 2652 bytes
docs/manual/searchindex.js | 2 +-
docs/source/examples.rst | 16 +-
docs/source/interfaces.rst | 5 +
10 files changed, 492 insertions(+), 136 deletions(-)
diff --git a/README.md b/README.md
index 0c55bcc..4c8a7be 100755
--- a/README.md
+++ b/README.md
@@ -53,7 +53,10 @@ For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ
- HMAC using SHA256 for authentication
- IVs are generated through os.urandom()
- Unforgeable packet delivery confirmations
-- A large variety of supported interface types
+- Flexible and extensible interface system
+ - Includes a large variety of built-in interface types
+ - Ability to load and utilise custom user- or community-supplied interface types
+ - Easily create your own custom interfaces for communicating over anything
- An intuitive and easy-to-use API
- Reliable and efficient transfer of arbitrary amounts of data
- Reticulum can handle a few bytes of data or files of many gigabytes
diff --git a/docs/manual/_sources/examples.rst.txt b/docs/manual/_sources/examples.rst.txt
index 739e9d2..b0887eb 100644
--- a/docs/manual/_sources/examples.rst.txt
+++ b/docs/manual/_sources/examples.rst.txt
@@ -125,4 +125,18 @@ interface to efficiently pass files of any size over a Reticulum :ref:`Link
The ExampleInterface demonstrates creating custom interfaces for Reticulum. +Any number of custom interfaces can be loaded and utilised by Reticulum, and +will be fully on-par with natively included interfaces, including all supported +interface modes and common configuration options.
+# MIT License - Copyright (c) 2024 Mark Qvist / unsigned.io
+
+# This example illustrates creating a custom interface
+# definition, that can be loaded and used by Reticulum at
+# runtime. Any number of custom interfaces can be created
+# and loaded. To use the interface place it in the folder
+# ~/.reticulum/interfaces, and add an interface entry to
+# your Reticulum configuration file similar to this:
+
+# [[Example Custom Interface]]
+# type = ExampleInterface
+# enabled = no
+# mode = gateway
+# port = /dev/ttyUSB0
+# speed = 115200
+# databits = 8
+# parity = none
+# stopbits = 1
+
+from time import sleep
+import sys
+import threading
+import time
+
+# This HDLC helper class is used by the interface
+# to delimit and packetize data over the physical
+# medium - in this case a serial connection.
+class HDLC():
+ # This example interface packetizes data using
+ # simplified HDLC framing, similar to PPP
+ FLAG = 0x7E
+ ESC = 0x7D
+ ESC_MASK = 0x20
+
+ @staticmethod
+ def escape(data):
+ data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK]))
+ data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
+ return data
+
+# Let's define our custom interface class. It must
+# be a sub-class of the RNS "Interface" class.
+class ExampleInterface(Interface):
+ # All interface classes must define a default
+ # IFAC size, used in IFAC setup when the user
+ # has not specified a custom IFAC size. This
+ # option is specified in bytes.
+ DEFAULT_IFAC_SIZE = 8
+
+ # The following properties are local to this
+ # particular interface implementation.
+ owner = None
+ port = None
+ speed = None
+ databits = None
+ parity = None
+ stopbits = None
+ serial = None
+
+ # All Reticulum interfaces must have an __init__
+ # method that takes 2 positional arguments:
+ # The owner RNS Transport instance, and a dict
+ # of configuration values.
+ def __init__(self, owner, configuration):
+
+ # The following lines demonstrate handling
+ # potential dependencies required for the
+ # interface to function correctly.
+ import importlib
+ if importlib.util.find_spec('serial') != None:
+ import serial
+ else:
+ RNS.log("Using this interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
+ RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
+ RNS.panic()
+
+ # We start out by initialising the super-class
+ super().__init__()
+
+ # To make sure the configuration data is in the
+ # correct format, we parse it through the following
+ # method on the generic Interface class. This step
+ # is required to ensure compatibility on all the
+ # platforms that Reticulum supports.
+ ifconf = Interface.get_config_obj(configuration)
+
+ # Read the interface name from the configuration
+ # and set it on our interface instance.
+ name = ifconf["name"]
+ self.name = name
+
+ # We read configuration parameters from the supplied
+ # configuration data, and provide default values in
+ # case any are missing.
+ port = ifconf["port"] if "port" in ifconf else None
+ speed = int(ifconf["speed"]) if "speed" in ifconf else 9600
+ databits = int(ifconf["databits"]) if "databits" in ifconf else 8
+ parity = ifconf["parity"] if "parity" in ifconf else "N"
+ stopbits = int(ifconf["stopbits"]) if "stopbits" in ifconf else 1
+
+ # In case no port is specified, we abort setup by
+ # raising an exception.
+ if port == None:
+ raise ValueError(f"No port specified for {self}")
+
+ # All interfaces must supply a hardware MTU value
+ # to the RNS Transport instance. This value should
+ # be the maximum data packet payload size that the
+ # underlying medium is capable of handling in all
+ # cases without any segmentation.
+ self.HW_MTU = 564
+
+ # We initially set the "online" property to false,
+ # since the interface has not actually been fully
+ # initialised and connected yet.
+ self.online = False
+
+ # In this case, we can also set the indicated bit-
+ # rate of the interface to the serial port speed.
+ self.bitrate = self.speed
+
+ # Configure internal properties on the interface
+ # according to the supplied configuration.
+ self.pyserial = serial
+ self.serial = None
+ self.owner = owner
+ self.port = port
+ self.speed = speed
+ self.databits = databits
+ self.parity = serial.PARITY_NONE
+ self.stopbits = stopbits
+ self.timeout = 100
+
+ if parity.lower() == "e" or parity.lower() == "even":
+ self.parity = serial.PARITY_EVEN
+
+ if parity.lower() == "o" or parity.lower() == "odd":
+ self.parity = serial.PARITY_ODD
+
+ # Since all required parameters are now configured,
+ # we will try opening the serial port.
+ try:
+ self.open_port()
+ except Exception as e:
+ RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
+ raise e
+
+ # If opening the port succeeded, run any post-open
+ # configuration required.
+ if self.serial.is_open:
+ self.configure_device()
+ else:
+ raise IOError("Could not open serial port")
+
+ # Open the serial port with supplied configuration
+ # parameters and store a reference to the open port.
+ def open_port(self):
+ RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE)
+ self.serial = self.pyserial.Serial(
+ port = self.port,
+ baudrate = self.speed,
+ bytesize = self.databits,
+ parity = self.parity,
+ stopbits = self.stopbits,
+ xonxoff = False,
+ rtscts = False,
+ timeout = 0,
+ inter_byte_timeout = None,
+ write_timeout = None,
+ dsrdtr = False,
+ )
+
+ # The only thing required after opening the port
+ # is to wait a small amount of time for the
+ # hardware to initialise and then start a thread
+ # that reads any incoming data from the device.
+ def configure_device(self):
+ sleep(0.5)
+ thread = threading.Thread(target=self.read_loop)
+ thread.daemon = True
+ thread.start()
+ self.online = True
+ RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
+
+
+ # This method will be called from our read-loop
+ # whenever a full packet has been received over
+ # the underlying medium.
+ def process_incoming(self, data):
+ # Update our received bytes counter
+ self.rxb += len(data)
+
+ # And send the data packet to the Transport
+ # instance for processing.
+ self.owner.inbound(data, self)
+
+ # The running Reticulum Transport instance will
+ # call this method on the interface whenever the
+ # interface must transmit a packet.
+ def process_outgoing(self,data):
+ if self.online:
+ # First, escape and packetize the data
+ # according to HDLC framing.
+ data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
+
+ # Then write the framed data to the port
+ written = self.serial.write(data)
+
+ # Update the transmitted bytes counter
+ # and ensure that all data was written
+ self.txb += len(data)
+ if written != len(data):
+ raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
+
+ # This read loop runs in a thread and continously
+ # receives bytes from the underlying serial port.
+ # When a full packet has been received, it will
+ # be sent to the process_incoming methed, which
+ # will in turn pass it to the Transport instance.
+ def read_loop(self):
+ try:
+ in_frame = False
+ escape = False
+ data_buffer = b""
+ last_read_ms = int(time.time()*1000)
+
+ while self.serial.is_open:
+ if self.serial.in_waiting:
+ byte = ord(self.serial.read(1))
+ last_read_ms = int(time.time()*1000)
+
+ if (in_frame and byte == HDLC.FLAG):
+ in_frame = False
+ self.process_incoming(data_buffer)
+ elif (byte == HDLC.FLAG):
+ in_frame = True
+ data_buffer = b""
+ elif (in_frame and len(data_buffer) < self.HW_MTU):
+ if (byte == HDLC.ESC):
+ escape = True
+ else:
+ if (escape):
+ if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
+ byte = HDLC.FLAG
+ if (byte == HDLC.ESC ^ HDLC.ESC_MASK):
+ byte = HDLC.ESC
+ escape = False
+ data_buffer = data_buffer+bytes([byte])
+
+ else:
+ time_since_last = int(time.time()*1000) - last_read_ms
+ if len(data_buffer) > 0 and time_since_last > self.timeout:
+ data_buffer = b""
+ in_frame = False
+ escape = False
+ sleep(0.08)
+
+ except Exception as e:
+ self.online = False
+ RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
+ RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
+
+ if RNS.Reticulum.panic_on_interface_error:
+ RNS.panic()
+
+ RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
+
+ self.online = False
+ self.serial.close()
+ self.reconnect_port()
+
+ # This method handles serial port disconnects.
+ def reconnect_port(self):
+ while not self.online:
+ try:
+ time.sleep(5)
+ RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
+ self.open_port()
+ if self.serial.is_open:
+ self.configure_device()
+ except Exception as e:
+ RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
+
+ RNS.log("Reconnected serial port for "+str(self))
+
+ # Signal to Reticulum that this interface should
+ # not perform any ingress limiting.
+ def should_ingress_limit(self):
+ return False
+
+ # We must provide a string representation of this
+ # interface, that is used whenever the interface
+ # is printed in logs or external programs.
+ def __str__(self):
+ return "ExampleInterface["+self.name+"]"
+
+# Finally, register the defined interface class as the
+# target class for Reticulum to use as an interface
+interface_class = ExampleInterface
+
This example can also be found at https://github.com/markqvist/Reticulum/blob/master/Examples/ExampleInterface.py.
+The following sections describe the interfaces currently available in Reticulum, and gives example configurations for the respective interface types.
+In addition to the built-in interface types, Reticulum is fully extensible with +custom, user- or community-supplied interfaces, and creating custom interface +modules is straightforward. Please see the custom interface +example for basic interface code to build upon.
For a high-level overview of how networks can be formed over different interface types, have a look at the Building Networks chapter of this manual.
@@ -644,89 +648,89 @@ Multi interface can be used to configure sub-interfaces individually. # id_interval = 600 # A subinterface - [[[HIGHDATARATE]]] - # Subinterfaces can be enabled and disabled in of themselves - interface_enabled = True + [[[High Datarate]]] + # Subinterfaces can be enabled and disabled in of themselves + interface_enabled = True - # Set frequency to 2.4GHz - frequency = 2400000000 + # Set frequency to 2.4GHz + frequency = 2400000000 - # Set LoRa bandwidth to 1625 KHz - bandwidth = 1625000 + # Set LoRa bandwidth to 1625 KHz + bandwidth = 1625000 - # Set TX power to 0 dBm (0.12 mW) - txpower = 0 + # Set TX power to 0 dBm (0.12 mW) + txpower = 0 - # The virtual port, only the manufacturer - # or the person who wrote the board config - # can tell you what it will be for which - # physical hardware interface - vport = 1 + # The virtual port, only the manufacturer + # or the person who wrote the board config + # can tell you what it will be for which + # physical hardware interface + vport = 1 - # Select spreading factor 5. Valid - # range is 5 through 12, with 5 - # being the fastest and 12 having - # the longest range. - spreadingfactor = 5 + # Select spreading factor 5. Valid + # range is 5 through 12, with 5 + # being the fastest and 12 having + # the longest range. + spreadingfactor = 5 - # Select coding rate 5. Valid range - # is 5 throough 8, with 5 being the - # fastest, and 8 the longest range. - codingrate = 5 + # Select coding rate 5. Valid range + # is 5 throough 8, with 5 being the + # fastest, and 8 the longest range. + codingrate = 5 - # It is possible to limit the airtime - # utilisation of an RNode by using the - # following two configuration options. - # The short-term limit is applied in a - # window of approximately 15 seconds, - # and the long-term limit is enforced - # over a rolling 60 minute window. Both - # options are specified in percent. + # It is possible to limit the airtime + # utilisation of an RNode by using the + # following two configuration options. + # The short-term limit is applied in a + # window of approximately 15 seconds, + # and the long-term limit is enforced + # over a rolling 60 minute window. Both + # options are specified in percent. - # airtime_limit_long = 100 - # airtime_limit_short = 100 + # airtime_limit_long = 100 + # airtime_limit_short = 100 - [[[LOWDATARATE]]] - # Subinterfaces can be enabled and disabled in of themselves - interface_enabled = True + [[[Low Datarate]]] + # Subinterfaces can be enabled and disabled in of themselves + interface_enabled = True - # Set frequency to 865.6 MHz - frequency = 865600000 + # Set frequency to 865.6 MHz + frequency = 865600000 - # The virtual port, only the manufacturer - # or the person who wrote the board config - # can tell you what it will be for which - # physical hardware interface - vport = 0 + # The virtual port, only the manufacturer + # or the person who wrote the board config + # can tell you what it will be for which + # physical hardware interface + vport = 0 - # Set LoRa bandwidth to 125 KHz - bandwidth = 125000 + # Set LoRa bandwidth to 125 KHz + bandwidth = 125000 - # Set TX power to 0 dBm (0.12 mW) - txpower = 0 + # Set TX power to 0 dBm (0.12 mW) + txpower = 0 - # Select spreading factor 7. Valid - # range is 5 through 12, with 5 - # being the fastest and 12 having - # the longest range. - spreadingfactor = 7 + # Select spreading factor 7. Valid + # range is 5 through 12, with 5 + # being the fastest and 12 having + # the longest range. + spreadingfactor = 7 - # Select coding rate 5. Valid range - # is 5 throough 8, with 5 being the - # fastest, and 8 the longest range. - codingrate = 5 + # Select coding rate 5. Valid range + # is 5 throough 8, with 5 being the + # fastest, and 8 the longest range. + codingrate = 5 - # It is possible to limit the airtime - # utilisation of an RNode by using the - # following two configuration options. - # The short-term limit is applied in a - # window of approximately 15 seconds, - # and the long-term limit is enforced - # over a rolling 60 minute window. Both - # options are specified in percent. + # It is possible to limit the airtime + # utilisation of an RNode by using the + # following two configuration options. + # The short-term limit is applied in a + # window of approximately 15 seconds, + # and the long-term limit is enforced + # over a rolling 60 minute window. Both + # options are specified in percent. - # airtime_limit_long = 100 - # airtime_limit_short = 100 + # airtime_limit_long = 100 + # airtime_limit_short = 100 diff --git a/docs/manual/objects.inv b/docs/manual/objects.inv index 488f955d007d48eaa2ee1a82dfc6aff49db3bee3..7c42f3150e86db9a7867b2c847f985fad9184ee4 100644 GIT binary patch delta 2517 zcmV;`2`cu^6x