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