/ RNS / Interfaces / Android / 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 RNS.vendor.platformutils.is_android():
 76              self.on_android  = True
 77              if importlib.util.find_spec('usbserial4a') != None:
 78                  if importlib.util.find_spec('jnius') == None:
 79                      RNS.log("Could not load jnius API wrapper for Android, KISS interface cannot be created.", RNS.LOG_CRITICAL)
 80                      RNS.log("This probably means you are trying to use an USB-based interface from within Termux or similar.", RNS.LOG_CRITICAL)
 81                      RNS.log("This is currently not possible, due to this environment limiting access to the native Android APIs.", RNS.LOG_CRITICAL)
 82                      RNS.panic()
 83  
 84                  from usbserial4a import serial4a as serial
 85                  self.parity = "N"
 86              
 87              else:
 88                  RNS.log("Could not load USB serial module for Android, KISS interface cannot be created.", RNS.LOG_CRITICAL)
 89                  RNS.log("You can install this module by issuing: pip install usbserial4a", RNS.LOG_CRITICAL)
 90                  RNS.panic()
 91          else:
 92              raise SystemError("Android-specific interface was used on non-Android OS")
 93  
 94          super().__init__()
 95  
 96          c = Interface.get_config_obj(configuration)
 97          name = c["name"]
 98          preamble = int(c["preamble"]) if "preamble" in c else None
 99          txtail = int(c["txtail"]) if "txtail" in c else None
100          persistence = int(c["persistence"]) if "persistence" in c else None
101          slottime = int(c["slottime"]) if "slottime" in c else None
102          flow_control = c.as_bool("flow_control") if "flow_control" in c else False
103          port = c["port"] if "port" in c else None
104          speed = int(c["speed"]) if "speed" in c else 9600
105          databits = int(c["databits"]) if "databits" in c else 8
106          parity = c["parity"] if "parity" in c else "N"
107          stopbits = int(c["stopbits"]) if "stopbits" in c else 1
108          beacon_interval = int(c["beacon_interval"]) if "beacon_interval" in c and c["beacon_interval"] != None else None
109          beacon_data = c["beacon_data"] if "beacon_data" in c else None
110          
111          self.HW_MTU = 564
112          
113          if beacon_data == None:
114              beacon_data = ""
115  
116          self.pyserial = serial
117          self.serial   = None
118          self.owner    = owner
119          self.name     = name
120          self.port     = port
121          self.speed    = speed
122          self.databits = databits
123          self.parity   = "N"
124          self.stopbits = stopbits
125          self.timeout  = 100
126          self.online   = False
127          self.beacon_i = beacon_interval
128          self.beacon_d = beacon_data.encode("utf-8")
129          self.first_tx = None
130          self.bitrate  = KISSInterface.BITRATE_GUESS
131  
132          self.packet_queue    = []
133          self.flow_control    = flow_control
134          self.interface_ready = False
135          self.flow_control_timeout = 5
136          self.flow_control_locked  = time.time()
137  
138          self.preamble    = preamble if preamble != None else 350;
139          self.txtail      = txtail if txtail != None else 20;
140          self.persistence = persistence if persistence != None else 64;
141          self.slottime    = slottime if slottime != None else 20;
142  
143          if parity.lower() == "e" or parity.lower() == "even":
144              self.parity = "E"
145  
146          if parity.lower() == "o" or parity.lower() == "odd":
147              self.parity = "O"
148  
149          try:
150              self.open_port()
151          except Exception as e:
152              RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR)
153              raise e
154  
155          if self.serial.is_open:
156              self.configure_device()
157          else:
158              raise IOError("Could not open serial port")
159  
160  
161      def open_port(self):
162          RNS.log("Opening serial port "+self.port+"...")
163          # Get device parameters
164          from usb4a import usb
165          device = usb.get_usb_device(self.port)
166          if device:
167              vid = device.getVendorId()
168              pid = device.getProductId()
169  
170              # Driver overrides for speficic chips
171              proxy = self.pyserial.get_serial_port
172              if vid == 0x1A86 and pid == 0x55D4:
173                  # Force CDC driver for Qinheng CH34x
174                  RNS.log(str(self)+" using CDC driver for "+RNS.hexrep(vid)+":"+RNS.hexrep(pid), RNS.LOG_DEBUG)
175                  from usbserial4a.cdcacmserial4a import CdcAcmSerial
176                  proxy = CdcAcmSerial
177  
178              self.serial = proxy(
179                  self.port,
180                  baudrate = self.speed,
181                  bytesize = self.databits,
182                  parity = self.parity,
183                  stopbits = self.stopbits,
184                  xonxoff = False,
185                  rtscts = False,
186                  timeout = None,
187                  inter_byte_timeout = None,
188                  # write_timeout = wtimeout,
189                  dsrdtr = False,
190              )
191  
192              if vid == 0x0403:
193                  # Hardware parameters for FTDI devices @ 115200 baud
194                  self.serial.DEFAULT_READ_BUFFER_SIZE = 16 * 1024
195                  self.serial.USB_READ_TIMEOUT_MILLIS = 100
196                  self.serial.timeout = 0.1
197              elif vid == 0x10C4:
198                  # Hardware parameters for SiLabs CP210x @ 115200 baud
199                  self.serial.DEFAULT_READ_BUFFER_SIZE = 64 
200                  self.serial.USB_READ_TIMEOUT_MILLIS = 12
201                  self.serial.timeout = 0.012
202              elif vid == 0x1A86 and pid == 0x55D4:
203                  # Hardware parameters for Qinheng CH34x @ 115200 baud
204                  self.serial.DEFAULT_READ_BUFFER_SIZE = 64
205                  self.serial.USB_READ_TIMEOUT_MILLIS = 12
206                  self.serial.timeout = 0.1
207              else:
208                  # Default values
209                  self.serial.DEFAULT_READ_BUFFER_SIZE = 1 * 1024
210                  self.serial.USB_READ_TIMEOUT_MILLIS = 100
211                  self.serial.timeout = 0.1
212  
213              RNS.log(str(self)+" USB read buffer size set to "+RNS.prettysize(self.serial.DEFAULT_READ_BUFFER_SIZE), RNS.LOG_DEBUG)
214              RNS.log(str(self)+" USB read timeout set to "+str(self.serial.USB_READ_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG)
215              RNS.log(str(self)+" USB write timeout set to "+str(self.serial.USB_WRITE_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG)
216  
217      def configure_device(self):
218          # Allow time for interface to initialise before config
219          sleep(2.0)
220          thread = threading.Thread(target=self.readLoop)
221          thread.daemon = True
222          thread.start()
223          self.online = True
224          RNS.log("Serial port "+self.port+" is now open")
225          RNS.log("Configuring KISS interface parameters...")
226          self.setPreamble(self.preamble)
227          self.setTxTail(self.txtail)
228          self.setPersistence(self.persistence)
229          self.setSlotTime(self.slottime)
230          self.setFlowControl(self.flow_control)
231          self.interface_ready = True
232          RNS.log("KISS interface configured")
233  
234      def setPreamble(self, preamble):
235          preamble_ms = preamble
236          preamble = int(preamble_ms / 10)
237          if preamble < 0:
238              preamble = 0
239          if preamble > 255:
240              preamble = 255
241  
242          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND])
243          written = self.serial.write(kiss_command)
244          if written != len(kiss_command):
245              raise IOError("Could not configure KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")")
246  
247      def setTxTail(self, txtail):
248          txtail_ms = txtail
249          txtail = int(txtail_ms / 10)
250          if txtail < 0:
251              txtail = 0
252          if txtail > 255:
253              txtail = 255
254  
255          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND])
256          written = self.serial.write(kiss_command)
257          if written != len(kiss_command):
258              raise IOError("Could not configure KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")")
259  
260      def setPersistence(self, persistence):
261          if persistence < 0:
262              persistence = 0
263          if persistence > 255:
264              persistence = 255
265  
266          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND])
267          written = self.serial.write(kiss_command)
268          if written != len(kiss_command):
269              raise IOError("Could not configure KISS interface persistence to "+str(persistence))
270  
271      def setSlotTime(self, slottime):
272          slottime_ms = slottime
273          slottime = int(slottime_ms / 10)
274          if slottime < 0:
275              slottime = 0
276          if slottime > 255:
277              slottime = 255
278  
279          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND])
280          written = self.serial.write(kiss_command)
281          if written != len(kiss_command):
282              raise IOError("Could not configure KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")")
283  
284      def setFlowControl(self, flow_control):
285          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND])
286          written = self.serial.write(kiss_command)
287          if written != len(kiss_command):
288              if (flow_control):
289                  raise IOError("Could not enable KISS interface flow control")
290              else:
291                  raise IOError("Could not enable KISS interface flow control")
292  
293  
294      def process_incoming(self, data):
295          self.rxb += len(data)
296          def af():
297              self.owner.inbound(data, self)
298          threading.Thread(target=af, daemon=True).start()
299  
300      def process_outgoing(self,data):
301          datalen = len(data)
302          if self.online:
303              if self.interface_ready:
304                  if self.flow_control:
305                      self.interface_ready = False
306                      self.flow_control_locked = time.time()
307  
308                  data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd]))
309                  data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc]))
310                  frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND])
311  
312                  written = self.serial.write(frame)
313                  self.txb += datalen
314  
315                  if data == self.beacon_d:
316                      self.first_tx = None
317                  else:
318                      if self.first_tx == None:
319                          self.first_tx = time.time()
320  
321                  if written != len(frame):
322                      raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
323  
324              else:
325                  self.queue(data)
326  
327      def queue(self, data):
328          self.packet_queue.append(data)
329  
330      def process_queue(self):
331          if len(self.packet_queue) > 0:
332              data = self.packet_queue.pop(0)
333              self.interface_ready = True
334              self.process_outgoing(data)
335          elif len(self.packet_queue) == 0:
336              self.interface_ready = True
337  
338      def readLoop(self):
339          try:
340              in_frame = False
341              escape = False
342              command = KISS.CMD_UNKNOWN
343              data_buffer = b""
344              last_read_ms = int(time.time()*1000)
345  
346              while self.serial.is_open:
347                  serial_bytes = self.serial.read()
348                  got = len(serial_bytes)
349  
350                  for byte in serial_bytes:
351                      last_read_ms = int(time.time()*1000)
352  
353                      if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
354                          in_frame = False
355                          self.process_incoming(data_buffer)
356                      elif (byte == KISS.FEND):
357                          in_frame = True
358                          command = KISS.CMD_UNKNOWN
359                          data_buffer = b""
360                      elif (in_frame and len(data_buffer) < self.HW_MTU):
361                          if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
362                              # We only support one HDLC port for now, so
363                              # strip off the port nibble
364                              byte = byte & 0x0F
365                              command = byte
366                          elif (command == KISS.CMD_DATA):
367                              if (byte == KISS.FESC):
368                                  escape = True
369                              else:
370                                  if (escape):
371                                      if (byte == KISS.TFEND):
372                                          byte = KISS.FEND
373                                      if (byte == KISS.TFESC):
374                                          byte = KISS.FESC
375                                      escape = False
376                                  data_buffer = data_buffer+bytes([byte])
377                          elif (command == KISS.CMD_READY):
378                              self.process_queue()
379                  
380                  if got == 0:
381                      time_since_last = int(time.time()*1000) - last_read_ms
382                      if len(data_buffer) > 0 and time_since_last > self.timeout:
383                          data_buffer = b""
384                          in_frame = False
385                          command = KISS.CMD_UNKNOWN
386                          escape = False
387                      sleep(0.05)
388  
389                      if self.flow_control:
390                          if not self.interface_ready:
391                              if time.time() > self.flow_control_locked + self.flow_control_timeout:
392                                  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)
393                                  self.process_queue()
394  
395                      if self.beacon_i != None and self.beacon_d != None:
396                          if self.first_tx != None:
397                              if time.time() > self.first_tx + self.beacon_i:
398                                  RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.beacon_d.decode("utf-8")), RNS.LOG_DEBUG)
399                                  self.first_tx = None
400  
401                                  # Pad to minimum length
402                                  frame = bytearray(self.beacon_d)
403                                  while len(frame) < 15:
404                                      frame.append(0x00)
405  
406                                  self.process_outgoing(bytes(frame))
407  
408          except Exception as e:
409              self.online = False
410              RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
411              RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
412              
413              if RNS.Reticulum.panic_on_interface_error:
414                  RNS.panic()
415  
416              RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
417  
418          self.online = False
419          self.serial.close()
420          self.reconnect_port()
421  
422      def reconnect_port(self):
423          while not self.online:
424              try:
425                  time.sleep(5)
426                  RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
427                  self.open_port()
428                  if self.serial.is_open:
429                      self.configure_device()
430              except Exception as e:
431                  RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
432  
433          RNS.log("Reconnected serial port for "+str(self))
434  
435      def should_ingress_limit(self):
436          return False
437  
438      def __str__(self):
439          return "KISSInterface["+self.name+"]"