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