SerialInterface.py
1 # Reticulum License 2 # 3 # Copyright (c) 2016-2025 Mark Qvist 4 # 5 # Permission is hereby granted, free of charge, to any person obtaining a copy 6 # of this software and associated documentation files (the "Software"), to deal 7 # in the Software without restriction, including without limitation the rights 8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 # copies of the Software, and to permit persons to whom the Software is 10 # furnished to do so, subject to the following conditions: 11 # 12 # - The Software shall not be used in any kind of system which includes amongst 13 # its functions the ability to purposefully do harm to human beings. 14 # 15 # - The Software shall not be used, directly or indirectly, in the creation of 16 # an artificial intelligence, machine learning or language model training 17 # dataset, including but not limited to any use that contributes to the 18 # training or development of such a model or algorithm. 19 # 20 # - The above copyright notice and this permission notice shall be included in 21 # all copies or substantial portions of the Software. 22 # 23 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 29 # SOFTWARE. 30 31 from RNS.Interfaces.Interface import Interface 32 from time import sleep 33 import sys 34 import threading 35 import time 36 import RNS 37 38 class HDLC(): 39 # The Serial Interface packetizes data using 40 # simplified HDLC framing, similar to PPP 41 FLAG = 0x7E 42 ESC = 0x7D 43 ESC_MASK = 0x20 44 45 @staticmethod 46 def escape(data): 47 data = data.replace(bytes([HDLC.ESC]), bytes([HDLC.ESC, HDLC.ESC^HDLC.ESC_MASK])) 48 data = data.replace(bytes([HDLC.FLAG]), bytes([HDLC.ESC, HDLC.FLAG^HDLC.ESC_MASK])) 49 return data 50 51 class SerialInterface(Interface): 52 MAX_CHUNK = 32768 53 DEFAULT_IFAC_SIZE = 8 54 55 owner = None 56 port = None 57 speed = None 58 databits = None 59 parity = None 60 stopbits = None 61 serial = None 62 63 def __init__(self, owner, configuration): 64 import importlib.util 65 if importlib.util.find_spec('serial') != None: 66 import serial 67 else: 68 RNS.log("Using the Serial interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL) 69 RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL) 70 RNS.panic() 71 72 super().__init__() 73 74 c = Interface.get_config_obj(configuration) 75 name = c["name"] 76 port = c["port"] if "port" in c else None 77 speed = int(c["speed"]) if "speed" in c else 9600 78 databits = int(c["databits"]) if "databits" in c else 8 79 parity = c["parity"] if "parity" in c else "N" 80 stopbits = int(c["stopbits"]) if "stopbits" in c else 1 81 82 if port == None: 83 raise ValueError("No port specified for serial interface") 84 85 self.HW_MTU = 564 86 87 self.pyserial = serial 88 self.serial = None 89 self.owner = owner 90 self.name = name 91 self.port = port 92 self.speed = speed 93 self.databits = databits 94 self.parity = serial.PARITY_NONE 95 self.stopbits = stopbits 96 self.timeout = 100 97 self.online = False 98 self.bitrate = self.speed 99 100 if parity.lower() == "e" or parity.lower() == "even": 101 self.parity = serial.PARITY_EVEN 102 103 if parity.lower() == "o" or parity.lower() == "odd": 104 self.parity = serial.PARITY_ODD 105 106 try: 107 self.open_port() 108 except Exception as e: 109 RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) 110 raise e 111 112 if self.serial.is_open: 113 self.configure_device() 114 else: 115 raise IOError("Could not open serial port") 116 117 118 def open_port(self): 119 RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE) 120 self.serial = self.pyserial.Serial( 121 port = self.port, 122 baudrate = self.speed, 123 bytesize = self.databits, 124 parity = self.parity, 125 stopbits = self.stopbits, 126 xonxoff = False, 127 rtscts = False, 128 timeout = 0, 129 inter_byte_timeout = None, 130 write_timeout = None, 131 dsrdtr = False, 132 ) 133 134 135 def configure_device(self): 136 sleep(0.5) 137 thread = threading.Thread(target=self.readLoop) 138 thread.daemon = True 139 thread.start() 140 self.online = True 141 RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE) 142 143 144 def process_incoming(self, data): 145 self.rxb += len(data) 146 self.owner.inbound(data, self) 147 148 149 def process_outgoing(self,data): 150 if self.online: 151 data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) 152 written = self.serial.write(data) 153 self.txb += len(data) 154 if written != len(data): 155 raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) 156 157 158 def readLoop(self): 159 try: 160 in_frame = False 161 escape = False 162 data_buffer = b"" 163 last_read_ms = int(time.time()*1000) 164 165 while self.serial.is_open: 166 if self.serial.in_waiting: 167 byte = ord(self.serial.read(1)) 168 last_read_ms = int(time.time()*1000) 169 170 if (in_frame and byte == HDLC.FLAG): 171 in_frame = False 172 self.process_incoming(data_buffer) 173 elif (byte == HDLC.FLAG): 174 in_frame = True 175 data_buffer = b"" 176 elif (in_frame and len(data_buffer) < self.HW_MTU): 177 if (byte == HDLC.ESC): 178 escape = True 179 else: 180 if (escape): 181 if (byte == HDLC.FLAG ^ HDLC.ESC_MASK): 182 byte = HDLC.FLAG 183 if (byte == HDLC.ESC ^ HDLC.ESC_MASK): 184 byte = HDLC.ESC 185 escape = False 186 data_buffer = data_buffer+bytes([byte]) 187 188 else: 189 time_since_last = int(time.time()*1000) - last_read_ms 190 if len(data_buffer) > 0 and time_since_last > self.timeout: 191 data_buffer = b"" 192 in_frame = False 193 escape = False 194 sleep(0.08) 195 196 except Exception as e: 197 self.online = False 198 RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) 199 RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) 200 201 if RNS.Reticulum.panic_on_interface_error: 202 RNS.panic() 203 204 RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) 205 206 self.online = False 207 self.serial.close() 208 self.reconnect_port() 209 210 def reconnect_port(self): 211 while not self.online: 212 try: 213 time.sleep(5) 214 RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) 215 self.open_port() 216 if self.serial.is_open: 217 self.configure_device() 218 except Exception as e: 219 RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) 220 221 RNS.log("Reconnected serial port for "+str(self)) 222 223 def should_ingress_limit(self): 224 return False 225 226 def __str__(self): 227 return "SerialInterface["+self.name+"]"