/ RNS / Interfaces / KISSInterface.py
KISSInterface.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 KISS():
 39      FEND              = 0xC0
 40      FESC              = 0xDB
 41      TFEND             = 0xDC
 42      TFESC             = 0xDD
 43      CMD_UNKNOWN       = 0xFE
 44      CMD_DATA          = 0x00
 45      CMD_TXDELAY       = 0x01
 46      CMD_P             = 0x02
 47      CMD_SLOTTIME      = 0x03
 48      CMD_TXTAIL        = 0x04
 49      CMD_FULLDUPLEX    = 0x05
 50      CMD_SETHARDWARE   = 0x06
 51      CMD_READY         = 0x0F
 52      CMD_RETURN        = 0xFF
 53  
 54      @staticmethod
 55      def escape(data):
 56          data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
 57          data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
 58          return data
 59  
 60  class KISSInterface(Interface):
 61      MAX_CHUNK = 32768
 62      BITRATE_GUESS = 1200
 63      DEFAULT_IFAC_SIZE = 8
 64  
 65      owner    = None
 66      port     = None
 67      speed    = None
 68      databits = None
 69      parity   = None
 70      stopbits = None
 71      serial   = None
 72  
 73      def __init__(self, owner, configuration):
 74          import importlib.util
 75          if importlib.util.find_spec('serial') != None:
 76              import serial
 77          else:
 78              RNS.log("Using the KISS interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
 79              RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
 80              RNS.panic()
 81  
 82          super().__init__()
 83  
 84          c = Interface.get_config_obj(configuration)
 85          name = c["name"]
 86          preamble = int(c["preamble"]) if "preamble" in c else None
 87          txtail = int(c["txtail"]) if "txtail" in c else None
 88          persistence = int(c["persistence"]) if "persistence" in c else None
 89          slottime = int(c["slottime"]) if "slottime" in c else None
 90          flow_control = c.as_bool("flow_control") if "flow_control" in c else False
 91          port = c["port"] if "port" in c else None
 92          speed = int(c["speed"]) if "speed" in c else 9600
 93          databits = int(c["databits"]) if "databits" in c else 8
 94          parity = c["parity"] if "parity" in c else "N"
 95          stopbits = int(c["stopbits"]) if "stopbits" in c else 1
 96          beacon_interval = int(c["id_interval"]) if "id_interval" in c else None
 97          beacon_data = c["id_callsign"] if "id_callsign" in c else None
 98  
 99          if port == None:
100              raise ValueError("No port specified for serial interface")
101          
102          self.HW_MTU = 564
103          
104          if beacon_data == None:
105              beacon_data = ""
106  
107          self.pyserial = serial
108          self.serial   = None
109          self.owner    = owner
110          self.name     = name
111          self.port     = port
112          self.speed    = speed
113          self.databits = databits
114          self.parity   = serial.PARITY_NONE
115          self.stopbits = stopbits
116          self.timeout  = 100
117          self.online   = False
118          self.beacon_i = beacon_interval
119          self.beacon_d = beacon_data.encode("utf-8")
120          self.first_tx = None
121          self.bitrate  = KISSInterface.BITRATE_GUESS
122  
123          self.packet_queue    = []
124          self.flow_control    = flow_control
125          self.interface_ready = False
126          self.flow_control_timeout = 5
127          self.flow_control_locked  = time.time()
128  
129          self.preamble    = preamble if preamble != None else 350;
130          self.txtail      = txtail if txtail != None else 20;
131          self.persistence = persistence if persistence != None else 64;
132          self.slottime    = slottime if slottime != None else 20;
133  
134          if parity.lower() == "e" or parity.lower() == "even":
135              self.parity = serial.PARITY_EVEN
136  
137          if parity.lower() == "o" or parity.lower() == "odd":
138              self.parity = serial.PARITY_ODD
139  
140          try:
141              self.open_port()
142          except Exception as e:
143              RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR)
144              raise e
145  
146          if self.serial.is_open:
147              self.configure_device()
148          else:
149              raise IOError("Could not open serial port")
150  
151  
152      def open_port(self):
153          RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE)
154          self.serial = self.pyserial.Serial(
155              port = self.port,
156              baudrate = self.speed,
157              bytesize = self.databits,
158              parity = self.parity,
159              stopbits = self.stopbits,
160              xonxoff = False,
161              rtscts = False,
162              timeout = 0,
163              inter_byte_timeout = None,
164              write_timeout = None,
165              dsrdtr = False,
166          )
167  
168  
169      def configure_device(self):
170          # Allow time for interface to initialise before config
171          sleep(2.0)
172          thread = threading.Thread(target=self.readLoop)
173          thread.daemon = True
174          thread.start()
175          self.online = True
176          RNS.log("Serial port "+self.port+" is now open")
177          RNS.log("Configuring KISS interface parameters...")
178          self.setPreamble(self.preamble)
179          self.setTxTail(self.txtail)
180          self.setPersistence(self.persistence)
181          self.setSlotTime(self.slottime)
182          self.setFlowControl(self.flow_control)
183          self.interface_ready = True
184          RNS.log("KISS interface configured")
185  
186  
187      def setPreamble(self, preamble):
188          preamble_ms = preamble
189          preamble = int(preamble_ms / 10)
190          if preamble < 0:
191              preamble = 0
192          if preamble > 255:
193              preamble = 255
194  
195          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND])
196          written = self.serial.write(kiss_command)
197          if written != len(kiss_command):
198              raise IOError("Could not configure KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")")
199  
200      def setTxTail(self, txtail):
201          txtail_ms = txtail
202          txtail = int(txtail_ms / 10)
203          if txtail < 0:
204              txtail = 0
205          if txtail > 255:
206              txtail = 255
207  
208          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND])
209          written = self.serial.write(kiss_command)
210          if written != len(kiss_command):
211              raise IOError("Could not configure KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")")
212  
213      def setPersistence(self, persistence):
214          if persistence < 0:
215              persistence = 0
216          if persistence > 255:
217              persistence = 255
218  
219          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND])
220          written = self.serial.write(kiss_command)
221          if written != len(kiss_command):
222              raise IOError("Could not configure KISS interface persistence to "+str(persistence))
223  
224      def setSlotTime(self, slottime):
225          slottime_ms = slottime
226          slottime = int(slottime_ms / 10)
227          if slottime < 0:
228              slottime = 0
229          if slottime > 255:
230              slottime = 255
231  
232          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND])
233          written = self.serial.write(kiss_command)
234          if written != len(kiss_command):
235              raise IOError("Could not configure KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")")
236  
237      def setFlowControl(self, flow_control):
238          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND])
239          written = self.serial.write(kiss_command)
240          if written != len(kiss_command):
241              if (flow_control):
242                  raise IOError("Could not enable KISS interface flow control")
243              else:
244                  raise IOError("Could not enable KISS interface flow control")
245  
246  
247      def process_incoming(self, data):
248          self.rxb += len(data)  
249          self.owner.inbound(data, self)
250  
251  
252      def process_outgoing(self,data):
253          datalen = len(data)
254          if self.online:
255              if self.interface_ready:
256                  if self.flow_control:
257                      self.interface_ready = False
258                      self.flow_control_locked = time.time()
259  
260                  data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd]))
261                  data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc]))
262                  frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND])
263  
264                  written = self.serial.write(frame)
265                  self.txb += datalen
266  
267                  if data == self.beacon_d:
268                      self.first_tx = None
269                  else:
270                      if self.first_tx == None:
271                          self.first_tx = time.time()
272  
273                  if written != len(frame):
274                      raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
275  
276              else:
277                  self.queue(data)
278  
279      def queue(self, data):
280          self.packet_queue.append(data)
281  
282      def process_queue(self):
283          if len(self.packet_queue) > 0:
284              data = self.packet_queue.pop(0)
285              self.interface_ready = True
286              self.process_outgoing(data)
287          elif len(self.packet_queue) == 0:
288              self.interface_ready = True
289  
290      def readLoop(self):
291          try:
292              in_frame = False
293              escape = False
294              command = KISS.CMD_UNKNOWN
295              data_buffer = b""
296              last_read_ms = int(time.time()*1000)
297  
298              while self.serial.is_open:
299                  if self.serial.in_waiting:
300                      byte = ord(self.serial.read(1))
301                      last_read_ms = int(time.time()*1000)
302  
303                      if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
304                          in_frame = False
305                          self.process_incoming(data_buffer)
306                      elif (byte == KISS.FEND):
307                          in_frame = True
308                          command = KISS.CMD_UNKNOWN
309                          data_buffer = b""
310                      elif (in_frame and len(data_buffer) < self.HW_MTU):
311                          if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
312                              # We only support one HDLC port for now, so
313                              # strip off the port nibble
314                              byte = byte & 0x0F
315                              command = byte
316                          elif (command == KISS.CMD_DATA):
317                              if (byte == KISS.FESC):
318                                  escape = True
319                              else:
320                                  if (escape):
321                                      if (byte == KISS.TFEND):
322                                          byte = KISS.FEND
323                                      if (byte == KISS.TFESC):
324                                          byte = KISS.FESC
325                                      escape = False
326                                  data_buffer = data_buffer+bytes([byte])
327                          elif (command == KISS.CMD_READY):
328                              self.process_queue()
329                  else:
330                      time_since_last = int(time.time()*1000) - last_read_ms
331                      if len(data_buffer) > 0 and time_since_last > self.timeout:
332                          data_buffer = b""
333                          in_frame = False
334                          command = KISS.CMD_UNKNOWN
335                          escape = False
336                      sleep(0.05)
337  
338                      if self.flow_control:
339                          if not self.interface_ready:
340                              if time.time() > self.flow_control_locked + self.flow_control_timeout:
341                                  RNS.log("Interface "+str(self)+" is unlocking flow control due to time-out. This should not happen. Your hardware might have missed a flow-control READY command, or maybe it does not support flow-control.", RNS.LOG_WARNING)
342                                  self.process_queue()
343  
344                      if self.beacon_i != None and self.beacon_d != None:
345                          if self.first_tx != None:
346                              if time.time() > self.first_tx + self.beacon_i:
347                                  RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.beacon_d.decode("utf-8")), RNS.LOG_DEBUG)
348                                  self.first_tx = None
349                                  
350                                  # Pad to minimum length
351                                  frame = bytearray(self.beacon_d)
352                                  while len(frame) < 15:
353                                      frame.append(0x00)
354                                      
355                                  self.process_outgoing(bytes(frame))
356  
357          except Exception as e:
358              self.online = False
359              RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
360              RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
361              
362              if RNS.Reticulum.panic_on_interface_error:
363                  RNS.panic()
364  
365              RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
366  
367          self.online = False
368          self.serial.close()
369          self.reconnect_port()
370  
371      def reconnect_port(self):
372          while not self.online:
373              try:
374                  time.sleep(5)
375                  RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
376                  self.open_port()
377                  if self.serial.is_open:
378                      self.configure_device()
379              except Exception as e:
380                  RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
381  
382          RNS.log("Reconnected serial port for "+str(self))
383  
384      def should_ingress_limit(self):
385          return False
386  
387      def __str__(self):
388          return "KISSInterface["+self.name+"]"