/ RNS / Interfaces / Android / RNodeInterface.py
RNodeInterface.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 socket
  36  import time
  37  import math
  38  import RNS
  39  
  40  try:
  41      from able import BluetoothDispatcher, GATT_SUCCESS
  42  except Exception as e:
  43      GATT_SUCCESS = 0x00
  44      class BluetoothDispatcher():
  45          def __init__(**kwargs):
  46              RNS.log("Attempt to initialise BLE connectivity, but Android BLE support library is unavailable", RNS.LOG_ERROR)
  47              raise OSError("No BLE support available")
  48  
  49  class KISS():
  50      FEND            = 0xC0
  51      FESC            = 0xDB
  52      TFEND           = 0xDC
  53      TFESC           = 0xDD
  54      
  55      CMD_UNKNOWN     = 0xFE
  56      CMD_DATA        = 0x00
  57      CMD_FREQUENCY   = 0x01
  58      CMD_BANDWIDTH   = 0x02
  59      CMD_TXPOWER     = 0x03
  60      CMD_SF          = 0x04
  61      CMD_CR          = 0x05
  62      CMD_RADIO_STATE = 0x06
  63      CMD_RADIO_LOCK  = 0x07
  64      CMD_ST_ALOCK    = 0x0B
  65      CMD_LT_ALOCK    = 0x0C
  66      CMD_DETECT      = 0x08
  67      CMD_LEAVE       = 0x0A
  68      CMD_READY       = 0x0F
  69      CMD_STAT_RX     = 0x21
  70      CMD_STAT_TX     = 0x22
  71      CMD_STAT_RSSI   = 0x23
  72      CMD_STAT_SNR    = 0x24
  73      CMD_STAT_CHTM   = 0x25
  74      CMD_STAT_PHYPRM = 0x26
  75      CMD_STAT_BAT    = 0x27
  76      CMD_STAT_CSMA   = 0x28
  77      CMD_STAT_TEMP   = 0x29
  78      CMD_BLINK       = 0x30
  79      CMD_RANDOM      = 0x40
  80      CMD_FB_EXT      = 0x41
  81      CMD_FB_READ     = 0x42
  82      CMD_DISP_READ   = 0x66
  83      CMD_FB_WRITE    = 0x43
  84      CMD_BT_CTRL     = 0x46
  85      CMD_PLATFORM    = 0x48
  86      CMD_MCU         = 0x49
  87      CMD_FW_VERSION  = 0x50
  88      CMD_ROM_READ    = 0x51
  89      CMD_RESET       = 0x55
  90  
  91      DETECT_REQ      = 0x73
  92      DETECT_RESP     = 0x46
  93      
  94      RADIO_STATE_OFF = 0x00
  95      RADIO_STATE_ON  = 0x01
  96      RADIO_STATE_ASK = 0xFF
  97      
  98      CMD_ERROR              = 0x90
  99      ERROR_INITRADIO        = 0x01
 100      ERROR_TXFAILED         = 0x02
 101      ERROR_EEPROM_LOCKED    = 0x03
 102      ERROR_QUEUE_FULL       = 0x04
 103      ERROR_MEMORY_LOW       = 0x05
 104      ERROR_MODEM_TIMEOUT    = 0x06
 105      ERROR_INVALID_FIRMWARE = 0x10
 106      ERROR_INVALID_BLE_MTU  = 0x20
 107      ERROR_INVALID_CONFIG   = 0x40
 108  
 109      PLATFORM_AVR   = 0x90
 110      PLATFORM_ESP32 = 0x80
 111      PLATFORM_NRF52 = 0x70
 112  
 113      @staticmethod
 114      def escape(data):
 115          data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
 116          data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
 117          return data
 118  
 119  class AndroidBluetoothManager():
 120      DEVICE_TYPE_CLASSIC = 1
 121      DEVICE_TYPE_LE = 2
 122      DEVICE_TYPE_DUAL = 3
 123  
 124      def __init__(self, owner, target_device_name = None, target_device_address = None):
 125          from jnius import autoclass
 126          self.owner = owner
 127          self.connected = False
 128          self.target_device_name = target_device_name
 129          self.target_device_address = target_device_address
 130          self.potential_remote_devices = []
 131          self.rfcomm_socket = None
 132          self.connected_device = None
 133          self.connection_failed = False
 134          self.bt_adapter = autoclass('android.bluetooth.BluetoothAdapter')
 135          self.bt_device  = autoclass('android.bluetooth.BluetoothDevice')
 136          self.bt_socket  = autoclass('android.bluetooth.BluetoothSocket')
 137          self.bt_rfcomm_service_record = autoclass('java.util.UUID').fromString("00001101-0000-1000-8000-00805F9B34FB")
 138          self.buffered_input_stream    = autoclass('java.io.BufferedInputStream')
 139  
 140      def connect(self, device_address=None):
 141          self.rfcomm_socket = self.remote_device.createRfcommSocketToServiceRecord(self.bt_rfcomm_service_record)
 142  
 143      def bt_enabled(self):
 144          return self.bt_adapter.getDefaultAdapter().isEnabled()
 145  
 146      def get_paired_devices(self):
 147          if self.bt_enabled():
 148              return self.bt_adapter.getDefaultAdapter().getBondedDevices()
 149          else:
 150              RNS.log("Could not query paired devices, Bluetooth is disabled", RNS.LOG_EXTREME)
 151              return []
 152  
 153      def get_potential_devices(self):
 154          potential_devices = []
 155          for device in self.get_paired_devices():
 156              if self.target_device_address != None:
 157                  if str(device.getAddress()).replace(":", "").lower() == str(self.target_device_address).replace(":", "").lower():
 158                      if self.target_device_name == None:
 159                          potential_devices.append(device)
 160                      else:
 161                          if device.getName().lower() == self.target_device_name.lower():
 162                              potential_devices.append(device)
 163  
 164              elif self.target_device_name != None:
 165                  if device.getName().lower() == self.target_device_name.lower():
 166                      potential_devices.append(device)
 167  
 168              else:
 169                  if device.getName().lower().startswith("rnode "):
 170                      potential_devices.append(device)
 171  
 172          return potential_devices
 173  
 174      def connect_any_device(self):
 175          if (self.rfcomm_socket != None and not self.rfcomm_socket.isConnected()) or self.rfcomm_socket == None:
 176              self.connection_failed = False
 177              if len(self.potential_remote_devices) == 0:
 178                  self.potential_remote_devices = self.get_potential_devices()
 179                  if len(self.potential_remote_devices) == 0:
 180                      RNS.log("No suitable bluetooth devices available, can't connect", RNS.LOG_DEBUG)
 181                      return
 182  
 183              while not self.connected and len(self.potential_remote_devices) > 0:
 184                  device = self.potential_remote_devices.pop()
 185                  try:
 186                      self.rfcomm_socket = device.createRfcommSocketToServiceRecord(self.bt_rfcomm_service_record)
 187                      if self.rfcomm_socket == None:
 188                          raise IOError("Bluetooth stack returned no socket object")
 189                      else:
 190                          if not self.rfcomm_socket.isConnected():
 191                              try:
 192                                  self.rfcomm_socket.connect()
 193                                  self.rfcomm_reader = self.buffered_input_stream(self.rfcomm_socket.getInputStream(), 1024)
 194                                  self.rfcomm_writer = self.rfcomm_socket.getOutputStream()
 195                                  self.connected = True
 196                                  self.connected_device = device
 197                                  RNS.log("Bluetooth device "+str(self.connected_device.getName())+" "+str(self.connected_device.getAddress())+" connected.")
 198                              except Exception as e:
 199                                  raise IOError("The Bluetooth RFcomm socket could not be connected: "+str(e))
 200  
 201                  except Exception as e:
 202                      RNS.log("Could not create and connect Bluetooth RFcomm socket for "+str(device.getName())+" "+str(device.getAddress()), RNS.LOG_EXTREME)
 203                      RNS.log("The contained exception was: "+str(e), RNS.LOG_EXTREME)
 204  
 205      def close(self):
 206          if self.connected:
 207              if self.rfcomm_reader != None:
 208                  self.rfcomm_reader.close()
 209                  self.rfcomm_reader = None
 210              
 211              if self.rfcomm_writer != None:
 212                  self.rfcomm_writer.close()
 213                  self.rfcomm_writer = None
 214  
 215              if self.rfcomm_socket != None:
 216                  self.rfcomm_socket.close()
 217  
 218              self.connected = False
 219              self.connected_device = None
 220              self.potential_remote_devices = []
 221  
 222  
 223      def read(self, len = None):
 224          if self.connection_failed:
 225              raise IOError("Bluetooth connection failed")
 226          else:
 227              if self.connected and self.rfcomm_reader != None:
 228                  available = self.rfcomm_reader.available()
 229                  if available > 0:
 230                      if hasattr(self.rfcomm_reader, "readNBytes"):
 231                          return self.rfcomm_reader.readNBytes(available)
 232                      else:
 233                          # Compatibility mode for older android versions lacking readNBytes
 234                          rb = self.rfcomm_reader.read().to_bytes(1, "big")
 235                          return rb
 236                  else:
 237                      return bytes([])
 238              else:
 239                  raise IOError("No RFcomm socket available")
 240  
 241      def write(self, data):
 242          try:
 243              self.rfcomm_writer.write(data)
 244              self.rfcomm_writer.flush()
 245              return len(data)
 246          except Exception as e:
 247              RNS.log("Bluetooth connection failed for "+str(self), RNS.LOG_ERROR)
 248              self.connection_failed = True
 249              return 0
 250  
 251  
 252  class RNodeInterface(Interface):
 253      MAX_CHUNK = 32768
 254      DEFAULT_IFAC_SIZE = 8
 255  
 256      FREQ_MIN = 137000000
 257      FREQ_MAX = 3000000000
 258  
 259      RSSI_OFFSET = 157
 260  
 261      CALLSIGN_MAX_LEN    = 32
 262  
 263      REQUIRED_FW_VER_MAJ = 1
 264      REQUIRED_FW_VER_MIN = 52
 265  
 266      RECONNECT_WAIT = 5
 267      PORT_IO_TIMEOUT = 3
 268  
 269      Q_SNR_MIN_BASE = -9
 270      Q_SNR_MAX      = 6
 271      Q_SNR_STEP     = 2
 272  
 273      BATTERY_STATE_UNKNOWN     = 0x00
 274      BATTERY_STATE_DISCHARGING = 0x01
 275      BATTERY_STATE_CHARGING    = 0x02
 276      BATTERY_STATE_CHARGED     = 0x03
 277  
 278      DISPLAY_READ_INTERVAL     = 1.0
 279  
 280      @classmethod
 281      def bluetooth_control(device_serial = None, port = None, enable_bluetooth = False, disable_bluetooth = False, pairing_mode = False):
 282          if (port != None or device_serial != None) and (enable_bluetooth or disable_bluetooth or pairing_mode):
 283              serial = None
 284              bluetooth_state = None
 285              if pairing_mode:
 286                  bluetooth_state = 0x01
 287              elif enable_bluetooth:
 288                  bluetooth_state = 0x01
 289              elif disable_bluetooth:
 290                  bluetooth_state = 0x00
 291  
 292              if port != None:
 293                  RNS.log("Opening serial port "+port+"...")
 294                  # Get device parameters
 295                  from usb4a import usb
 296                  device = usb.get_usb_device(port)
 297                  if device:
 298                      vid = device.getVendorId()
 299                      pid = device.getProductId()
 300  
 301                      # Driver overrides for speficic chips
 302                      from usbserial4a import serial4a as pyserial
 303                      proxy = pyserial.get_serial_port
 304                      if vid == 0x1A86 and pid == 0x55D4:
 305                          # Force CDC driver for Qinheng CH34x
 306                          RNS.log("Using CDC driver for "+RNS.hexrep(vid)+":"+RNS.hexrep(pid), RNS.LOG_DEBUG)
 307                          from usbserial4a.cdcacmserial4a import CdcAcmSerial
 308                          proxy = CdcAcmSerial
 309  
 310                      serial = proxy(
 311                          port,
 312                          baudrate = 115200,
 313                          bytesize = 8,
 314                          parity = "N",
 315                          stopbits = 1,
 316                          xonxoff = False,
 317                          rtscts = False,
 318                          timeout = None,
 319                          inter_byte_timeout = None,
 320                          # write_timeout = wtimeout,
 321                          dsrdtr = False,
 322                      )
 323  
 324                      if vid == 0x0403:
 325                          # Hardware parameters for FTDI devices @ 115200 baud
 326                          serial.DEFAULT_READ_BUFFER_SIZE = 16 * 1024
 327                          serial.USB_READ_TIMEOUT_MILLIS = 100
 328                          serial.timeout = 0.1
 329                      elif vid == 0x10C4:
 330                          # Hardware parameters for SiLabs CP210x @ 115200 baud
 331                          serial.DEFAULT_READ_BUFFER_SIZE = 64
 332                          serial.USB_READ_TIMEOUT_MILLIS = 12
 333                          serial.timeout = 0.012
 334                      elif vid == 0x1A86 and pid == 0x55D4:
 335                          # Hardware parameters for Qinheng CH34x @ 115200 baud
 336                          serial.DEFAULT_READ_BUFFER_SIZE = 64
 337                          serial.USB_READ_TIMEOUT_MILLIS = 12
 338                          serial.timeout = 0.1
 339                      else:
 340                          # Default values
 341                          serial.DEFAULT_READ_BUFFER_SIZE = 1 * 1024
 342                          serial.USB_READ_TIMEOUT_MILLIS = 100
 343                          serial.timeout = 0.1
 344  
 345              elif device_serial != None:
 346                  serial = device_serial
 347  
 348              if serial != None:
 349                  if serial.is_open:
 350                      kiss_command = bytes([KISS.FEND, KISS.CMD_BT_CTRL, bluetooth_state, KISS.FEND])
 351                      serial.write(kiss_command)
 352                      if pairing_mode:
 353                          kiss_command = bytes([KISS.FEND, KISS.CMD_BT_CTRL, 0x02, KISS.FEND])
 354                          serial.write(kiss_command)
 355  
 356              if port != None:
 357                  serial.close()
 358  
 359  
 360      def __init__(self, owner, configuration):
 361          c = Interface.get_config_obj(configuration)
 362          name = c["name"]
 363          allow_bluetooth = c.as_bool("allow_bluetooth") if "allow_bluetooth" in c else False
 364          target_device_name = c["target_device_name"] if "target_device_name" in c else None
 365          target_device_address = c["target_device_address"] if "target_device_address" in c else None
 366          ble_name = c["ble_name"] if "ble_name" in c else None
 367          ble_addr = c["ble_addr"] if "ble_addr" in c else None
 368          tcp_host = c["tcp_host"] if "tcp_host" in c else None
 369          force_ble = c["force_ble"] if "force_ble" in c else False
 370          force_tcp = c["force_tcp"] if "force_tcp" in c else False
 371          frequency = int(c["frequency"]) if "frequency" in c else 0
 372          bandwidth = int(c["bandwidth"]) if "bandwidth" in c else 0
 373          txpower = int(c["txpower"]) if "txpower" in c else 0
 374          sf = int(c["spreadingfactor"]) if "spreadingfactor" in c else 0
 375          cr = int(c["codingrate"]) if "codingrate" in c else 0
 376          flow_control = c.as_bool("flow_control") if "flow_control" in c else False
 377          id_interval = int(c["id_interval"]) if "id_interval" in c and c["id_interval"] != None else None
 378          id_callsign = c["id_callsign"] if "id_callsign" in c else None
 379          st_alock = float(c["airtime_limit_short"]) if "airtime_limit_short" in c and c["airtime_limit_short"] != None else None
 380          lt_alock = float(c["airtime_limit_long"]) if "airtime_limit_long" in c and c["airtime_limit_long"] != None else None
 381          port = c["port"] if "port" in c else None
 382  
 383          import importlib.util
 384          if RNS.vendor.platformutils.is_android():
 385              self.on_android  = True
 386              if importlib.util.find_spec('usbserial4a') != None:
 387                  if importlib.util.find_spec('jnius') == None:
 388                      RNS.log("Could not load jnius API wrapper for Android, RNode interface cannot be created.", RNS.LOG_CRITICAL)
 389                      RNS.log("This probably means you are trying to use an USB-based interface from within Termux or similar.", RNS.LOG_CRITICAL)
 390                      RNS.log("This is currently not possible, due to this environment limiting access to the native Android APIs.", RNS.LOG_CRITICAL)
 391                      RNS.panic()
 392  
 393                  from usbserial4a import serial4a as serial
 394                  self.parity = "N"
 395  
 396                  self.bt_target_device_name = target_device_name
 397                  self.bt_target_device_address = target_device_address
 398                  if allow_bluetooth:
 399                      self.bt_manager = AndroidBluetoothManager(
 400                          owner = self,
 401                          target_device_name = self.bt_target_device_name,
 402                          target_device_address = self.bt_target_device_address
 403                      )
 404  
 405                  else:
 406                      self.bt_manager = None
 407              
 408              else:
 409                  RNS.log("Could not load USB serial module for Android, RNode interface cannot be created.", RNS.LOG_CRITICAL)
 410                  RNS.log("You can install this module by issuing: pip install usbserial4a", RNS.LOG_CRITICAL)
 411                  RNS.panic()
 412          else:
 413              raise SystemError("Android-specific interface was used on non-Android OS")
 414  
 415          super().__init__()
 416  
 417          self.HW_MTU = 508
 418          
 419          self.pyserial    = serial
 420          self.serial      = None
 421          self.owner       = owner
 422          self.name        = name
 423          self.port        = port
 424          self.speed       = 115200
 425          self.databits    = 8
 426          self.stopbits    = 1
 427          self.timeout     = 150
 428          self.online      = False
 429          self.detached    = False
 430          self.hw_errors   = []
 431          self.allow_bluetooth = allow_bluetooth
 432  
 433          self.use_ble     = False
 434          self.ble_name    = ble_name
 435          self.ble_addr    = ble_addr
 436          self.ble         = None
 437          self.ble_rx_lock = threading.Lock()
 438          self.ble_tx_lock = threading.Lock()
 439          self.ble_rx_queue= b""
 440          self.ble_tx_queue= b""
 441  
 442          self.tcp         = None
 443          self.use_tcp     = False
 444          self.tcp_host    = tcp_host
 445          self.tcp_rx_queue= b""
 446          self.tcp_tx_queue= b""
 447          self.tcp_rx_lock = threading.Lock()
 448          self.tcp_tx_lock = threading.Lock()
 449  
 450          self.frequency   = frequency
 451          self.bandwidth   = bandwidth
 452          self.txpower     = txpower
 453          self.sf          = sf
 454          self.cr          = cr
 455          self.state       = KISS.RADIO_STATE_OFF
 456          self.bitrate     = 0
 457          self.st_alock    = st_alock
 458          self.lt_alock    = lt_alock
 459          self.cpu_temp    = None
 460          self.platform    = None
 461          self.display     = None
 462          self.mcu         = None
 463          self.detected    = False
 464          self.firmware_ok = False
 465          self.maj_version = 0
 466          self.min_version = 0
 467  
 468          self.last_id     = 0
 469          self.first_tx    = None
 470          self.reconnect_w = RNodeInterface.RECONNECT_WAIT
 471          self.reconnect_lock = threading.Lock()
 472          self.awaiting_ble_reset = False
 473  
 474          self.r_frequency = None
 475          self.r_bandwidth = None
 476          self.r_txpower   = None
 477          self.r_sf        = None
 478          self.r_cr        = None
 479          self.r_state     = None
 480          self.r_lock      = None
 481          self.r_stat_rx   = None
 482          self.r_stat_tx   = None
 483          self.r_stat_rssi = None
 484          self.r_stat_snr  = None
 485          self.r_st_alock  = None
 486          self.r_lt_alock  = None
 487          self.r_random    = None
 488          self.r_airtime_short      = 0.0
 489          self.r_airtime_long       = 0.0
 490          self.r_channel_load_short = 0.0
 491          self.r_channel_load_long  = 0.0
 492          self.r_symbol_time_ms     = None
 493          self.r_symbol_rate        = None
 494          self.r_preamble_symbols   = None
 495          self.r_premable_time_ms   = None
 496          self.r_csma_slot_time_ms  = None
 497          self.r_csma_difs_ms       = None
 498          self.r_csma_cw_band       = None
 499          self.r_csma_cw_min        = None
 500          self.r_csma_cw_max        = None
 501          self.r_current_rssi       = None
 502          self.r_noise_floor        = None
 503          self.r_interference       = None
 504          self.r_interference_l     = None
 505          self.r_temperature        = None
 506  
 507          self.r_battery_state = RNodeInterface.BATTERY_STATE_UNKNOWN
 508          self.r_battery_percent = 0
 509          self.r_framebuffer = b""
 510          self.r_framebuffer_readtime = 0
 511          self.r_framebuffer_latency = 0
 512          self.r_disp = b""
 513          self.r_disp_readtime = 0
 514          self.r_disp_latency = 0
 515  
 516          self.should_read_display = False
 517          self.read_display_interval = RNodeInterface.DISPLAY_READ_INTERVAL
 518  
 519          self.packet_queue    = []
 520          self.flow_control    = flow_control
 521          self.interface_ready = False
 522          self.announce_rate_target = None
 523          self.last_port_io = 0
 524          self.port_io_timeout = RNodeInterface.PORT_IO_TIMEOUT
 525          self.last_imagedata = None
 526  
 527          if force_ble or self.ble_addr != None or self.ble_name != None: self.use_ble = True
 528          if force_tcp or self.tcp_host != None:                          self.use_tcp = True
 529  
 530          self.validcfg  = True
 531          if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX):
 532              RNS.log("Invalid frequency configured for "+str(self), RNS.LOG_ERROR)
 533              self.validcfg = False
 534  
 535          if (self.txpower < 0 or self.txpower > 37):
 536              RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR)
 537              self.validcfg = False
 538  
 539          if (self.bandwidth < 7800 or self.bandwidth > 500000):
 540              RNS.log("Invalid bandwidth configured for "+str(self), RNS.LOG_ERROR)
 541              self.validcfg = False
 542  
 543          if (self.sf < 5 or self.sf > 12):
 544              RNS.log("Invalid spreading factor configured for "+str(self), RNS.LOG_ERROR)
 545              self.validcfg = False
 546  
 547          if (self.cr < 5 or self.cr > 8):
 548              RNS.log("Invalid coding rate configured for "+str(self), RNS.LOG_ERROR)
 549              self.validcfg = False
 550  
 551          if (self.st_alock and (self.st_alock < 0.0 or self.st_alock > 100.0)):
 552              RNS.log("Invalid short-term airtime limit configured for "+str(self), RNS.LOG_ERROR)
 553              self.validcfg = False
 554  
 555          if (self.lt_alock and (self.lt_alock < 0.0 or self.lt_alock > 100.0)):
 556              RNS.log("Invalid long-term airtime limit configured for "+str(self), RNS.LOG_ERROR)
 557              self.validcfg = False
 558  
 559          if id_interval != None and id_callsign != None:
 560              if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN):
 561                  self.should_id = True
 562                  self.id_callsign = id_callsign.encode("utf-8")
 563                  self.id_interval = id_interval
 564              else:
 565                  RNS.log("The encoded ID callsign for "+str(self)+" exceeds the max length of "+str(RNodeInterface.CALLSIGN_MAX_LEN)+" bytes.", RNS.LOG_ERROR)
 566                  self.validcfg = False
 567          else:
 568              self.id_interval = None
 569              self.id_callsign = None
 570  
 571          if (not self.validcfg):
 572              raise ValueError("The configuration for "+str(self)+" contains errors, interface is offline")
 573  
 574          try:
 575              self.open_port()
 576  
 577              if self.serial != None:
 578                  if self.serial.is_open: self.configure_device()
 579                  else: raise IOError("Could not open serial port")
 580              elif self.bt_manager != None:
 581                  if self.bt_manager.connected:
 582                      self.configure_device()
 583                  else:
 584                      raise IOError("Could not connect to any Bluetooth devices")
 585              else:
 586                  raise IOError("Neither serial port nor Bluetooth devices available")
 587  
 588          except Exception as e:
 589              RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
 590              RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
 591              if len(self.hw_errors) == 0:
 592                  RNS.log("Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR)
 593                  thread = threading.Thread(target=self.reconnect_port, daemon=True).start()
 594  
 595  
 596      def read_mux(self, len=None):
 597          if self.serial != None: return self.serial.read()
 598          elif self.bt_manager != None: return self.bt_manager.read()
 599          else: raise IOError("No ports available for reading")
 600  
 601      def write_mux(self, data):
 602          if self.serial != None:
 603              written = self.serial.write(data)
 604              self.last_port_io = time.time()
 605              return written
 606          elif self.bt_manager != None:
 607              written = self.bt_manager.write(data)
 608              if (written == len(data)):
 609                  self.last_port_io = time.time()
 610              return written
 611          else:
 612              raise IOError("No ports available for writing")
 613  
 614      def reset_ble(self):
 615          if not self.awaiting_ble_reset: return
 616          else:
 617              RNS.log(f"Clearing previous connection instance: "+str(self.ble), RNS.LOG_DEBUG)
 618              self.ble = None
 619              self.serial = None
 620              self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
 621              self.serial = self.ble
 622              self.awaiting_ble_reset = False
 623              RNS.log(f"New connection instance: "+str(self.ble), RNS.LOG_DEBUG)
 624          
 625      def open_port(self):
 626          if not self.use_ble and not self.use_tcp:
 627              if self.port != None:
 628                  RNS.log("Opening serial port "+self.port+"...")
 629                  # Get device parameters
 630                  from usb4a import usb
 631                  device = usb.get_usb_device(self.port)
 632                  if device:
 633                      vid = device.getVendorId()
 634                      pid = device.getProductId()
 635  
 636                      # Driver overrides for speficic chips
 637                      proxy = self.pyserial.get_serial_port
 638                      if vid == 0x1A86 and pid == 0x55D4:
 639                          # Force CDC driver for Qinheng CH34x
 640                          RNS.log(str(self)+" using CDC driver for "+RNS.hexrep(vid)+":"+RNS.hexrep(pid), RNS.LOG_DEBUG)
 641                          from usbserial4a.cdcacmserial4a import CdcAcmSerial
 642                          proxy = CdcAcmSerial
 643  
 644                      self.serial = proxy(
 645                          self.port,
 646                          baudrate = self.speed,
 647                          bytesize = self.databits,
 648                          parity = self.parity,
 649                          stopbits = self.stopbits,
 650                          xonxoff = False,
 651                          rtscts = False,
 652                          timeout = None,
 653                          inter_byte_timeout = None,
 654                          # write_timeout = wtimeout,
 655                          dsrdtr = False,
 656                      )
 657  
 658                      if vid == 0x0403:
 659                          # Hardware parameters for FTDI devices @ 115200 baud
 660                          self.serial.DEFAULT_READ_BUFFER_SIZE = 16 * 1024
 661                          self.serial.USB_READ_TIMEOUT_MILLIS = 100
 662                          self.serial.timeout = 0.1
 663                      elif vid == 0x10C4:
 664                          # Hardware parameters for SiLabs CP210x @ 115200 baud
 665                          self.serial.DEFAULT_READ_BUFFER_SIZE = 64 
 666                          self.serial.USB_READ_TIMEOUT_MILLIS = 12
 667                          self.serial.timeout = 0.012
 668                      elif vid == 0x1A86 and pid == 0x55D4:
 669                          # Hardware parameters for Qinheng CH34x @ 115200 baud
 670                          self.serial.DEFAULT_READ_BUFFER_SIZE = 64
 671                          self.serial.USB_READ_TIMEOUT_MILLIS = 12
 672                          self.serial.timeout = 0.1
 673                      else:
 674                          # Default values
 675                          self.serial.DEFAULT_READ_BUFFER_SIZE = 1 * 1024
 676                          self.serial.USB_READ_TIMEOUT_MILLIS = 100
 677                          self.serial.timeout = 0.1
 678  
 679                      RNS.log(str(self)+" USB read buffer size set to "+RNS.prettysize(self.serial.DEFAULT_READ_BUFFER_SIZE), RNS.LOG_DEBUG)
 680                      RNS.log(str(self)+" USB read timeout set to "+str(self.serial.USB_READ_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG)
 681                      RNS.log(str(self)+" USB write timeout set to "+str(self.serial.USB_WRITE_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG)
 682  
 683              elif self.allow_bluetooth:
 684                  if self.bt_manager == None:
 685                      self.bt_manager = AndroidBluetoothManager(
 686                          owner = self,
 687                          target_device_name = self.bt_target_device_name,
 688                          target_device_address = self.bt_target_device_address
 689                      )
 690  
 691                  if self.bt_manager != None:
 692                      self.bt_manager.connect_any_device()
 693  
 694          elif self.use_ble:
 695              if self.ble == None:
 696                  self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
 697                  self.serial = self.ble
 698  
 699              open_time = time.time()
 700              while not self.ble.connected and time.time() < open_time + self.ble.CONNECT_TIMEOUT:
 701                  time.sleep(1)
 702  
 703          elif self.use_tcp:
 704              RNS.log(f"Opening TCP connection for {self}...")
 705              if self.tcp != None and self.tcp.running == False:
 706                  self.tcp.close()
 707                  self.tcp.cleanup()
 708                  self.tcp = None
 709  
 710              if self.tcp == None:
 711                  self.tcp = TCPConnection(owner=self, target_host=self.tcp_host)
 712                  self.serial = self.tcp
 713  
 714              open_time = time.time()
 715              while not self.tcp.connected and time.time() < open_time + self.tcp.CONNECT_TIMEOUT:
 716                  time.sleep(1)
 717  
 718          else:
 719              raise TypeError("No valid device connection type defined for RNode interface")
 720  
 721  
 722      def configure_device(self):
 723          self.resetRadioState()
 724          sleep(2.0)
 725          thread = threading.Thread(target=self.readLoop, daemon=True).start()
 726  
 727          self.detect()
 728          if self.use_tcp:
 729              tcp_detect_timeout = 5.0
 730              detect_time = time.time()
 731              while not self.detected and time.time() < detect_time + tcp_detect_timeout: time.sleep(0.1)
 732              if not self.detected: RNS.log(f"RNode detect timed out over TCP", RNS.LOG_ERROR)
 733          elif self.use_ble:
 734              ble_detect_timeout = 5
 735              detect_time = time.time()
 736              while not self.detected and time.time() < detect_time + ble_detect_timeout: time.sleep(0.1)
 737              if self.detected: detect_time = RNS.prettytime(time.time()-detect_time)
 738              else: RNS.log(f"RNode detect timed out over {self.port}", RNS.LOG_ERROR)
 739          else:
 740              sleep(0.2)
 741  
 742          if not self.detected:
 743              raise IOError("Could not detect device")
 744          else:
 745              if self.platform == KISS.PLATFORM_ESP32 or self.platform == KISS.PLATFORM_NRF52:
 746                  self.display = True
 747  
 748          if not self.firmware_ok:
 749              raise IOError("Invalid device firmware")
 750  
 751          if self.serial != None and self.port != None:
 752              self.timeout = 200
 753              RNS.log(f"Serial port {self.port} is now open")
 754  
 755          if self.bt_manager != None and self.bt_manager.connected:
 756              self.timeout = 1500
 757              RNS.log(f"Bluetooth connection to RNode now open")
 758  
 759          if self.ble != None and self.ble.connected:
 760              self.timeout = 1500
 761              RNS.log(f"BLE connection {self.port} to RNode now open")
 762  
 763          if self.tcp != None and self.tcp.connected:
 764              self.timeout = 1500
 765              RNS.log(f"TCP connection tcp://{self.tcp_host} to RNode now open")
 766  
 767          RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
 768          self.initRadio()
 769          if (self.validateRadioState()):
 770              self.interface_ready = True
 771              RNS.log(str(self)+" is configured and powered up")
 772              sleep(0.3)
 773              self.online = True
 774          else:
 775              RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR)
 776              RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR)
 777              RNS.log("Aborting RNode startup", RNS.LOG_ERROR)
 778              self.hw_errors.append({"error": KISS.ERROR_INVALID_CONFIG, "description": "The configuration parameters were not validated by the device. Make sure that the device actually supports the TX power, frequency, bandwidth, spreading factor and coding rate you configured."})
 779              
 780              if self.serial != None:
 781                  self.serial.close()
 782              if self.bt_manager != None:
 783                  self.bt_manager.close()
 784  
 785              raise IOError("RNode interface did not pass configuration validation")
 786              
 787  
 788      def initRadio(self):
 789          self.setFrequency()
 790          time.sleep(0.15)
 791  
 792          self.setBandwidth()
 793          time.sleep(0.15)
 794          
 795          self.setTXPower()
 796          time.sleep(0.15)
 797          
 798          self.setSpreadingFactor()
 799          time.sleep(0.15)
 800          
 801          self.setCodingRate()
 802          time.sleep(0.15)
 803  
 804          self.setSTALock()
 805          time.sleep(0.15)
 806          
 807          self.setLTALock()
 808          time.sleep(0.15)
 809          
 810          self.setRadioState(KISS.RADIO_STATE_ON)
 811          time.sleep(0.15)
 812  
 813          if self.use_ble:
 814              time.sleep(1)
 815  
 816      def detect(self):
 817          kiss_command = bytes([KISS.FEND, KISS.CMD_DETECT, KISS.DETECT_REQ, KISS.FEND, KISS.CMD_FW_VERSION, 0x00, KISS.FEND, KISS.CMD_PLATFORM, 0x00, KISS.FEND, KISS.CMD_MCU, 0x00, KISS.FEND])
 818          written = self.write_mux(kiss_command)
 819          if written != len(kiss_command):
 820              raise IOError("An IO error occurred while detecting hardware for "+str(self))
 821  
 822      def leave(self):
 823          kiss_command = bytes([KISS.FEND, KISS.CMD_LEAVE, 0xFF, KISS.FEND])
 824          written = self.write_mux(kiss_command)
 825          if written != len(kiss_command):
 826              raise IOError("An IO error occurred while sending host left command to device")
 827      
 828      def enable_bluetooth(self):
 829          kiss_command = bytes([KISS.FEND, KISS.CMD_BT_CTRL, 0x01, KISS.FEND])
 830          written = self.write_mux(kiss_command)
 831          if written != len(kiss_command):
 832              raise IOError("An IO error occurred while sending bluetooth enable command to device")
 833  
 834      def disable_bluetooth(self):
 835          kiss_command = bytes([KISS.FEND, KISS.CMD_BT_CTRL, 0x00, KISS.FEND])
 836          written = self.write_mux(kiss_command)
 837          if written != len(kiss_command):
 838              raise IOError("An IO error occurred while sending bluetooth disable command to device")
 839  
 840      def bluetooth_pair(self):
 841          kiss_command = bytes([KISS.FEND, KISS.CMD_BT_CTRL, 0x02, KISS.FEND])
 842          written = self.write_mux(kiss_command)
 843          if written != len(kiss_command):
 844              raise IOError("An IO error occurred while sending bluetooth pair command to device")
 845  
 846      def enable_external_framebuffer(self):
 847          if self.display != None:
 848              kiss_command = bytes([KISS.FEND, KISS.CMD_FB_EXT, 0x01, KISS.FEND])
 849              written = self.write_mux(kiss_command)
 850              if written != len(kiss_command):
 851                  raise IOError("An IO error occurred while enabling external framebuffer on device")
 852  
 853      def disable_external_framebuffer(self):
 854          if self.display != None:
 855              kiss_command = bytes([KISS.FEND, KISS.CMD_FB_EXT, 0x00, KISS.FEND])
 856              written = self.write_mux(kiss_command)
 857              if written != len(kiss_command):
 858                  raise IOError("An IO error occurred while disabling external framebuffer on device")
 859  
 860      FB_PIXEL_WIDTH     = 64
 861      FB_BITS_PER_PIXEL  = 1
 862      FB_PIXELS_PER_BYTE = 8//FB_BITS_PER_PIXEL
 863      FB_BYTES_PER_LINE  = FB_PIXEL_WIDTH//FB_PIXELS_PER_BYTE
 864      def display_image(self, imagedata):
 865          self.last_imagedata = imagedata
 866          if self.display != None:
 867              lines = len(imagedata)//8
 868              for line in range(lines):
 869                  line_start = line*RNodeInterface.FB_BYTES_PER_LINE
 870                  line_end   = line_start+RNodeInterface.FB_BYTES_PER_LINE
 871                  line_data = bytes(imagedata[line_start:line_end])
 872                  self.write_framebuffer(line, line_data)
 873  
 874      def write_framebuffer(self, line, line_data):
 875          if self.display != None:
 876              line_byte = line.to_bytes(1, byteorder="big", signed=False)
 877              data = line_byte+line_data
 878              escaped_data = KISS.escape(data)
 879              kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FB_WRITE])+escaped_data+bytes([KISS.FEND])
 880              
 881              written = self.write_mux(kiss_command)
 882              if written != len(kiss_command):
 883                  raise IOError("An IO error occurred while writing framebuffer data device")
 884  
 885      def read_framebuffer(self):
 886          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FB_READ])+bytes([0x01])+bytes([KISS.FEND])
 887          written = self.serial.write(kiss_command)
 888          self.r_framebuffer_readtime = time.time()
 889          if written != len(kiss_command):
 890              raise IOError("An IO error occurred while sending framebuffer read command")
 891  
 892      def read_display(self):
 893          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_DISP_READ])+bytes([0x01])+bytes([KISS.FEND])
 894          written = self.serial.write(kiss_command)
 895          self.r_disp_readtime = time.time()
 896          if written != len(kiss_command):
 897              raise IOError("An IO error occurred while sending display read command")
 898  
 899      def _read_display_job(self):
 900          while self.should_read_display:
 901              self.read_display()
 902              time.sleep(self.read_display_interval)
 903  
 904      def start_display_updates(self):
 905          if not self.should_read_display:
 906              self.should_read_display = True
 907              threading.Thread(target=self._read_display_job, daemon=True).start()
 908  
 909      def stop_display_updates(self):
 910          self.should_read_display = False
 911  
 912      def hard_reset(self):
 913          kiss_command = bytes([KISS.FEND, KISS.CMD_RESET, 0xf8, KISS.FEND])
 914          written = self.write_mux(kiss_command)
 915          if written != len(kiss_command):
 916              raise IOError("An IO error occurred while restarting device")
 917          sleep(4.0);
 918  
 919      def setFrequency(self):
 920          c1 = self.frequency >> 24
 921          c2 = self.frequency >> 16 & 0xFF
 922          c3 = self.frequency >> 8 & 0xFF
 923          c4 = self.frequency & 0xFF
 924          data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4]))
 925  
 926          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FREQUENCY])+data+bytes([KISS.FEND])
 927          written = self.write_mux(kiss_command)
 928          if written != len(kiss_command):
 929              raise IOError("An IO error occurred while configuring frequency for "+str(self))
 930  
 931      def setBandwidth(self):
 932          c1 = self.bandwidth >> 24
 933          c2 = self.bandwidth >> 16 & 0xFF
 934          c3 = self.bandwidth >> 8 & 0xFF
 935          c4 = self.bandwidth & 0xFF
 936          data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4]))
 937  
 938          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_BANDWIDTH])+data+bytes([KISS.FEND])
 939          written = self.write_mux(kiss_command)
 940          if written != len(kiss_command):
 941              raise IOError("An IO error occurred while configuring bandwidth for "+str(self))
 942  
 943      def setTXPower(self):
 944          txp = bytes([self.txpower])
 945          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXPOWER])+txp+bytes([KISS.FEND])
 946          written = self.write_mux(kiss_command)
 947          if written != len(kiss_command):
 948              raise IOError("An IO error occurred while configuring TX power for "+str(self))
 949  
 950      def setSpreadingFactor(self):
 951          sf = bytes([self.sf])
 952          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SF])+sf+bytes([KISS.FEND])
 953          written = self.write_mux(kiss_command)
 954          if written != len(kiss_command):
 955              raise IOError("An IO error occurred while configuring spreading factor for "+str(self))
 956  
 957      def setCodingRate(self):
 958          cr = bytes([self.cr])
 959          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_CR])+cr+bytes([KISS.FEND])
 960          written = self.write_mux(kiss_command)
 961          if written != len(kiss_command):
 962              raise IOError("An IO error occurred while configuring coding rate for "+str(self))
 963  
 964      def setSTALock(self):
 965          if self.st_alock != None:
 966              at = int(self.st_alock*100)
 967              c1 = at >> 8 & 0xFF
 968              c2 = at & 0xFF
 969              data = KISS.escape(bytes([c1])+bytes([c2]))
 970  
 971              kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_ST_ALOCK])+data+bytes([KISS.FEND])
 972              written = self.write_mux(kiss_command)
 973              if written != len(kiss_command):
 974                  raise IOError("An IO error occurred while configuring short-term airtime limit for "+str(self))
 975  
 976      def setLTALock(self):
 977          if self.lt_alock != None:
 978              at = int(self.lt_alock*100)
 979              c1 = at >> 8 & 0xFF
 980              c2 = at & 0xFF
 981              data = KISS.escape(bytes([c1])+bytes([c2]))
 982  
 983              kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_LT_ALOCK])+data+bytes([KISS.FEND])
 984              written = self.write_mux(kiss_command)
 985              if written != len(kiss_command):
 986                  raise IOError("An IO error occurred while configuring long-term airtime limit for "+str(self))
 987  
 988      def setRadioState(self, state):
 989          self.state = state
 990          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
 991          written = self.write_mux(kiss_command)
 992          if written != len(kiss_command):
 993              raise IOError("An IO error occurred while configuring radio state for "+str(self))
 994  
 995      def validate_firmware(self):
 996          if (self.maj_version > RNodeInterface.REQUIRED_FW_VER_MAJ):
 997              self.firmware_ok = True
 998          else:
 999              if (self.maj_version >= RNodeInterface.REQUIRED_FW_VER_MAJ):
1000                  if (self.min_version >= RNodeInterface.REQUIRED_FW_VER_MIN):
1001                      self.firmware_ok = True
1002          
1003          if self.firmware_ok:
1004              return
1005  
1006          RNS.log("The firmware version of the connected RNode is "+str(self.maj_version)+"."+str(self.min_version), RNS.LOG_ERROR)
1007          RNS.log("This version of Reticulum requires at least version "+str(RNodeInterface.REQUIRED_FW_VER_MAJ)+"."+str(RNodeInterface.REQUIRED_FW_VER_MIN), RNS.LOG_ERROR)
1008          RNS.log("Please update your RNode firmware with rnodeconf from https://github.com/markqvist/reticulum/")
1009          error_description  = "The firmware version of the connected RNode is "+str(self.maj_version)+"."+str(self.min_version)+". "
1010          error_description += "This version of Reticulum requires at least version "+str(RNodeInterface.REQUIRED_FW_VER_MAJ)+"."+str(RNodeInterface.REQUIRED_FW_VER_MIN)+". "
1011          error_description += "Please update your RNode firmware with rnodeconf from: https://github.com/markqvist/rnodeconfigutil/"
1012          self.hw_errors.append({"error": KISS.ERROR_INVALID_FIRMWARE, "description": error_description})
1013  
1014  
1015      def validateRadioState(self):
1016          RNS.log("Waiting for radio configuration validation for "+str(self)+"...", RNS.LOG_VERBOSE)
1017          if not self.platform == KISS.PLATFORM_ESP32: sleep(1.00);
1018          else: sleep(2.00);
1019  
1020          self.validcfg = True
1021          if (self.r_frequency != None and abs(self.frequency - int(self.r_frequency)) > 100):
1022              RNS.log("Frequency mismatch", RNS.LOG_ERROR)
1023              self.validcfg = False
1024          if (self.bandwidth != self.r_bandwidth):
1025              RNS.log("Bandwidth mismatch", RNS.LOG_ERROR)
1026              self.validcfg = False
1027          if (self.txpower != self.r_txpower):
1028              RNS.log("TX power mismatch", RNS.LOG_ERROR)
1029              self.validcfg = False
1030          if (self.sf != self.r_sf):
1031              RNS.log("Spreading factor mismatch", RNS.LOG_ERROR)
1032              self.validcfg = False
1033          if (self.state != self.r_state):
1034              RNS.log("Radio state mismatch", RNS.LOG_ERROR)
1035              self.validcfg = False
1036  
1037          if (self.validcfg):
1038              return True
1039          else:
1040              return False
1041  
1042      def resetRadioState(self):
1043          self.r_frequency = None
1044          self.r_bandwidth = None
1045          self.r_txpower = None
1046          self.r_sf = None
1047          self.r_cr = None
1048          self.r_state = None
1049  
1050      def updateBitrate(self):
1051          try:
1052              self.bitrate = self.r_sf * ( (4.0/self.r_cr) / (math.pow(2,self.r_sf)/(self.r_bandwidth/1000)) ) * 1000
1053              self.bitrate_kbps = round(self.bitrate/1000.0, 2)
1054              RNS.log(str(self)+" On-air bitrate is now "+str(self.bitrate_kbps)+ " kbps", RNS.LOG_VERBOSE)
1055          except:
1056              self.bitrate = 0
1057  
1058      def process_incoming(self, data):
1059          self.rxb += len(data)
1060  
1061          def af():
1062              self.owner.inbound(data, self)
1063          threading.Thread(target=af, daemon=True).start()
1064  
1065  
1066      def process_outgoing(self,data):
1067          datalen = len(data)
1068          if self.online:
1069              if self.interface_ready:
1070                  if self.flow_control:
1071                      self.interface_ready = False
1072  
1073                  if data == self.id_callsign:
1074                      self.first_tx = None
1075                  else:
1076                      if self.first_tx == None:
1077                          self.first_tx = time.time()
1078  
1079                  data    = KISS.escape(data)
1080                  frame   = bytes([0xc0])+bytes([0x00])+data+bytes([0xc0])
1081  
1082                  written = self.write_mux(frame)
1083                  self.txb += datalen
1084  
1085                  if written != len(frame):
1086                      raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
1087              else:
1088                  self.queue(data)
1089  
1090      def queue(self, data):
1091          self.packet_queue.append(data)
1092  
1093      def process_queue(self):
1094          if len(self.packet_queue) > 0:
1095              data = self.packet_queue.pop(0)
1096              self.interface_ready = True
1097              self.process_outgoing(data)
1098          elif len(self.packet_queue) == 0:
1099              self.interface_ready = True
1100  
1101      def readLoop(self):
1102          try:
1103              in_frame = False
1104              escape = False
1105              command = KISS.CMD_UNKNOWN
1106              data_buffer = b""
1107              command_buffer = b""
1108              last_read_ms = int(time.time()*1000)
1109  
1110              # TODO: Ensure hotplug support for serial drivers
1111              # This should work now with the new time-based
1112              # detect polling.
1113              while (self.serial != None and self.serial.is_open) or (self.bt_manager != None and self.bt_manager.connected):
1114                  serial_bytes = self.read_mux()
1115                  got = len(serial_bytes)
1116                  if got > 0:
1117                      self.last_port_io = time.time()
1118  
1119                  for byte in serial_bytes:
1120                      last_read_ms = int(time.time()*1000)
1121  
1122                      if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
1123                          in_frame = False
1124                          self.process_incoming(data_buffer)
1125                          data_buffer = b""
1126                          command_buffer = b""
1127                      elif (byte == KISS.FEND):
1128                          in_frame = True
1129                          command = KISS.CMD_UNKNOWN
1130                          data_buffer = b""
1131                          command_buffer = b""
1132                      elif (in_frame and len(data_buffer) < self.HW_MTU):
1133                          if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
1134                              command = byte
1135                          elif (command == KISS.CMD_DATA):
1136                              if (byte == KISS.FESC):
1137                                  escape = True
1138                              else:
1139                                  if (escape):
1140                                      if (byte == KISS.TFEND):
1141                                          byte = KISS.FEND
1142                                      if (byte == KISS.TFESC):
1143                                          byte = KISS.FESC
1144                                      escape = False
1145                                  data_buffer = data_buffer+bytes([byte])
1146                          elif (command == KISS.CMD_FREQUENCY):
1147                              if (byte == KISS.FESC):
1148                                  escape = True
1149                              else:
1150                                  if (escape):
1151                                      if (byte == KISS.TFEND):
1152                                          byte = KISS.FEND
1153                                      if (byte == KISS.TFESC):
1154                                          byte = KISS.FESC
1155                                      escape = False
1156                                  command_buffer = command_buffer+bytes([byte])
1157                                  if (len(command_buffer) == 4):
1158                                      self.r_frequency = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3]
1159                                      RNS.log(str(self)+" Radio reporting frequency is "+str(self.r_frequency/1000000.0)+" MHz", RNS.LOG_DEBUG)
1160                                      self.updateBitrate()
1161  
1162                          elif (command == KISS.CMD_BANDWIDTH):
1163                              if (byte == KISS.FESC):
1164                                  escape = True
1165                              else:
1166                                  if (escape):
1167                                      if (byte == KISS.TFEND):
1168                                          byte = KISS.FEND
1169                                      if (byte == KISS.TFESC):
1170                                          byte = KISS.FESC
1171                                      escape = False
1172                                  command_buffer = command_buffer+bytes([byte])
1173                                  if (len(command_buffer) == 4):
1174                                      self.r_bandwidth = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3]
1175                                      RNS.log(str(self)+" Radio reporting bandwidth is "+str(self.r_bandwidth/1000.0)+" KHz", RNS.LOG_DEBUG)
1176                                      self.updateBitrate()
1177  
1178                          elif (command == KISS.CMD_TXPOWER):
1179                              self.r_txpower = byte
1180                              RNS.log(str(self)+" Radio reporting TX power is "+str(self.r_txpower)+" dBm", RNS.LOG_DEBUG)
1181                          elif (command == KISS.CMD_SF):
1182                              self.r_sf = byte
1183                              RNS.log(str(self)+" Radio reporting spreading factor is "+str(self.r_sf), RNS.LOG_DEBUG)
1184                              self.updateBitrate()
1185                          elif (command == KISS.CMD_CR):
1186                              self.r_cr = byte
1187                              RNS.log(str(self)+" Radio reporting coding rate is "+str(self.r_cr), RNS.LOG_DEBUG)
1188                              self.updateBitrate()
1189                          elif (command == KISS.CMD_RADIO_STATE):
1190                              self.r_state = byte
1191                              if self.r_state:
1192                                  RNS.log(str(self)+" Radio reporting state is online", RNS.LOG_DEBUG)
1193                              else:
1194                                  RNS.log(str(self)+" Radio reporting state is offline", RNS.LOG_DEBUG)
1195  
1196                          elif (command == KISS.CMD_RADIO_LOCK):
1197                              self.r_lock = byte
1198                          elif (command == KISS.CMD_FW_VERSION):
1199                              if (byte == KISS.FESC):
1200                                  escape = True
1201                              else:
1202                                  if (escape):
1203                                      if (byte == KISS.TFEND):
1204                                          byte = KISS.FEND
1205                                      if (byte == KISS.TFESC):
1206                                          byte = KISS.FESC
1207                                      escape = False
1208                                  command_buffer = command_buffer+bytes([byte])
1209                                  if (len(command_buffer) == 2):
1210                                      self.maj_version = int(command_buffer[0])
1211                                      self.min_version = int(command_buffer[1])
1212                                      self.validate_firmware()
1213  
1214                          elif (command == KISS.CMD_STAT_RX):
1215                              if (byte == KISS.FESC):
1216                                  escape = True
1217                              else:
1218                                  if (escape):
1219                                      if (byte == KISS.TFEND):
1220                                          byte = KISS.FEND
1221                                      if (byte == KISS.TFESC):
1222                                          byte = KISS.FESC
1223                                      escape = False
1224                                  command_buffer = command_buffer+bytes([byte])
1225                                  if (len(command_buffer) == 4):
1226                                      self.r_stat_rx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3])
1227  
1228                          elif (command == KISS.CMD_STAT_TX):
1229                              if (byte == KISS.FESC):
1230                                  escape = True
1231                              else:
1232                                  if (escape):
1233                                      if (byte == KISS.TFEND):
1234                                          byte = KISS.FEND
1235                                      if (byte == KISS.TFESC):
1236                                          byte = KISS.FESC
1237                                      escape = False
1238                                  command_buffer = command_buffer+bytes([byte])
1239                                  if (len(command_buffer) == 4):
1240                                      self.r_stat_tx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3])
1241  
1242                          elif (command == KISS.CMD_STAT_RSSI):
1243                              self.r_stat_rssi = byte-RNodeInterface.RSSI_OFFSET
1244                          elif (command == KISS.CMD_STAT_SNR):
1245                              self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25
1246                              try:
1247                                  sfs = self.r_sf-7
1248                                  snr = self.r_stat_snr
1249                                  q_snr_min = RNodeInterface.Q_SNR_MIN_BASE-sfs*RNodeInterface.Q_SNR_STEP
1250                                  q_snr_max = RNodeInterface.Q_SNR_MAX
1251                                  q_snr_span = q_snr_max-q_snr_min
1252                                  quality = round(((snr-q_snr_min)/(q_snr_span))*100,1)
1253                                  if quality > 100.0: quality = 100.0
1254                                  if quality < 0.0: quality = 0.0
1255                                  self.r_stat_q = quality
1256                              except:
1257                                  pass
1258  
1259                          elif (command == KISS.CMD_ST_ALOCK):
1260                              if (byte == KISS.FESC):
1261                                  escape = True
1262                              else:
1263                                  if (escape):
1264                                      if (byte == KISS.TFEND):
1265                                          byte = KISS.FEND
1266                                      if (byte == KISS.TFESC):
1267                                          byte = KISS.FESC
1268                                      escape = False
1269                                  command_buffer = command_buffer+bytes([byte])
1270                                  if (len(command_buffer) == 2):
1271                                      at = command_buffer[0] << 8 | command_buffer[1]
1272                                      self.r_st_alock = at/100.0
1273                                      RNS.log(str(self)+" Radio reporting short-term airtime limit is "+str(self.r_st_alock)+"%", RNS.LOG_DEBUG)
1274                          elif (command == KISS.CMD_LT_ALOCK):
1275                              if (byte == KISS.FESC):
1276                                  escape = True
1277                              else:
1278                                  if (escape):
1279                                      if (byte == KISS.TFEND):
1280                                          byte = KISS.FEND
1281                                      if (byte == KISS.TFESC):
1282                                          byte = KISS.FESC
1283                                      escape = False
1284                                  command_buffer = command_buffer+bytes([byte])
1285                                  if (len(command_buffer) == 2):
1286                                      at = command_buffer[0] << 8 | command_buffer[1]
1287                                      self.r_lt_alock = at/100.0
1288                                      RNS.log(str(self)+" Radio reporting long-term airtime limit is "+str(self.r_lt_alock)+"%", RNS.LOG_DEBUG)
1289                          elif (command == KISS.CMD_STAT_CHTM):
1290                              if (byte == KISS.FESC):
1291                                  escape = True
1292                              else:
1293                                  if (escape):
1294                                      if (byte == KISS.TFEND):
1295                                          byte = KISS.FEND
1296                                      if (byte == KISS.TFESC):
1297                                          byte = KISS.FESC
1298                                      escape = False
1299                                  command_buffer = command_buffer+bytes([byte])
1300                                  if (len(command_buffer) == 11):
1301                                      ats = command_buffer[0] << 8 | command_buffer[1]
1302                                      atl = command_buffer[2] << 8 | command_buffer[3]
1303                                      cus = command_buffer[4] << 8 | command_buffer[5]
1304                                      cul = command_buffer[6] << 8 | command_buffer[7]
1305                                      crs = command_buffer[8]
1306                                      nfl = command_buffer[9]
1307                                      ntf = command_buffer[10]
1308                                      
1309                                      self.r_airtime_short      = ats/100.0
1310                                      self.r_airtime_long       = atl/100.0
1311                                      self.r_channel_load_short = cus/100.0
1312                                      self.r_channel_load_long  = cul/100.0
1313                                      self.r_current_rssi       = crs-RNodeInterface.RSSI_OFFSET
1314                                      self.r_noise_floor        = nfl-RNodeInterface.RSSI_OFFSET
1315                                      
1316                                      if ntf == 0xFF:
1317                                          self.r_interference   = None
1318                                      else:
1319                                          self.r_interference   = ntf-RNodeInterface.RSSI_OFFSET
1320                                          self.r_interference_l = [time.time(), self.r_interference]
1321                                      
1322                                      if self.r_interference != None:
1323                                          RNS.log(f"{self} Radio detected interference at {self.r_interference} dBm", RNS.LOG_DEBUG)
1324  
1325                                      # TODO: Remove debug
1326                                      # RNS.log(f"RSSI: {self.r_current_rssi}, Noise floor: {self.r_noise_floor}, Interference: {self.r_interference}", RNS.LOG_EXTREME)
1327                          elif (command == KISS.CMD_STAT_PHYPRM):
1328                              if (byte == KISS.FESC):
1329                                  escape = True
1330                              else:
1331                                  if (escape):
1332                                      if (byte == KISS.TFEND):
1333                                          byte = KISS.FEND
1334                                      if (byte == KISS.TFESC):
1335                                          byte = KISS.FESC
1336                                      escape = False
1337                                  command_buffer = command_buffer+bytes([byte])
1338                                  if (len(command_buffer) == 12):
1339                                      lst = (command_buffer[0] << 8 | command_buffer[1])/1000.0
1340                                      lsr = command_buffer[2] << 8 | command_buffer[3]
1341                                      prs = command_buffer[4] << 8 | command_buffer[5]
1342                                      prt = command_buffer[6] << 8 | command_buffer[7]
1343                                      cst = command_buffer[8] << 8 | command_buffer[9]
1344                                      dft = command_buffer[10] << 8 | command_buffer[11]
1345  
1346                                      if lst != self.r_symbol_time_ms or lsr != self.r_symbol_rate or prs != self.r_preamble_symbols or prt != self.r_premable_time_ms or cst != self.r_csma_slot_time_ms or dft != self.r_csma_difs_ms:
1347                                          self.r_symbol_time_ms    = lst
1348                                          self.r_symbol_rate       = lsr
1349                                          self.r_preamble_symbols  = prs
1350                                          self.r_premable_time_ms  = prt
1351                                          self.r_csma_slot_time_ms = cst
1352                                          self.r_csma_difs_ms      = dft
1353                                          RNS.log(f"{self} Radio reporting symbol time is "+str(round(self.r_symbol_time_ms,2))+"ms ("+str(self.r_symbol_rate)+" baud)", RNS.LOG_DEBUG)
1354                                          RNS.log(f"{self} Radio reporting preamble is "+str(self.r_preamble_symbols)+" symbols ("+str(self.r_premable_time_ms)+"ms)", RNS.LOG_DEBUG)
1355                                          RNS.log(f"{self} Radio reporting CSMA slot time is "+str(self.r_csma_slot_time_ms)+"ms", RNS.LOG_DEBUG)
1356                                          RNS.log(f"{self} Radio reporting DIFS time is "+str(self.r_csma_difs_ms)+"ms", RNS.LOG_DEBUG)
1357                          elif (command == KISS.CMD_STAT_CSMA):
1358                              if (byte == KISS.FESC):
1359                                  escape = True
1360                              else:
1361                                  if (escape):
1362                                      if (byte == KISS.TFEND):
1363                                          byte = KISS.FEND
1364                                      if (byte == KISS.TFESC):
1365                                          byte = KISS.FESC
1366                                      escape = False
1367                                  command_buffer = command_buffer+bytes([byte])
1368                                  if (len(command_buffer) == 3):
1369                                      cbw = command_buffer[0]
1370                                      cbl = command_buffer[1]
1371                                      cbh = command_buffer[2]
1372  
1373                                      if cbw != self.r_csma_cw_band or cbl != self.r_csma_cw_min or cbh != self.r_csma_cw_max:
1374                                          self.r_csma_cw_band = cbw
1375                                          self.r_csma_cw_min  = cbl
1376                                          self.r_csma_cw_max  = cbh
1377                                          # TODO: Remove debug
1378                                          # RNS.log(f"{self} Radio reporting contention window band is {self.r_csma_cw_band}", RNS.LOG_EXTREME)
1379                                          # RNS.log(f"{self} Radio reporting minimum contention window is {self.r_csma_cw_min}", RNS.LOG_EXTREME)
1380                                          # RNS.log(f"{self} Radio reporting maximum contention window is {self.r_csma_cw_max}", RNS.LOG_EXTREME)
1381                          elif (command == KISS.CMD_STAT_BAT):
1382                              if (byte == KISS.FESC):
1383                                  escape = True
1384                              else:
1385                                  if (escape):
1386                                      if (byte == KISS.TFEND):
1387                                          byte = KISS.FEND
1388                                      if (byte == KISS.TFESC):
1389                                          byte = KISS.FESC
1390                                      escape = False
1391                                  command_buffer = command_buffer+bytes([byte])
1392                                  if (len(command_buffer) == 2):
1393                                      bat_percent = command_buffer[1]
1394                                      if bat_percent > 100:
1395                                          bat_percent = 100
1396                                      if bat_percent < 0:
1397                                          bat_percent = 0
1398                                      self.r_battery_state   = command_buffer[0]
1399                                      self.r_battery_percent = bat_percent
1400                          elif (command == KISS.CMD_STAT_TEMP):
1401                              if (byte == KISS.FESC):
1402                                  escape = True
1403                              else:
1404                                  if (escape):
1405                                      if (byte == KISS.TFEND):
1406                                          byte = KISS.FEND
1407                                      if (byte == KISS.TFESC):
1408                                          byte = KISS.FESC
1409                                      escape = False
1410                                  command_buffer = command_buffer+bytes([byte])
1411                                  if (len(command_buffer) == 1):
1412                                      temp = command_buffer[0]-120
1413                                      if temp >= -30 and temp <= 90: self.r_temperature = temp
1414                                      else:                          self.r_temperature = None
1415                                      self.cpu_temp = self.r_temperature
1416                          elif (command == KISS.CMD_RANDOM):
1417                              self.r_random = byte
1418                          elif (command == KISS.CMD_PLATFORM):
1419                              self.platform = byte
1420                          elif (command == KISS.CMD_MCU):
1421                              self.mcu = byte
1422                          elif (command == KISS.CMD_ERROR):
1423                              if (byte == KISS.ERROR_INITRADIO):
1424                                  RNS.log(str(self)+" hardware initialisation error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
1425                                  raise IOError("Radio initialisation failure")
1426                              elif (byte == KISS.ERROR_TXFAILED):
1427                                  RNS.log(str(self)+" hardware TX error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
1428                                  raise IOError("Hardware transmit failure")
1429                              elif (byte == KISS.ERROR_MEMORY_LOW):
1430                                  RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+"): Memory exhausted", RNS.LOG_ERROR)
1431                                  self.hw_errors.append({"error": KISS.ERROR_MEMORY_LOW, "description": "Memory exhausted on connected device"})
1432                              elif (byte == KISS.ERROR_MODEM_TIMEOUT):
1433                                  RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+"): Modem communication timed out", RNS.LOG_ERROR)
1434                                  self.hw_errors.append({"error": KISS.ERROR_MODEM_TIMEOUT, "description": "Modem communication timed out on connected device"})
1435                              else:
1436                                  RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
1437                                  raise IOError("Unknown hardware failure")
1438                          elif (command == KISS.CMD_RESET):
1439                              if (byte == 0xF8):
1440                                  if self.platform == KISS.PLATFORM_ESP32:
1441                                      if self.online:
1442                                          RNS.log("Detected reset while device was online, reinitialising device...", RNS.LOG_ERROR)
1443                                          raise IOError("ESP32 reset")
1444                          elif (command == KISS.CMD_READY):
1445                              self.process_queue()
1446                          
1447                          elif (command == KISS.CMD_FB_READ):
1448                              if (byte == KISS.FESC):
1449                                  escape = True
1450                              else:
1451                                  if (escape):
1452                                      if (byte == KISS.TFEND):
1453                                          byte = KISS.FEND
1454                                      if (byte == KISS.TFESC):
1455                                          byte = KISS.FESC
1456                                      escape = False
1457                                  command_buffer = command_buffer+bytes([byte])
1458                                  if (len(command_buffer) == 512):
1459                                      self.r_framebuffer_latency = time.time() - self.r_framebuffer_readtime
1460                                      self.r_framebuffer = command_buffer
1461  
1462                          elif (command == KISS.CMD_DISP_READ):
1463                              if (byte == KISS.FESC):
1464                                  escape = True
1465                              else:
1466                                  if (escape):
1467                                      if (byte == KISS.TFEND):
1468                                          byte = KISS.FEND
1469                                      if (byte == KISS.TFESC):
1470                                          byte = KISS.FESC
1471                                      escape = False
1472                                  command_buffer = command_buffer+bytes([byte])
1473                                  if (len(command_buffer) == 1024):
1474                                      self.r_disp_latency = time.time() - self.r_disp_readtime
1475                                      self.r_disp = command_buffer
1476                          
1477                          elif (command == KISS.CMD_DETECT):
1478                              if byte == KISS.DETECT_RESP:
1479                                  self.detected = True
1480                              else:
1481                                  self.detected = False
1482  
1483                  if got == 0:
1484                      time_since_last = int(time.time()*1000) - last_read_ms
1485                      if len(data_buffer) > 0 and time_since_last > self.timeout:
1486                          RNS.log(f"{self} device read timeout in command {command} after {RNS.prettytime(self.timeout/1000.0)}", RNS.LOG_WARNING)
1487                          data_buffer = b""
1488                          in_frame = False
1489                          command = KISS.CMD_UNKNOWN
1490                          escape = False
1491  
1492                      if self.id_interval != None and self.id_callsign != None:
1493                          if self.first_tx != None:
1494                              if time.time() > self.first_tx + self.id_interval:
1495                                  RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.id_callsign.decode("utf-8")), RNS.LOG_DEBUG)
1496                                  self.process_outgoing(self.id_callsign)
1497  
1498                      if self.use_tcp:
1499                          if self.tcp and self.tcp.connected:
1500                              if time.time() > self.tcp.last_write + self.tcp.ACTIVITY_KEEPALIVE:
1501                                  self.detect()
1502                      
1503                      if (time.time() - self.last_port_io > self.port_io_timeout): self.detect()
1504                      if (time.time() - self.last_port_io > self.port_io_timeout*3): raise IOError("Connected port for "+str(self)+" became unresponsive")
1505                      if self.bt_manager != None or self.ble != None: sleep(0.08)
1506  
1507          except Exception as e:
1508              self.online = False
1509              RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
1510              RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
1511  
1512              if RNS.Reticulum.panic_on_interface_error:
1513                  RNS.panic()
1514  
1515              RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
1516  
1517          self.online = False
1518  
1519          if self.serial != None:
1520              self.serial.close()
1521  
1522          if self.bt_manager != None:
1523              self.bt_manager.close()
1524  
1525          if not self.detached:
1526              self.reconnect_port()
1527  
1528      def reconnect_port(self):
1529          if self.reconnect_lock.locked():
1530              RNS.log("Dropping superflous reconnect port job")
1531              return
1532  
1533          with self.reconnect_lock:
1534              while not self.online and len(self.hw_errors) == 0:
1535                  try:
1536                      time.sleep(self.reconnect_w)
1537                      if self.serial != None and self.port != None:
1538                          RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_EXTREME)
1539  
1540                      if self.bt_manager != None:
1541                          RNS.log("Attempting to reconnect Bluetooth device for "+str(self)+"...", RNS.LOG_EXTREME)
1542  
1543                      self.open_port()
1544  
1545                      if hasattr(self, "serial") and self.serial != None and self.serial.is_open:
1546                          self.configure_device()
1547                          if self.online:
1548                              if self.last_imagedata != None:
1549                                  self.display_image(self.last_imagedata)
1550                                  self.enable_external_framebuffer()
1551                      
1552                      elif hasattr(self, "bt_manager") and self.bt_manager != None and self.bt_manager.connected:
1553                          self.configure_device()
1554                          if self.online:
1555                              if self.last_imagedata != None:
1556                                  self.display_image(self.last_imagedata)
1557                                  self.enable_external_framebuffer()
1558  
1559                  except Exception as e:
1560                      RNS.log("Error while reconnecting RNode, the contained exception was: "+str(e), RNS.LOG_ERROR)
1561  
1562              if self.online:
1563                  RNS.log("Reconnected serial port for "+str(self))
1564  
1565      def detach(self):
1566          self.detached = True
1567          try:
1568              self.disable_external_framebuffer()
1569              self.setRadioState(KISS.RADIO_STATE_OFF)
1570              self.leave()
1571  
1572          except Exception as e:
1573              RNS.log(f"An error occurred while detaching {self}: {e}", RNS.LOG_ERROR)
1574  
1575          if self.use_ble: self.ble.close()
1576          if self.use_tcp:
1577              time.sleep(0.5)
1578              self.tcp.close()
1579  
1580      def should_ingress_limit(self):
1581          return False
1582  
1583      def get_battery_state(self):
1584          return self.r_battery_state
1585  
1586      def get_battery_state_string(self):
1587          if self.r_battery_state == RNodeInterface.BATTERY_STATE_CHARGED:
1588              return "charged"
1589          elif  self.r_battery_state == RNodeInterface.BATTERY_STATE_CHARGING:
1590              return "charging"
1591          elif self.r_battery_state == RNodeInterface.BATTERY_STATE_DISCHARGING:
1592              return "discharging"
1593          else:
1594              return "unknown"
1595  
1596      def get_battery_percent(self):
1597          return self.r_battery_percent
1598  
1599      def tcp_receive(self, data):
1600          with self.tcp_rx_lock: self.tcp_rx_queue += data
1601  
1602      def tcp_waiting(self): return len(self.tcp_tx_queue) > 0
1603  
1604      def get_tcp_waiting(self, n):
1605          with self.tcp_tx_lock:
1606              data = self.tcp_tx_queue[:n]
1607              self.tcp_tx_queue = self.tcp_tx_queue[n:]
1608              return data
1609  
1610      def ble_receive(self, data):
1611          with self.ble_rx_lock:
1612              self.ble_rx_queue += data
1613  
1614      def ble_waiting(self):
1615          return len(self.ble_tx_queue) > 0
1616  
1617      def get_ble_waiting(self, n):
1618          with self.ble_tx_lock:
1619              data = self.ble_tx_queue[:n]
1620              self.ble_tx_queue = self.ble_tx_queue[n:]
1621              return data
1622  
1623      def __str__(self):
1624          return f"RNodeInterface[{self.name}]"
1625  
1626  class BLEConnection(BluetoothDispatcher):
1627      UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
1628      UART_RX_CHAR_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
1629      UART_TX_CHAR_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"
1630      MAX_GATT_ATTR_LEN = 512
1631      BASE_MTU          = 20
1632      TARGET_MTU        = 512
1633  
1634      MTU_TIMEOUT = 4.0
1635      CONNECT_TIMEOUT = 7.0
1636      RECONNECT_WAIT = 2.5
1637  
1638      @property
1639      def is_open(self):
1640          return self.connected
1641  
1642      @property
1643      def in_waiting(self):
1644          return len(self.owner.ble_rx_queue) > 0
1645  
1646      def write(self, data_bytes):
1647          with self.owner.ble_tx_lock:
1648              self.owner.ble_tx_queue += data_bytes
1649              return len(data_bytes)
1650  
1651      def read(self):
1652          with self.owner.ble_rx_lock:
1653              data = self.owner.ble_rx_queue
1654              self.owner.ble_rx_queue = b""
1655              return data
1656  
1657      def close(self):
1658          try:
1659              if self.connected:
1660                  RNS.log(f"Disconnecting BLE device from {self.owner}", RNS.LOG_DEBUG)
1661                  # RNS.log("Waiting for BLE write buffer to empty...")
1662                  timeout = time.time() + 10
1663                  while self.owner.ble_waiting() and self.write_thread != None and time.time() < timeout:
1664                      time.sleep(0.1)
1665                  # if time.time() > timeout:
1666                  #     RNS.log("Writing timed out")
1667                  # else:
1668                  #     RNS.log("Writing concluded")
1669  
1670                  self.rx_char = None
1671                  self.tx_char = None
1672                  self.mtu = BLEConnection.BASE_MTU
1673                  self.mtu_requested_time = None
1674  
1675                  if self.write_thread != None:
1676                      # RNS.log("Waiting for write thread to finish...")
1677                      while self.write_thread != None:
1678                          time.sleep(0.1)
1679  
1680                  # RNS.log("Writing finished, closing GATT connection")
1681                  self.close_gatt()
1682  
1683                  with self.owner.ble_rx_lock:
1684                      self.owner.ble_rx_queue = b""
1685  
1686                  with self.owner.ble_tx_lock:
1687                      self.owner.ble_tx_queue = b""
1688  
1689                  self.connected = False
1690                  self.ble_device = None
1691  
1692          except Exception as e:
1693              RNS.log("An error occurred while closing BLE connection for {self.owner}: {e}", RNS.LOG_ERROR)
1694              RNS.trace_exception(e)
1695  
1696      def __init__(self, owner=None, target_name=None, target_bt_addr=None):
1697          super(BLEConnection, self).__init__()
1698          self.owner = owner
1699          self.target_name = target_name
1700          self.target_bt_addr = target_bt_addr
1701          self.connect_timeout = BLEConnection.CONNECT_TIMEOUT
1702          self.ble_device = None
1703          self.rx_char = None
1704          self.tx_char = None
1705          self.connected = False
1706          self.was_connected = False
1707          self.connected_time = None
1708          self.mtu_requested_time = None
1709          self.running = False
1710          self.should_run = False
1711          self.connect_job_running = False
1712          self.write_thread = None
1713          self.mtu = BLEConnection.BASE_MTU
1714          self.target_mtu = BLEConnection.TARGET_MTU
1715  
1716          self.bt_manager = AndroidBluetoothManager(owner=self)
1717  
1718          self.should_run = True
1719          self.connection_thread = threading.Thread(target=self.connection_job, daemon=True).start()
1720  
1721      def write_loop(self):
1722          try:
1723              while self.connected and self.rx_char != None:
1724                  if self.owner.ble_waiting():
1725                      data = self.owner.get_ble_waiting(self.mtu)
1726                      self.write_characteristic(self.rx_char, data)
1727                  else:
1728                      time.sleep(0.1)
1729          
1730          except Exception as e:
1731              RNS.log("An error occurred in {self} write loop: {e}", RNS.LOG_ERROR)
1732              RNS.trace_exception(e)
1733  
1734          self.write_thread = None
1735  
1736      def connection_job(self):
1737          ble_devices = []
1738          while self.should_run:
1739              if self.bt_manager.bt_enabled():
1740                  if not self.connected:
1741                      if len(ble_devices) == 0:
1742                          ble_devices = self.find_target_devices()
1743                      
1744                      if len(ble_devices) > 0: self.ble_device = ble_devices.pop()
1745                      else:                    self.ble_device == None
1746  
1747                      if self.ble_device != None:
1748                          if self.was_connected:
1749                              RNS.log(f"Throttling BLE reconnect for {BLEConnection.RECONNECT_WAIT} seconds", RNS.LOG_DEBUG)
1750                              time.sleep(BLEConnection.RECONNECT_WAIT)
1751  
1752                          self.connect_device()
1753  
1754              else:
1755                  if self.connected:
1756                      RNS.log("Bluetooth was disabled, closing active BLE device connection", RNS.LOG_ERROR)
1757                      self.close()
1758  
1759              time.sleep(1)
1760  
1761      def connect_device(self):
1762          if self.ble_device != None and self.bt_manager.bt_enabled():
1763              RNS.log(f"Trying to connect BLE device {self.ble_device.getName()} / {self.ble_device.getAddress()} for {self.owner}...", RNS.LOG_DEBUG)
1764              self.mtu = BLEConnection.BASE_MTU
1765              self.connect_by_device_address(self.ble_device.getAddress())
1766              end = time.time() + BLEConnection.CONNECT_TIMEOUT
1767              while time.time() < end and not self.connected:
1768                  time.sleep(0.25)
1769  
1770              if self.connected:
1771                  self.owner.port = f"ble://{self.ble_device.getAddress()}"
1772                  self.write_thread = threading.Thread(target=self.write_loop, daemon=True)
1773                  self.write_thread.start()
1774              else:
1775                  RNS.log(f"BLE device connection timed out for {self.owner}", RNS.LOG_DEBUG)
1776                  if self.mtu_requested_time:
1777                      RNS.log("MTU update timeout, tearing down connection and resetting BLE dispatcher")
1778                      self.owner.hw_errors.append({"error": KISS.ERROR_INVALID_BLE_MTU, "description": "The Bluetooth Low Energy transfer MTU could not be configured for the connected device, and communication has failed. Restart Reticulum and any connected applications to retry connecting."})
1779                      self.close()
1780                      self.close_gatt()
1781                      self.should_run = False
1782                      self.owner.awaiting_ble_reset = True
1783  
1784                  else:
1785                      self.close_gatt()
1786  
1787              self.connect_job_running = False
1788  
1789      def device_disconnected(self):
1790          RNS.log(f"BLE device for {self.owner} disconnected", RNS.LOG_NOTICE)
1791          self.connected = False
1792          self.ble_device = None
1793          self.close_gatt()
1794  
1795      def find_target_devices(self):
1796          found_device = None
1797          potential_devices = self.bt_manager.get_paired_devices()
1798          suitable_devices = []
1799  
1800          if self.target_bt_addr != None:
1801              for device in potential_devices:
1802                  if (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_LE) or (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_DUAL):
1803                      if str(device.getAddress()).replace(":", "").lower() == str(self.target_bt_addr).replace(":", "").lower():
1804                          found_device = device
1805                          suitable_devices.append(device)
1806                          break
1807  
1808          if not found_device and self.target_name != None:
1809              for device in potential_devices:
1810                  if (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_LE) or (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_DUAL):
1811                      if device.getName().lower() == self.target_name.lower():
1812                          found_device = device
1813                          suitable_devices.append(device)
1814                          break
1815  
1816          if not found_device:
1817              for device in potential_devices:
1818                  if (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_LE) or (device.getType() == AndroidBluetoothManager.DEVICE_TYPE_DUAL):
1819                      if device.getName().startswith("RNode "):
1820                          found_device = device
1821                          suitable_devices.append(device)
1822  
1823          return suitable_devices
1824  
1825      def on_connection_state_change(self, status, state):
1826          if status == GATT_SUCCESS and state:
1827              self.discover_services()
1828          else:
1829              self.device_disconnected()
1830  
1831      def on_services(self, status, services):
1832          if status == GATT_SUCCESS:
1833              self.rx_char = services.search(BLEConnection.UART_RX_CHAR_UUID)
1834              
1835              if self.rx_char is not None:
1836                  self.tx_char = services.search(BLEConnection.UART_TX_CHAR_UUID)
1837  
1838                  if self.tx_char is not None:                
1839                      if self.enable_notifications(self.tx_char):
1840                          RNS.log("Enabled notifications for BLE TX characteristic", RNS.LOG_DEBUG)
1841                          
1842                          RNS.log(f"Requesting BLE connection MTU update to {self.target_mtu}", RNS.LOG_DEBUG)
1843                          self.mtu_requested_time = time.time()
1844                          self.request_mtu(self.target_mtu)
1845  
1846                      else:
1847                          RNS.log("Could not enable notifications for BLE TX characteristic", RNS.LOG_ERROR)
1848  
1849          else:
1850              RNS.log("BLE device service discovery failure", RNS.LOG_ERROR)
1851  
1852      def on_mtu_changed(self, mtu, status):
1853          if status == GATT_SUCCESS:
1854              self.mtu = min(mtu-5, BLEConnection.MAX_GATT_ATTR_LEN)
1855              RNS.log(f"BLE MTU updated to {self.mtu} for {self.owner}", RNS.LOG_DEBUG)
1856              self.connected = True
1857              self.was_connected = True
1858              self.connected_time = time.time()
1859              self.mtu_requested_time = None
1860  
1861          else:
1862              RNS.log(f"MTU update request did not succeed, mtu={mtu}, status={status}", RNS.LOG_ERROR)
1863  
1864      def on_characteristic_changed(self, characteristic):
1865          if characteristic.getUuid().toString() == BLEConnection.UART_TX_CHAR_UUID:
1866              recvd = bytes(characteristic.getValue())
1867              self.owner.ble_receive(recvd)
1868  
1869  class TCPConnection():
1870      TARGET_PORT = 7633
1871      CONNECT_TIMEOUT = 5.0
1872      INITIAL_CONNECT_TIMEOUT = 5.0
1873      RECONNECT_WAIT = 4.0
1874      ACTIVITY_TIMEOUT = 6.0
1875      ACTIVITY_KEEPALIVE = ACTIVITY_TIMEOUT-2.5
1876  
1877      TCP_USER_TIMEOUT = 24
1878      TCP_PROBE_AFTER = 5
1879      TCP_PROBE_INTERVAL = 2
1880      TCP_PROBES = 12
1881  
1882      @property
1883      def is_open(self):
1884          return self.connected
1885  
1886      @property
1887      def in_waiting(self):
1888          buflen = len(self.owner.tcp_rx_queue)
1889          return buflen > 0
1890  
1891      def write(self, data_bytes):
1892          if self.connected and self.socket:
1893              with self.owner.tcp_tx_lock:
1894                  if len(self.owner.tcp_tx_queue) > 0:
1895                      self.socket.sendall(self.owner.tcp_tx_queue)
1896                      self.owner.tcp_tx_queue = b""
1897  
1898              self.socket.sendall(data_bytes)
1899              self.last_write = time.time()
1900  
1901          else:
1902              with self.owner.tcp_tx_lock: self.owner.tcp_tx_queue += data_bytes
1903  
1904          return len(data_bytes)
1905  
1906      def read(self, n=4096):
1907          with self.owner.tcp_rx_lock:
1908              data = self.owner.tcp_rx_queue[:n]
1909              self.owner.tcp_rx_queue = self.owner.tcp_rx_queue[n:]
1910              return data
1911  
1912      def close(self):
1913          if self.connected:
1914              RNS.log(f"Disconnecting TCP socket for {self.owner}", RNS.LOG_DEBUG)
1915              self.must_disconnect = True
1916              if self.socket: self.socket.close()
1917  
1918      def __init__(self, owner=None, target_host=None):
1919          self.owner = owner
1920          self.target_host = target_host
1921          self.connected = False
1922          self.reconnecting = False
1923          self.running = False
1924          self.should_run = False
1925          self.must_disconnect = False
1926          self.connect_job_running = False
1927          self.last_write = time.time()
1928  
1929          self.should_run = True
1930          self.connection_thread = threading.Thread(target=self.initial_connect, daemon=True).start()
1931  
1932      def set_timeouts_linux(self):
1933          self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_USER_TIMEOUT, int(self.TCP_USER_TIMEOUT * 1000))
1934          self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
1935          self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, int(self.TCP_PROBE_AFTER))
1936          self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, int(self.TCP_PROBE_INTERVAL))
1937          self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, int(self.TCP_PROBES))
1938  
1939      def set_timeouts_osx(self):
1940          if hasattr(socket, "TCP_KEEPALIVE"): TCP_KEEPIDLE = socket.TCP_KEEPALIVE
1941          else: TCP_KEEPIDLE = 0x10
1942          self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
1943          self.socket.setsockopt(socket.IPPROTO_TCP, TCP_KEEPIDLE, int(self.TCP_PROBE_AFTER))
1944  
1945      def cleanup(self):
1946          try:
1947              if self.socket: self.socket.close()
1948          except Exception as e:
1949              RNS.log(f"Error while disconnecting TCP socket on cleanup for {self.owner}", RNS.LOG_ERROR)
1950  
1951          self.should_run = False
1952  
1953      def initial_connect(self):
1954          if self.connect(initial=True): threading.Thread(target=self.read_loop, daemon=True).start()
1955  
1956      def connect(self, initial=False):
1957          try:
1958              if initial:
1959                  RNS.log(f"Establishing TCP connection to device for {self.owner}...", RNS.LOG_DEBUG)
1960  
1961              address_info = socket.getaddrinfo(self.target_host, self.TARGET_PORT, proto=socket.IPPROTO_TCP)[0]
1962              address_family = address_info[0]
1963              target_address = address_info[4]
1964  
1965              self.socket = socket.socket(address_family, socket.SOCK_STREAM)
1966              self.socket.settimeout(self.INITIAL_CONNECT_TIMEOUT)
1967              self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
1968              self.socket.connect(target_address)
1969              self.socket.settimeout(None)
1970              self.connected  = True
1971              self.last_write = time.time()
1972  
1973              RNS.log(f"TCP connection to device for {self.owner} established", RNS.LOG_DEBUG)
1974  
1975              if RNS.vendor.platformutils.is_linux():    self.set_timeouts_linux()
1976              elif RNS.vendor.platformutils.is_darwin(): self.set_timeouts_osx()
1977  
1978              return True
1979          
1980          except Exception as e:
1981              if initial:
1982                  RNS.log(f"TCP connection to device for {self.owner} could not be established: {e}", RNS.LOG_ERROR)
1983                  return False
1984              
1985              else: raise e
1986  
1987      def read_loop(self):
1988          try:
1989              data_in = b""
1990              while not self.must_disconnect:
1991                  if self.socket: data_in = self.socket.recv(4096)
1992                  else: data_in = b""
1993  
1994                  if len(data_in) > 0: self.owner.tcp_receive(data_in)
1995                  else:
1996                      self.connected = False
1997                      RNS.log(f"The TCP socket for {self} was closed", RNS.LOG_WARNING)
1998                      break
1999  
2000          except Exception as e:
2001              self.connected = False
2002              RNS.log(f"A TCP read error occurred for {self}, the contained exception was: {e}", RNS.LOG_WARNING)