/ Examples / ExampleInterface.py
ExampleInterface.py
  1  # This example illustrates creating a custom interface
  2  # definition, that can be loaded and used by Reticulum at
  3  # runtime. Any number of custom interfaces can be created
  4  # and loaded. To use the interface place it in the folder
  5  # ~/.reticulum/interfaces, and add an interface entry to
  6  # your Reticulum configuration file similar to this:
  7  
  8  #  [[Example Custom Interface]]
  9  #    type = ExampleInterface
 10  #    enabled = no
 11  #    mode = gateway
 12  #    port = /dev/ttyUSB0
 13  #    speed = 115200
 14  #    databits = 8
 15  #    parity = none
 16  #    stopbits = 1
 17  
 18  from time import sleep
 19  import sys
 20  import threading
 21  import time
 22  
 23  # This HDLC helper class is used by the interface
 24  # to delimit and packetize data over the physical
 25  # medium - in this case a serial connection.
 26  class HDLC():
 27      # This example interface packetizes data using
 28      # simplified HDLC framing, similar to PPP
 29      FLAG     = 0x7E
 30      ESC      = 0x7D
 31      ESC_MASK = 0x20
 32  
 33      @staticmethod
 34      def escape(data):
 35          data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK]))
 36          data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK]))
 37          return data
 38  
 39  # Let's define our custom interface class. It must
 40  # be a sub-class of the RNS "Interface" class.
 41  class ExampleInterface(Interface):
 42      # All interface classes must define a default
 43      # IFAC size, used in IFAC setup when the user
 44      # has not specified a custom IFAC size. This
 45      # option is specified in bytes.
 46      DEFAULT_IFAC_SIZE = 8
 47  
 48      # The following properties are local to this
 49      # particular interface implementation.
 50      owner    = None
 51      port     = None
 52      speed    = None
 53      databits = None
 54      parity   = None
 55      stopbits = None
 56      serial   = None
 57  
 58      # All Reticulum interfaces must have an __init__
 59      # method that takes 2 positional arguments:
 60      # The owner RNS Transport instance, and a dict
 61      # of configuration values.
 62      def __init__(self, owner, configuration):
 63  
 64          # The following lines demonstrate handling
 65          # potential dependencies required for the
 66          # interface to function correctly.
 67          import importlib
 68          if importlib.util.find_spec('serial') != None:
 69              import serial
 70          else:
 71              RNS.log("Using this interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
 72              RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
 73              RNS.panic()
 74  
 75          # We start out by initialising the super-class
 76          super().__init__()
 77  
 78          # To make sure the configuration data is in the
 79          # correct format, we parse it through the following
 80          # method on the generic Interface class. This step
 81          # is required to ensure compatibility on all the
 82          # platforms that Reticulum supports.
 83          ifconf    = Interface.get_config_obj(configuration)
 84  
 85          # Read the interface name from the configuration
 86          # and set it on our interface instance.
 87          name      = ifconf["name"]
 88          self.name = name
 89  
 90          # We read configuration parameters from the supplied
 91          # configuration data, and provide default values in
 92          # case any are missing.
 93          port      = ifconf["port"] if "port" in ifconf else None
 94          speed     = int(ifconf["speed"]) if "speed" in ifconf else 9600
 95          databits  = int(ifconf["databits"]) if "databits" in ifconf else 8
 96          parity    = ifconf["parity"] if "parity" in ifconf else "N"
 97          stopbits  = int(ifconf["stopbits"]) if "stopbits" in ifconf else 1
 98  
 99          # In case no port is specified, we abort setup by
100          # raising an exception.
101          if port == None:
102              raise ValueError(f"No port specified for {self}")
103  
104          # All interfaces must supply a hardware MTU value
105          # to the RNS Transport instance. This value should
106          # be the maximum data packet payload size that the
107          # underlying medium is capable of handling in all
108          # cases without any segmentation.
109          self.HW_MTU = 564
110  
111          # We initially set the "online" property to false,
112          # since the interface has not actually been fully
113          # initialised and connected yet.
114          self.online   = False
115  
116          # In this case, we can also set the indicated bit-
117          # rate of the interface to the serial port speed.
118          self.bitrate  = speed
119          
120          # Configure internal properties on the interface
121          # according to the supplied configuration.
122          self.pyserial = serial
123          self.serial   = None
124          self.owner    = owner
125          self.port     = port
126          self.speed    = speed
127          self.databits = databits
128          self.parity   = serial.PARITY_NONE
129          self.stopbits = stopbits
130          self.timeout  = 100
131  
132          if parity.lower() == "e" or parity.lower() == "even":
133              self.parity = serial.PARITY_EVEN
134  
135          if parity.lower() == "o" or parity.lower() == "odd":
136              self.parity = serial.PARITY_ODD
137  
138          # Since all required parameters are now configured,
139          # we will try opening the serial port.
140          try:
141              self.open_port()
142          except Exception as e:
143              RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
144              raise e
145  
146          # If opening the port succeeded, run any post-open
147          # configuration required.
148          if self.serial.is_open:
149              self.configure_device()
150          else:
151              raise IOError("Could not open serial port")
152  
153      # Open the serial port with supplied configuration
154      # parameters and store a reference to the open port.
155      def open_port(self):
156          RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE)
157          self.serial = self.pyserial.Serial(
158              port = self.port,
159              baudrate = self.speed,
160              bytesize = self.databits,
161              parity = self.parity,
162              stopbits = self.stopbits,
163              xonxoff = False,
164              rtscts = False,
165              timeout = 0,
166              inter_byte_timeout = None,
167              write_timeout = None,
168              dsrdtr = False,
169          )
170  
171      # The only thing required after opening the port
172      # is to wait a small amount of time for the
173      # hardware to initialise and then start a thread
174      # that reads any incoming data from the device.
175      def configure_device(self):
176          sleep(0.5)
177          thread = threading.Thread(target=self.read_loop)
178          thread.daemon = True
179          thread.start()
180          self.online = True
181          RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE)
182  
183  
184      # This method will be called from our read-loop
185      # whenever a full packet has been received over
186      # the underlying medium.
187      def process_incoming(self, data):
188          # Update our received bytes counter
189          self.rxb += len(data)            
190  
191          # And send the data packet to the Transport
192          # instance for processing.
193          self.owner.inbound(data, self)
194  
195      # The running Reticulum Transport instance will
196      # call this method on the interface whenever the
197      # interface must transmit a packet.
198      def process_outgoing(self,data):
199          if self.online:
200              # First, escape and packetize the data
201              # according to HDLC framing.
202              data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG])
203  
204              # Then write the framed data to the port
205              written = self.serial.write(data)
206  
207              # Update the transmitted bytes counter
208              # and ensure that all data was written
209              self.txb += len(data)            
210              if written != len(data):
211                  raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
212  
213      # This read loop runs in a thread and continously
214      # receives bytes from the underlying serial port.
215      # When a full packet has been received, it will
216      # be sent to the process_incoming methed, which
217      # will in turn pass it to the Transport instance.
218      def read_loop(self):
219          try:
220              in_frame = False
221              escape = False
222              data_buffer = b""
223              last_read_ms = int(time.time()*1000)
224  
225              while self.serial.is_open:
226                  if self.serial.in_waiting:
227                      byte = ord(self.serial.read(1))
228                      last_read_ms = int(time.time()*1000)
229  
230                      if (in_frame and byte == HDLC.FLAG):
231                          in_frame = False
232                          self.process_incoming(data_buffer)
233                      elif (byte == HDLC.FLAG):
234                          in_frame = True
235                          data_buffer = b""
236                      elif (in_frame and len(data_buffer) < self.HW_MTU):
237                          if (byte == HDLC.ESC):
238                              escape = True
239                          else:
240                              if (escape):
241                                  if (byte == HDLC.FLAG ^ HDLC.ESC_MASK):
242                                      byte = HDLC.FLAG
243                                  if (byte == HDLC.ESC  ^ HDLC.ESC_MASK):
244                                      byte = HDLC.ESC
245                                  escape = False
246                              data_buffer = data_buffer+bytes([byte])
247                          
248                  else:
249                      time_since_last = int(time.time()*1000) - last_read_ms
250                      if len(data_buffer) > 0 and time_since_last > self.timeout:
251                          data_buffer = b""
252                          in_frame = False
253                          escape = False
254                      sleep(0.08)
255                      
256          except Exception as e:
257              self.online = False
258              RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
259              RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
260              
261              if RNS.Reticulum.panic_on_interface_error:
262                  RNS.panic()
263  
264              RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
265  
266          self.online = False
267          self.serial.close()
268          self.reconnect_port()
269  
270      # This method handles serial port disconnects.
271      def reconnect_port(self):
272          while not self.online:
273              try:
274                  time.sleep(5)
275                  RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
276                  self.open_port()
277                  if self.serial.is_open:
278                      self.configure_device()
279              except Exception as e:
280                  RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
281  
282          RNS.log("Reconnected serial port for "+str(self))
283  
284      # Signal to Reticulum that this interface should
285      # not perform any ingress limiting.
286      def should_ingress_limit(self):
287          return False
288  
289      # We must provide a string representation of this
290      # interface, that is used whenever the interface
291      # is printed in logs or external programs.
292      def __str__(self):
293          return "ExampleInterface["+self.name+"]"
294  
295  # Finally, register the defined interface class as the
296  # target class for Reticulum to use as an interface
297  interface_class = ExampleInterface