/ RNS / Interfaces / SerialInterface.py
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+"]"