/ RNS / Interfaces / 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  class KISS():
  40      FEND            = 0xC0
  41      FESC            = 0xDB
  42      TFEND           = 0xDC
  43      TFESC           = 0xDD
  44      
  45      CMD_UNKNOWN     = 0xFE
  46      CMD_DATA        = 0x00
  47      CMD_FREQUENCY   = 0x01
  48      CMD_BANDWIDTH   = 0x02
  49      CMD_TXPOWER     = 0x03
  50      CMD_SF          = 0x04
  51      CMD_CR          = 0x05
  52      CMD_RADIO_STATE = 0x06
  53      CMD_RADIO_LOCK  = 0x07
  54      CMD_ST_ALOCK    = 0x0B
  55      CMD_LT_ALOCK    = 0x0C
  56      CMD_DETECT      = 0x08
  57      CMD_LEAVE       = 0x0A
  58      CMD_READY       = 0x0F
  59      CMD_STAT_RX     = 0x21
  60      CMD_STAT_TX     = 0x22
  61      CMD_STAT_RSSI   = 0x23
  62      CMD_STAT_SNR    = 0x24
  63      CMD_STAT_CHTM   = 0x25
  64      CMD_STAT_PHYPRM = 0x26
  65      CMD_STAT_BAT    = 0x27
  66      CMD_STAT_CSMA   = 0x28
  67      CMD_BLINK       = 0x30
  68      CMD_RANDOM      = 0x40
  69      CMD_FB_EXT      = 0x41
  70      CMD_FB_READ     = 0x42
  71      CMD_DISP_READ   = 0x66
  72      CMD_FB_WRITE    = 0x43
  73      CMD_BT_CTRL     = 0x46
  74      CMD_PLATFORM    = 0x48
  75      CMD_MCU         = 0x49
  76      CMD_FW_VERSION  = 0x50
  77      CMD_ROM_READ    = 0x51
  78      CMD_RESET       = 0x55
  79  
  80      DETECT_REQ      = 0x73
  81      DETECT_RESP     = 0x46
  82      
  83      RADIO_STATE_OFF = 0x00
  84      RADIO_STATE_ON  = 0x01
  85      RADIO_STATE_ASK = 0xFF
  86      
  87      CMD_ERROR           = 0x90
  88      ERROR_INITRADIO     = 0x01
  89      ERROR_TXFAILED      = 0x02
  90      ERROR_EEPROM_LOCKED = 0x03
  91      ERROR_QUEUE_FULL    = 0x04
  92      ERROR_MEMORY_LOW    = 0x05
  93      ERROR_MODEM_TIMEOUT = 0x06
  94  
  95      PLATFORM_AVR   = 0x90
  96      PLATFORM_ESP32 = 0x80
  97      PLATFORM_NRF52 = 0x70
  98  
  99      @staticmethod
 100      def escape(data):
 101          data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd]))
 102          data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc]))
 103          return data
 104      
 105  
 106  class RNodeInterface(Interface):
 107      MAX_CHUNK = 32768
 108      DEFAULT_IFAC_SIZE = 8
 109  
 110      FREQ_MIN = 137000000
 111      FREQ_MAX = 3000000000
 112  
 113      RSSI_OFFSET = 157
 114  
 115      CALLSIGN_MAX_LEN    = 32
 116  
 117      REQUIRED_FW_VER_MAJ = 1
 118      REQUIRED_FW_VER_MIN = 52
 119  
 120      RECONNECT_WAIT = 5
 121  
 122      Q_SNR_MIN_BASE = -9
 123      Q_SNR_MAX      = 6
 124      Q_SNR_STEP     = 2
 125  
 126      BATTERY_STATE_UNKNOWN     = 0x00
 127      BATTERY_STATE_DISCHARGING = 0x01
 128      BATTERY_STATE_CHARGING    = 0x02
 129      BATTERY_STATE_CHARGED     = 0x03
 130  
 131      DISPLAY_READ_INTERVAL     = 1.0
 132  
 133      def __init__(self, owner, configuration):
 134          if RNS.vendor.platformutils.is_android():
 135              raise SystemError("Invalid interface type. The Android-specific RNode interface must be used on Android")
 136  
 137          import importlib.util
 138          if importlib.util.find_spec('serial') != None:
 139              import serial
 140          else:
 141              RNS.log("Using the RNode interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL)
 142              RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL)
 143              RNS.panic()
 144  
 145          super().__init__()
 146  
 147          c = Interface.get_config_obj(configuration)
 148          name         = c["name"]
 149          frequency    = int(c["frequency"]) if "frequency" in c else 0
 150          bandwidth    = int(c["bandwidth"]) if "bandwidth" in c else 0
 151          txpower      = int(c["txpower"]) if "txpower" in c else 0
 152          sf           = int(c["spreadingfactor"]) if "spreadingfactor" in c else 0
 153          cr           = int(c["codingrate"]) if "codingrate" in c else 0
 154          flow_control = c.as_bool("flow_control") if "flow_control" in c else False
 155          id_interval  = int(c["id_interval"]) if "id_interval" in c else None
 156          id_callsign  = c["id_callsign"] if "id_callsign" in c else None
 157          st_alock     = float(c["airtime_limit_short"]) if "airtime_limit_short" in c else None
 158          lt_alock     = float(c["airtime_limit_long"]) if "airtime_limit_long" in c else None
 159  
 160          force_ble = False
 161          ble_name = None
 162          ble_addr = None
 163  
 164          port = c["port"] if "port" in c else None
 165  
 166          if port == None:
 167              raise ValueError("No port specified for RNode interface")
 168  
 169          if port != None:
 170              ble_uri_scheme = "ble://"
 171              if port.lower().startswith(ble_uri_scheme):
 172                  force_ble = True
 173                  ble_string = port[len(ble_uri_scheme):]
 174                  port = None
 175                  if len(ble_string) == 0:
 176                      pass
 177                  elif len(ble_string.split(":")) == 6 and len(ble_string) == 17:
 178                      ble_addr = ble_string
 179                  else:
 180                      ble_name = ble_string
 181  
 182          self.HW_MTU = 508
 183          
 184          self.pyserial    = serial
 185          self.serial      = None
 186          self.owner       = owner
 187          self.name        = name
 188          self.port        = port
 189          self.speed       = 115200
 190          self.databits    = 8
 191          self.stopbits    = 1
 192          self.timeout     = 100
 193          self.online      = False
 194          self.detached    = False
 195          self.reconnecting= False
 196          self.hw_errors   = []
 197  
 198          self.use_ble     = False
 199          self.ble_name    = ble_name
 200          self.ble_addr    = ble_addr
 201          self.ble         = None
 202          self.ble_rx_lock = threading.Lock()
 203          self.ble_tx_lock = threading.Lock()
 204          self.ble_rx_queue= b""
 205          self.ble_tx_queue= b""
 206  
 207          self.frequency   = frequency
 208          self.bandwidth   = bandwidth
 209          self.txpower     = txpower
 210          self.sf          = sf
 211          self.cr          = cr
 212          self.state       = KISS.RADIO_STATE_OFF
 213          self.bitrate     = 0
 214          self.st_alock    = st_alock
 215          self.lt_alock    = lt_alock
 216          self.platform    = None
 217          self.display     = None
 218          self.mcu         = None
 219          self.detected    = False
 220          self.firmware_ok = False
 221          self.maj_version = 0
 222          self.min_version = 0
 223  
 224          self.last_id     = 0
 225          self.first_tx    = None
 226          self.reconnect_w = RNodeInterface.RECONNECT_WAIT
 227  
 228          self.r_frequency = None
 229          self.r_bandwidth = None
 230          self.r_txpower   = None
 231          self.r_sf        = None
 232          self.r_cr        = None
 233          self.r_state     = None
 234          self.r_lock      = None
 235          self.r_stat_rx   = None
 236          self.r_stat_tx   = None
 237          self.r_stat_rssi = None
 238          self.r_stat_snr  = None
 239          self.r_st_alock  = None
 240          self.r_lt_alock  = None
 241          self.r_random    = None
 242          self.r_airtime_short      = 0.0
 243          self.r_airtime_long       = 0.0
 244          self.r_channel_load_short = 0.0
 245          self.r_channel_load_long  = 0.0
 246          self.r_symbol_time_ms     = None
 247          self.r_symbol_rate        = None
 248          self.r_preamble_symbols   = None
 249          self.r_premable_time_ms   = None
 250          self.r_csma_slot_time_ms  = None
 251          self.r_csma_difs_ms       = None
 252          self.r_csma_cw_band       = None
 253          self.r_csma_cw_min        = None
 254          self.r_csma_cw_max        = None
 255          self.r_current_rssi       = None
 256          self.r_noise_floor        = None
 257  
 258          self.r_battery_state = RNodeInterface.BATTERY_STATE_UNKNOWN
 259          self.r_battery_percent = 0
 260          self.r_framebuffer = b""
 261          self.r_framebuffer_readtime = 0
 262          self.r_framebuffer_latency = 0
 263          self.r_disp = b""
 264          self.r_disp_readtime = 0
 265          self.r_disp_latency = 0
 266  
 267          self.should_read_display   = False
 268          self.read_display_interval = RNodeInterface.DISPLAY_READ_INTERVAL
 269  
 270          self.packet_queue    = []
 271          self.flow_control    = flow_control
 272          self.interface_ready = False
 273          self.announce_rate_target = None
 274  
 275          if force_ble or self.ble_addr != None or self.ble_name != None:
 276              self.use_ble = True
 277  
 278          self.validcfg  = True
 279          if (self.frequency < RNodeInterface.FREQ_MIN or self.frequency > RNodeInterface.FREQ_MAX):
 280              RNS.log("Invalid frequency configured for "+str(self), RNS.LOG_ERROR)
 281              self.validcfg = False
 282  
 283          if (self.txpower < 0 or self.txpower > 22):
 284              RNS.log("Invalid TX power configured for "+str(self), RNS.LOG_ERROR)
 285              self.validcfg = False
 286  
 287          if (self.bandwidth < 7800 or self.bandwidth > 1625000):
 288              RNS.log("Invalid bandwidth configured for "+str(self), RNS.LOG_ERROR)
 289              self.validcfg = False
 290  
 291          if (self.sf < 5 or self.sf > 12):
 292              RNS.log("Invalid spreading factor configured for "+str(self), RNS.LOG_ERROR)
 293              self.validcfg = False
 294  
 295          if (self.cr < 5 or self.cr > 8):
 296              RNS.log("Invalid coding rate configured for "+str(self), RNS.LOG_ERROR)
 297              self.validcfg = False
 298  
 299          if (self.st_alock and (self.st_alock < 0.0 or self.st_alock > 100.0)):
 300              RNS.log("Invalid short-term airtime limit configured for "+str(self), RNS.LOG_ERROR)
 301              self.validcfg = False
 302  
 303          if (self.lt_alock and (self.lt_alock < 0.0 or self.lt_alock > 100.0)):
 304              RNS.log("Invalid long-term airtime limit configured for "+str(self), RNS.LOG_ERROR)
 305              self.validcfg = False
 306  
 307          if id_interval != None and id_callsign != None:
 308              if (len(id_callsign.encode("utf-8")) <= RNodeInterface.CALLSIGN_MAX_LEN):
 309                  self.should_id = True
 310                  self.id_callsign = id_callsign.encode("utf-8")
 311                  self.id_interval = id_interval
 312              else:
 313                  RNS.log("The encoded ID callsign for "+str(self)+" exceeds the max length of "+str(RNodeInterface.CALLSIGN_MAX_LEN)+" bytes.", RNS.LOG_ERROR)
 314                  self.validcfg = False
 315          else:
 316              self.id_interval = None
 317              self.id_callsign = None
 318  
 319          if (not self.validcfg):
 320              raise ValueError("The configuration for "+str(self)+" contains errors, interface is offline")
 321  
 322          try:
 323              self.open_port()
 324  
 325              if self.serial.is_open:
 326                  self.configure_device()
 327              else:
 328                  raise IOError("Could not open serial port")
 329  
 330          except Exception as e:
 331              RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR)
 332              RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
 333              RNS.log("Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR)
 334              if not self.detached and not self.reconnecting:
 335                  thread = threading.Thread(target=self.reconnect_port)
 336                  thread.daemon = True
 337                  thread.start()
 338  
 339  
 340      def open_port(self):
 341          if not self.use_ble:
 342              RNS.log("Opening serial port "+self.port+"...")
 343              self.serial = self.pyserial.Serial(
 344                  port = self.port,
 345                  baudrate = self.speed,
 346                  bytesize = self.databits,
 347                  parity = self.pyserial.PARITY_NONE,
 348                  stopbits = self.stopbits,
 349                  xonxoff = False,
 350                  rtscts = False,
 351                  timeout = 0,
 352                  inter_byte_timeout = None,
 353                  write_timeout = None,
 354                  dsrdtr = False,
 355              )
 356          
 357          else:
 358              RNS.log(f"Opening BLE connection for {self}...")
 359              if self.ble != None and self.ble.running == False:
 360                  self.ble.close()
 361                  self.ble.cleanup()
 362                  self.ble = None
 363  
 364              if self.ble == None:
 365                  self.ble = BLEConnection(owner=self, target_name=self.ble_name, target_bt_addr=self.ble_addr)
 366                  self.serial = self.ble
 367  
 368              open_time = time.time()
 369              while not self.ble.connected and time.time() < open_time + self.ble.CONNECT_TIMEOUT:
 370                  time.sleep(1)
 371  
 372      def reset_radio_state(self):
 373          self.r_frequency = None
 374          self.r_bandwidth = None
 375          self.r_txpower   = None
 376          self.r_sf        = None
 377          self.r_cr        = None
 378          self.r_state     = None
 379          self.r_lock      = None
 380          self.detected    = False
 381  
 382      def configure_device(self):
 383          self.reset_radio_state()
 384          sleep(2.0)
 385  
 386          thread = threading.Thread(target=self.readLoop)
 387          thread.daemon = True
 388          thread.start()
 389  
 390          self.detect()
 391          if not self.use_ble:
 392              sleep(0.2)
 393          else:
 394              ble_detect_timeout = 5
 395              detect_time = time.time()
 396              while not self.detected and time.time() < detect_time + ble_detect_timeout:
 397                  time.sleep(0.1)
 398              if self.detected:
 399                  detect_time = RNS.prettytime(time.time()-detect_time)
 400              else:
 401                  RNS.log(f"RNode detect timed out over {self.port}", RNS.LOG_ERROR)
 402          
 403          if not self.detected:
 404              RNS.log("Could not detect device for "+str(self), RNS.LOG_ERROR)
 405              self.serial.close()
 406          else:
 407              if self.platform == KISS.PLATFORM_ESP32 or self.platform == KISS.PLATFORM_NRF52:
 408                  self.display = True
 409  
 410          RNS.log("Serial port "+self.port+" is now open")
 411          RNS.log("Configuring RNode interface...", RNS.LOG_VERBOSE)
 412          self.initRadio()
 413          if (self.validateRadioState()):
 414              self.interface_ready = True
 415              RNS.log(str(self)+" is configured and powered up")
 416              sleep(0.3)
 417              self.online = True
 418          else:
 419              RNS.log("After configuring "+str(self)+", the reported radio parameters did not match your configuration.", RNS.LOG_ERROR)
 420              RNS.log("Make sure that your hardware actually supports the parameters specified in the configuration", RNS.LOG_ERROR)
 421              RNS.log("Aborting RNode startup", RNS.LOG_ERROR)
 422              self.serial.close()
 423              
 424  
 425      def initRadio(self):
 426          self.setFrequency()
 427          self.setBandwidth()
 428          self.setTXPower()
 429          self.setSpreadingFactor()
 430          self.setCodingRate()
 431          self.setSTALock()
 432          self.setLTALock()
 433          self.setRadioState(KISS.RADIO_STATE_ON)
 434  
 435          if self.use_ble:
 436              time.sleep(2)
 437  
 438      def detect(self):
 439          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])
 440          written = self.serial.write(kiss_command)
 441          if written != len(kiss_command):
 442              raise IOError("An IO error occurred while detecting hardware for "+str(self))
 443      
 444      def leave(self):
 445          kiss_command = bytes([KISS.FEND, KISS.CMD_LEAVE, 0xFF, KISS.FEND])
 446          written = self.serial.write(kiss_command)
 447          if written != len(kiss_command):
 448              raise IOError("An IO error occurred while sending host left command to device")
 449      
 450      def enable_external_framebuffer(self):
 451          if self.display != None:
 452              kiss_command = bytes([KISS.FEND, KISS.CMD_FB_EXT, 0x01, KISS.FEND])
 453              written = self.serial.write(kiss_command)
 454              if written != len(kiss_command):
 455                  raise IOError("An IO error occurred while enabling external framebuffer on device")
 456  
 457      def disable_external_framebuffer(self):
 458          if self.display != None:
 459              kiss_command = bytes([KISS.FEND, KISS.CMD_FB_EXT, 0x00, KISS.FEND])
 460              written = self.serial.write(kiss_command)
 461              if written != len(kiss_command):
 462                  raise IOError("An IO error occurred while disabling external framebuffer on device")
 463  
 464      FB_PIXEL_WIDTH     = 64
 465      FB_BITS_PER_PIXEL  = 1
 466      FB_PIXELS_PER_BYTE = 8//FB_BITS_PER_PIXEL
 467      FB_BYTES_PER_LINE  = FB_PIXEL_WIDTH//FB_PIXELS_PER_BYTE
 468      def display_image(self, imagedata):
 469          if self.display != None:
 470              lines = len(imagedata)//8
 471              for line in range(lines):
 472                  line_start = line*RNodeInterface.FB_BYTES_PER_LINE
 473                  line_end   = line_start+RNodeInterface.FB_BYTES_PER_LINE
 474                  line_data = bytes(imagedata[line_start:line_end])
 475                  self.write_framebuffer(line, line_data)
 476  
 477      def write_framebuffer(self, line, line_data):
 478          if self.display != None:
 479              line_byte = line.to_bytes(1, byteorder="big", signed=False)
 480              data = line_byte+line_data
 481              escaped_data = KISS.escape(data)
 482              kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FB_WRITE])+escaped_data+bytes([KISS.FEND])
 483              
 484              written = self.serial.write(kiss_command)
 485              if written != len(kiss_command):
 486                  raise IOError("An IO error occurred while writing framebuffer data to device")
 487  
 488      def read_framebuffer(self):
 489          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FB_READ])+bytes([0x01])+bytes([KISS.FEND])
 490          written = self.serial.write(kiss_command)
 491          self.r_framebuffer_readtime = time.time()
 492          if written != len(kiss_command):
 493              raise IOError("An IO error occurred while sending framebuffer read command")
 494  
 495      def read_display(self):
 496          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_DISP_READ])+bytes([0x01])+bytes([KISS.FEND])
 497          written = self.serial.write(kiss_command)
 498          self.r_disp_readtime = time.time()
 499          if written != len(kiss_command):
 500              raise IOError("An IO error occurred while sending display read command")
 501  
 502      def _read_display_job(self):
 503          while self.should_read_display:
 504              self.read_display()
 505              time.sleep(self.read_display_interval)
 506  
 507      def start_display_updates(self):
 508          if not self.should_read_display:
 509              self.should_read_display = True
 510              threading.Thread(target=self._read_display_job, daemon=True).start()
 511  
 512      def stop_display_updates(self):
 513          self.should_read_display = False
 514  
 515      def hard_reset(self):
 516          kiss_command = bytes([KISS.FEND, KISS.CMD_RESET, 0xf8, KISS.FEND])
 517          written = self.serial.write(kiss_command)
 518          if written != len(kiss_command):
 519              raise IOError("An IO error occurred while restarting device")
 520          sleep(2.25);
 521  
 522      def setFrequency(self):
 523          c1 = self.frequency >> 24
 524          c2 = self.frequency >> 16 & 0xFF
 525          c3 = self.frequency >> 8 & 0xFF
 526          c4 = self.frequency & 0xFF
 527          data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4]))
 528  
 529          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_FREQUENCY])+data+bytes([KISS.FEND])
 530          written = self.serial.write(kiss_command)
 531          if written != len(kiss_command):
 532              raise IOError("An IO error occurred while configuring frequency for "+str(self))
 533  
 534      def setBandwidth(self):
 535          c1 = self.bandwidth >> 24
 536          c2 = self.bandwidth >> 16 & 0xFF
 537          c3 = self.bandwidth >> 8 & 0xFF
 538          c4 = self.bandwidth & 0xFF
 539          data = KISS.escape(bytes([c1])+bytes([c2])+bytes([c3])+bytes([c4]))
 540  
 541          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_BANDWIDTH])+data+bytes([KISS.FEND])
 542          written = self.serial.write(kiss_command)
 543          if written != len(kiss_command):
 544              raise IOError("An IO error occurred while configuring bandwidth for "+str(self))
 545  
 546      def setTXPower(self):
 547          txp = bytes([self.txpower])
 548          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXPOWER])+txp+bytes([KISS.FEND])
 549          written = self.serial.write(kiss_command)
 550          if written != len(kiss_command):
 551              raise IOError("An IO error occurred while configuring TX power for "+str(self))
 552  
 553      def setSpreadingFactor(self):
 554          sf = bytes([self.sf])
 555          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SF])+sf+bytes([KISS.FEND])
 556          written = self.serial.write(kiss_command)
 557          if written != len(kiss_command):
 558              raise IOError("An IO error occurred while configuring spreading factor for "+str(self))
 559  
 560      def setCodingRate(self):
 561          cr = bytes([self.cr])
 562          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_CR])+cr+bytes([KISS.FEND])
 563          written = self.serial.write(kiss_command)
 564          if written != len(kiss_command):
 565              raise IOError("An IO error occurred while configuring coding rate for "+str(self))
 566  
 567      def setSTALock(self):
 568          if self.st_alock != None:
 569              at = int(self.st_alock*100)
 570              c1 = at >> 8 & 0xFF
 571              c2 = at & 0xFF
 572              data = KISS.escape(bytes([c1])+bytes([c2]))
 573  
 574              kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_ST_ALOCK])+data+bytes([KISS.FEND])
 575              written = self.serial.write(kiss_command)
 576              if written != len(kiss_command):
 577                  raise IOError("An IO error occurred while configuring short-term airtime limit for "+str(self))
 578  
 579      def setLTALock(self):
 580          if self.lt_alock != None:
 581              at = int(self.lt_alock*100)
 582              c1 = at >> 8 & 0xFF
 583              c2 = at & 0xFF
 584              data = KISS.escape(bytes([c1])+bytes([c2]))
 585  
 586              kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_LT_ALOCK])+data+bytes([KISS.FEND])
 587              written = self.serial.write(kiss_command)
 588              if written != len(kiss_command):
 589                  raise IOError("An IO error occurred while configuring long-term airtime limit for "+str(self))
 590  
 591      def setRadioState(self, state):
 592          self.state = state
 593          kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_RADIO_STATE])+bytes([state])+bytes([KISS.FEND])
 594          written = self.serial.write(kiss_command)
 595          if written != len(kiss_command):
 596              raise IOError("An IO error occurred while configuring radio state for "+str(self))
 597  
 598      def validate_firmware(self):
 599          if (self.maj_version > RNodeInterface.REQUIRED_FW_VER_MAJ):
 600              self.firmware_ok = True
 601          else:
 602              if (self.maj_version >= RNodeInterface.REQUIRED_FW_VER_MAJ):
 603                  if (self.min_version >= RNodeInterface.REQUIRED_FW_VER_MIN):
 604                      self.firmware_ok = True
 605          
 606          if self.firmware_ok:
 607              return
 608  
 609          RNS.log("The firmware version of the connected RNode is "+str(self.maj_version)+"."+str(self.min_version), RNS.LOG_ERROR)
 610          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)
 611          RNS.log("Please update your RNode firmware with rnodeconf from https://github.com/markqvist/rnodeconfigutil/")
 612          RNS.panic()
 613  
 614  
 615      def validateRadioState(self):
 616          RNS.log("Waiting for radio configuration validation for "+str(self)+"...", RNS.LOG_VERBOSE)
 617          if self.use_ble:
 618              sleep(1.00)
 619          else:
 620              sleep(0.25)
 621  
 622          if self.use_ble and self.ble != None and self.ble.device_disappeared:
 623              RNS.log(f"Device disappeared during radio state validation for {self}", RNS.LOG_ERROR)
 624              return False
 625  
 626          self.validcfg = True
 627          if (self.r_frequency != None and abs(self.frequency - int(self.r_frequency)) > 100):
 628              RNS.log("Frequency mismatch", RNS.LOG_ERROR)
 629              self.validcfg = False
 630          if (self.bandwidth != self.r_bandwidth):
 631              RNS.log("Bandwidth mismatch", RNS.LOG_ERROR)
 632              self.validcfg = False
 633          if (self.txpower != self.r_txpower):
 634              RNS.log("TX power mismatch", RNS.LOG_ERROR)
 635              self.validcfg = False
 636          if (self.sf != self.r_sf):
 637              RNS.log("Spreading factor mismatch", RNS.LOG_ERROR)
 638              self.validcfg = False
 639          if (self.state != self.r_state):
 640              RNS.log("Radio state mismatch", RNS.LOG_ERROR)
 641              self.validcfg = False
 642  
 643          if (self.validcfg):
 644              return True
 645          else:
 646              return False
 647  
 648  
 649      def updateBitrate(self):
 650          try:
 651              self.bitrate = self.r_sf * ( (4.0/self.r_cr) / (math.pow(2,self.r_sf)/(self.r_bandwidth/1000)) ) * 1000
 652              self.bitrate_kbps = round(self.bitrate/1000.0, 2)
 653              RNS.log(str(self)+" On-air bitrate is now "+str(self.bitrate_kbps)+ " kbps", RNS.LOG_VERBOSE)
 654          except:
 655              self.bitrate = 0
 656  
 657      def process_incoming(self, data):
 658          self.rxb += len(data)
 659          self.owner.inbound(data, self)
 660          self.r_stat_rssi = None
 661          self.r_stat_snr = None
 662  
 663  
 664      def process_outgoing(self,data):
 665          datalen = len(data)
 666          if self.online:
 667              if self.interface_ready:
 668                  if self.flow_control:
 669                      self.interface_ready = False
 670  
 671                  if data == self.id_callsign:
 672                      self.first_tx = None
 673                  else:
 674                      if self.first_tx == None:
 675                          self.first_tx = time.time()
 676  
 677                  data    = KISS.escape(data)
 678                  frame   = bytes([0xc0])+bytes([0x00])+data+bytes([0xc0])
 679  
 680                  written = self.serial.write(frame)
 681                  self.txb += datalen
 682  
 683                  if written != len(frame):
 684                      raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data)))
 685              else:
 686                  self.queue(data)
 687  
 688      def queue(self, data):
 689          self.packet_queue.append(data)
 690  
 691      def process_queue(self):
 692          if len(self.packet_queue) > 0:
 693              data = self.packet_queue.pop(0)
 694              self.interface_ready = True
 695              self.process_outgoing(data)
 696          elif len(self.packet_queue) == 0:
 697              self.interface_ready = True
 698  
 699      def readLoop(self):
 700          try:
 701              in_frame = False
 702              escape = False
 703              command = KISS.CMD_UNKNOWN
 704              data_buffer = b""
 705              command_buffer = b""
 706              last_read_ms = int(time.time()*1000)
 707  
 708              while self.serial.is_open:
 709                  if self.serial.in_waiting:
 710                      byte = ord(self.serial.read(1))
 711                      last_read_ms = int(time.time()*1000)
 712  
 713                      if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA):
 714                          in_frame = False
 715                          self.process_incoming(data_buffer)
 716                          data_buffer = b""
 717                          command_buffer = b""
 718                      elif (byte == KISS.FEND):
 719                          in_frame = True
 720                          command = KISS.CMD_UNKNOWN
 721                          data_buffer = b""
 722                          command_buffer = b""
 723                      elif (in_frame and len(data_buffer) < self.HW_MTU):
 724                          if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN):
 725                              command = byte
 726                          elif (command == KISS.CMD_DATA):
 727                              if (byte == KISS.FESC):
 728                                  escape = True
 729                              else:
 730                                  if (escape):
 731                                      if (byte == KISS.TFEND):
 732                                          byte = KISS.FEND
 733                                      if (byte == KISS.TFESC):
 734                                          byte = KISS.FESC
 735                                      escape = False
 736                                  data_buffer = data_buffer+bytes([byte])
 737                          elif (command == KISS.CMD_FREQUENCY):
 738                              if (byte == KISS.FESC):
 739                                  escape = True
 740                              else:
 741                                  if (escape):
 742                                      if (byte == KISS.TFEND):
 743                                          byte = KISS.FEND
 744                                      if (byte == KISS.TFESC):
 745                                          byte = KISS.FESC
 746                                      escape = False
 747                                  command_buffer = command_buffer+bytes([byte])
 748                                  if (len(command_buffer) == 4):
 749                                      self.r_frequency = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3]
 750                                      RNS.log(str(self)+" Radio reporting frequency is "+str(self.r_frequency/1000000.0)+" MHz", RNS.LOG_DEBUG)
 751                                      self.updateBitrate()
 752  
 753                          elif (command == KISS.CMD_BANDWIDTH):
 754                              if (byte == KISS.FESC):
 755                                  escape = True
 756                              else:
 757                                  if (escape):
 758                                      if (byte == KISS.TFEND):
 759                                          byte = KISS.FEND
 760                                      if (byte == KISS.TFESC):
 761                                          byte = KISS.FESC
 762                                      escape = False
 763                                  command_buffer = command_buffer+bytes([byte])
 764                                  if (len(command_buffer) == 4):
 765                                      self.r_bandwidth = command_buffer[0] << 24 | command_buffer[1] << 16 | command_buffer[2] << 8 | command_buffer[3]
 766                                      RNS.log(str(self)+" Radio reporting bandwidth is "+str(self.r_bandwidth/1000.0)+" KHz", RNS.LOG_DEBUG)
 767                                      self.updateBitrate()
 768  
 769                          elif (command == KISS.CMD_TXPOWER):
 770                              self.r_txpower = byte
 771                              RNS.log(str(self)+" Radio reporting TX power is "+str(self.r_txpower)+" dBm", RNS.LOG_DEBUG)
 772                          elif (command == KISS.CMD_SF):
 773                              self.r_sf = byte
 774                              RNS.log(str(self)+" Radio reporting spreading factor is "+str(self.r_sf), RNS.LOG_DEBUG)
 775                              self.updateBitrate()
 776                          elif (command == KISS.CMD_CR):
 777                              self.r_cr = byte
 778                              RNS.log(str(self)+" Radio reporting coding rate is "+str(self.r_cr), RNS.LOG_DEBUG)
 779                              self.updateBitrate()
 780                          elif (command == KISS.CMD_RADIO_STATE):
 781                              self.r_state = byte
 782                              if self.r_state:
 783                                  pass
 784                              else:
 785                                  RNS.log(str(self)+" Radio reporting state is offline", RNS.LOG_DEBUG)
 786  
 787                          elif (command == KISS.CMD_RADIO_LOCK):
 788                              self.r_lock = byte
 789                          elif (command == KISS.CMD_FW_VERSION):
 790                              if (byte == KISS.FESC):
 791                                  escape = True
 792                              else:
 793                                  if (escape):
 794                                      if (byte == KISS.TFEND):
 795                                          byte = KISS.FEND
 796                                      if (byte == KISS.TFESC):
 797                                          byte = KISS.FESC
 798                                      escape = False
 799                                  command_buffer = command_buffer+bytes([byte])
 800                                  if (len(command_buffer) == 2):
 801                                      self.maj_version = int(command_buffer[0])
 802                                      self.min_version = int(command_buffer[1])
 803                                      self.validate_firmware()
 804  
 805                          elif (command == KISS.CMD_STAT_RX):
 806                              if (byte == KISS.FESC):
 807                                  escape = True
 808                              else:
 809                                  if (escape):
 810                                      if (byte == KISS.TFEND):
 811                                          byte = KISS.FEND
 812                                      if (byte == KISS.TFESC):
 813                                          byte = KISS.FESC
 814                                      escape = False
 815                                  command_buffer = command_buffer+bytes([byte])
 816                                  if (len(command_buffer) == 4):
 817                                      self.r_stat_rx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3])
 818  
 819                          elif (command == KISS.CMD_STAT_TX):
 820                              if (byte == KISS.FESC):
 821                                  escape = True
 822                              else:
 823                                  if (escape):
 824                                      if (byte == KISS.TFEND):
 825                                          byte = KISS.FEND
 826                                      if (byte == KISS.TFESC):
 827                                          byte = KISS.FESC
 828                                      escape = False
 829                                  command_buffer = command_buffer+bytes([byte])
 830                                  if (len(command_buffer) == 4):
 831                                      self.r_stat_tx = ord(command_buffer[0]) << 24 | ord(command_buffer[1]) << 16 | ord(command_buffer[2]) << 8 | ord(command_buffer[3])
 832  
 833                          elif (command == KISS.CMD_STAT_RSSI):
 834                              self.r_stat_rssi = byte-RNodeInterface.RSSI_OFFSET
 835                          elif (command == KISS.CMD_STAT_SNR):
 836                              self.r_stat_snr = int.from_bytes(bytes([byte]), byteorder="big", signed=True) * 0.25
 837                              try:
 838                                  sfs = self.r_sf-7
 839                                  snr = self.r_stat_snr
 840                                  q_snr_min = RNodeInterface.Q_SNR_MIN_BASE-sfs*RNodeInterface.Q_SNR_STEP
 841                                  q_snr_max = RNodeInterface.Q_SNR_MAX
 842                                  q_snr_span = q_snr_max-q_snr_min
 843                                  quality = round(((snr-q_snr_min)/(q_snr_span))*100,1)
 844                                  if quality > 100.0: quality = 100.0
 845                                  if quality < 0.0: quality = 0.0
 846                                  self.r_stat_q = quality
 847                              except:
 848                                  pass
 849                          elif (command == KISS.CMD_ST_ALOCK):
 850                              if (byte == KISS.FESC):
 851                                  escape = True
 852                              else:
 853                                  if (escape):
 854                                      if (byte == KISS.TFEND):
 855                                          byte = KISS.FEND
 856                                      if (byte == KISS.TFESC):
 857                                          byte = KISS.FESC
 858                                      escape = False
 859                                  command_buffer = command_buffer+bytes([byte])
 860                                  if (len(command_buffer) == 2):
 861                                      at = command_buffer[0] << 8 | command_buffer[1]
 862                                      self.r_st_alock = at/100.0
 863                                      RNS.log(str(self)+" Radio reporting short-term airtime limit is "+str(self.r_st_alock)+"%", RNS.LOG_DEBUG)
 864                          elif (command == KISS.CMD_LT_ALOCK):
 865                              if (byte == KISS.FESC):
 866                                  escape = True
 867                              else:
 868                                  if (escape):
 869                                      if (byte == KISS.TFEND):
 870                                          byte = KISS.FEND
 871                                      if (byte == KISS.TFESC):
 872                                          byte = KISS.FESC
 873                                      escape = False
 874                                  command_buffer = command_buffer+bytes([byte])
 875                                  if (len(command_buffer) == 2):
 876                                      at = command_buffer[0] << 8 | command_buffer[1]
 877                                      self.r_lt_alock = at/100.0
 878                                      RNS.log(str(self)+" Radio reporting long-term airtime limit is "+str(self.r_lt_alock)+"%", RNS.LOG_DEBUG)
 879                          elif (command == KISS.CMD_STAT_CHTM):
 880                              if (byte == KISS.FESC):
 881                                  escape = True
 882                              else:
 883                                  if (escape):
 884                                      if (byte == KISS.TFEND):
 885                                          byte = KISS.FEND
 886                                      if (byte == KISS.TFESC):
 887                                          byte = KISS.FESC
 888                                      escape = False
 889                                  command_buffer = command_buffer+bytes([byte])
 890                                  if (len(command_buffer) == 11):
 891                                      ats = command_buffer[0] << 8 | command_buffer[1]
 892                                      atl = command_buffer[2] << 8 | command_buffer[3]
 893                                      cus = command_buffer[4] << 8 | command_buffer[5]
 894                                      cul = command_buffer[6] << 8 | command_buffer[7]
 895                                      crs = command_buffer[8]
 896                                      nfl = command_buffer[9]
 897                                      ntf = command_buffer[10]
 898                                      
 899                                      self.r_airtime_short      = ats/100.0
 900                                      self.r_airtime_long       = atl/100.0
 901                                      self.r_channel_load_short = cus/100.0
 902                                      self.r_channel_load_long  = cul/100.0
 903                                      self.r_current_rssi       = crs-RNodeInterface.RSSI_OFFSET
 904                                      self.r_noise_floor        = nfl-RNodeInterface.RSSI_OFFSET
 905                                      if ntf == 0xFF:
 906                                          self.r_interference   = None
 907                                      else:
 908                                          self.r_interference   = ntf-RNodeInterface.RSSI_OFFSET
 909                                      
 910                                      if self.r_interference != None:
 911                                          RNS.log(f"{self} Radio detected interference at {self.r_interference} dBm", RNS.LOG_DEBUG)
 912  
 913                                      # TODO: Remove debug
 914                                      # RNS.log(f"RSSI: {self.r_current_rssi}, Noise floor: {self.r_noise_floor}, Interference: {self.r_interference}", RNS.LOG_EXTREME)
 915                          elif (command == KISS.CMD_STAT_PHYPRM):
 916                              if (byte == KISS.FESC):
 917                                  escape = True
 918                              else:
 919                                  if (escape):
 920                                      if (byte == KISS.TFEND):
 921                                          byte = KISS.FEND
 922                                      if (byte == KISS.TFESC):
 923                                          byte = KISS.FESC
 924                                      escape = False
 925                                  command_buffer = command_buffer+bytes([byte])
 926                                  if (len(command_buffer) == 12):
 927                                      lst = (command_buffer[0] << 8 | command_buffer[1])/1000.0
 928                                      lsr = command_buffer[2] << 8 | command_buffer[3]
 929                                      prs = command_buffer[4] << 8 | command_buffer[5]
 930                                      prt = command_buffer[6] << 8 | command_buffer[7]
 931                                      cst = command_buffer[8] << 8 | command_buffer[9]
 932                                      dft = command_buffer[10] << 8 | command_buffer[11]
 933  
 934                                      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:
 935                                          self.r_symbol_time_ms    = lst
 936                                          self.r_symbol_rate       = lsr
 937                                          self.r_preamble_symbols  = prs
 938                                          self.r_premable_time_ms  = prt
 939                                          self.r_csma_slot_time_ms = cst
 940                                          self.r_csma_difs_ms      = dft
 941                                          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)
 942                                          RNS.log(f"{self} Radio reporting preamble is "+str(self.r_preamble_symbols)+" symbols ("+str(self.r_premable_time_ms)+"ms)", RNS.LOG_DEBUG)
 943                                          RNS.log(f"{self} Radio reporting CSMA slot time is "+str(self.r_csma_slot_time_ms)+"ms", RNS.LOG_DEBUG)
 944                                          RNS.log(f"{self} Radio reporting DIFS time is "+str(self.r_csma_difs_ms)+"ms", RNS.LOG_DEBUG)
 945                          elif (command == KISS.CMD_STAT_CSMA):
 946                              if (byte == KISS.FESC):
 947                                  escape = True
 948                              else:
 949                                  if (escape):
 950                                      if (byte == KISS.TFEND):
 951                                          byte = KISS.FEND
 952                                      if (byte == KISS.TFESC):
 953                                          byte = KISS.FESC
 954                                      escape = False
 955                                  command_buffer = command_buffer+bytes([byte])
 956                                  if (len(command_buffer) == 3):
 957                                      cbw = command_buffer[0]
 958                                      cbl = command_buffer[1]
 959                                      cbh = command_buffer[2]
 960  
 961                                      if cbw != self.r_csma_cw_band or cbl != self.r_csma_cw_min or cbh != self.r_csma_cw_max:
 962                                          self.r_csma_cw_band = cbw
 963                                          self.r_csma_cw_min  = cbl
 964                                          self.r_csma_cw_max  = cbh
 965                                          # TODO: Remove debug
 966                                          # RNS.log(f"{self} Radio reporting contention window band is {self.r_csma_cw_band}", RNS.LOG_EXTREME)
 967                                          # RNS.log(f"{self} Radio reporting minimum contention window is {self.r_csma_cw_min}", RNS.LOG_EXTREME)
 968                                          # RNS.log(f"{self} Radio reporting maximum contention window is {self.r_csma_cw_max}", RNS.LOG_EXTREME)
 969                          elif (command == KISS.CMD_STAT_BAT):
 970                              if (byte == KISS.FESC):
 971                                  escape = True
 972                              else:
 973                                  if (escape):
 974                                      if (byte == KISS.TFEND):
 975                                          byte = KISS.FEND
 976                                      if (byte == KISS.TFESC):
 977                                          byte = KISS.FESC
 978                                      escape = False
 979                                  command_buffer = command_buffer+bytes([byte])
 980                                  if (len(command_buffer) == 2):
 981                                      bat_percent = command_buffer[1]
 982                                      if bat_percent > 100:
 983                                          bat_percent = 100
 984                                      if bat_percent < 0:
 985                                          bat_percent = 0
 986                                      self.r_battery_state   = command_buffer[0]
 987                                      self.r_battery_percent = bat_percent
 988                          elif (command == KISS.CMD_RANDOM):
 989                              self.r_random = byte
 990                          elif (command == KISS.CMD_PLATFORM):
 991                              self.platform = byte
 992                          elif (command == KISS.CMD_MCU):
 993                              self.mcu = byte
 994                          elif (command == KISS.CMD_ERROR):
 995                              if (byte == KISS.ERROR_INITRADIO):
 996                                  RNS.log(str(self)+" hardware initialisation error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
 997                                  raise IOError("Radio initialisation failure")
 998                              elif (byte == KISS.ERROR_TXFAILED):
 999                                  RNS.log(str(self)+" hardware TX error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
1000                                  raise IOError("Hardware transmit failure")
1001                              elif (byte == KISS.ERROR_MEMORY_LOW):
1002                                  RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+"): Memory exhausted", RNS.LOG_ERROR)
1003                                  self.hw_errors.append({"error": KISS.ERROR_MEMORY_LOW, "description": "Memory exhausted on connected device"})
1004                              elif (byte == KISS.ERROR_MODEM_TIMEOUT):
1005                                  RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+"): Modem communication timed out", RNS.LOG_ERROR)
1006                                  self.hw_errors.append({"error": KISS.ERROR_MODEM_TIMEOUT, "description": "Modem communication timed out on connected device"})
1007                              else:
1008                                  RNS.log(str(self)+" hardware error (code "+RNS.hexrep(byte)+")", RNS.LOG_ERROR)
1009                                  raise IOError("Unknown hardware failure")
1010                          elif (command == KISS.CMD_RESET):
1011                              if (byte == 0xF8):
1012                                  if self.platform == KISS.PLATFORM_ESP32:
1013                                      if self.online:
1014                                          RNS.log("Detected reset while device was online, reinitialising device...", RNS.LOG_ERROR)
1015                                          raise IOError("ESP32 reset")
1016                          elif (command == KISS.CMD_READY):
1017                              self.process_queue()
1018                          elif (command == KISS.CMD_FB_READ):
1019                              if (byte == KISS.FESC):
1020                                  escape = True
1021                              else:
1022                                  if (escape):
1023                                      if (byte == KISS.TFEND):
1024                                          byte = KISS.FEND
1025                                      if (byte == KISS.TFESC):
1026                                          byte = KISS.FESC
1027                                      escape = False
1028                                  command_buffer = command_buffer+bytes([byte])
1029                                  if (len(command_buffer) == 512):
1030                                      self.r_framebuffer_latency = time.time() - self.r_framebuffer_readtime
1031                                      self.r_framebuffer = command_buffer
1032  
1033                          elif (command == KISS.CMD_DISP_READ):
1034                              if (byte == KISS.FESC):
1035                                  escape = True
1036                              else:
1037                                  if (escape):
1038                                      if (byte == KISS.TFEND):
1039                                          byte = KISS.FEND
1040                                      if (byte == KISS.TFESC):
1041                                          byte = KISS.FESC
1042                                      escape = False
1043                                  command_buffer = command_buffer+bytes([byte])
1044                                  if (len(command_buffer) == 1024):
1045                                      self.r_disp_latency = time.time() - self.r_disp_readtime
1046                                      self.r_disp = command_buffer
1047  
1048                          elif (command == KISS.CMD_DETECT):
1049                              if byte == KISS.DETECT_RESP:
1050                                  self.detected = True
1051                              else:
1052                                  self.detected = False
1053                          
1054                  else:
1055                      time_since_last = int(time.time()*1000) - last_read_ms
1056                      if len(data_buffer) > 0 and time_since_last > self.timeout:
1057                          RNS.log(str(self)+" serial read timeout in command "+str(command), RNS.LOG_WARNING)
1058                          data_buffer = b""
1059                          in_frame = False
1060                          command = KISS.CMD_UNKNOWN
1061                          escape = False
1062  
1063                      if self.id_interval != None and self.id_callsign != None:
1064                          if self.first_tx != None:
1065                              if time.time() > self.first_tx + self.id_interval:
1066                                  RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.id_callsign.decode("utf-8")), RNS.LOG_DEBUG)
1067                                  self.process_outgoing(self.id_callsign)
1068  
1069                      sleep(0.08)
1070  
1071          except Exception as e:
1072              self.online = False
1073              RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR)
1074              RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR)
1075  
1076              if RNS.Reticulum.panic_on_interface_error:
1077                  RNS.panic()
1078  
1079              RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR)
1080  
1081          self.online = False
1082          try:
1083              self.serial.close()
1084          except Exception as e:
1085              pass
1086  
1087          if not self.detached and not self.reconnecting:
1088              self.reconnect_port()
1089  
1090      def reconnect_port(self):
1091          self.reconnecting = True
1092          while not self.online and not self.detached:
1093              try:
1094                  time.sleep(5)
1095                  RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE)
1096                  self.open_port()
1097                  if self.serial.is_open:
1098                      self.configure_device()
1099              except Exception as e:
1100                  RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR)
1101  
1102          self.reconnecting = False
1103          if self.online:
1104              RNS.log("Reconnected serial port for "+str(self))
1105  
1106      def detach(self):
1107          self.detached = True
1108          self.disable_external_framebuffer()
1109          self.setRadioState(KISS.RADIO_STATE_OFF)
1110          self.leave()
1111          
1112          if self.use_ble:
1113              self.ble.close()
1114  
1115      def should_ingress_limit(self):
1116          return False
1117  
1118      def get_battery_state(self):
1119          return self.r_battery_state
1120  
1121      def get_battery_state_string(self):
1122          if self.r_battery_state == RNodeInterface.BATTERY_STATE_CHARGED:
1123              return "charged"
1124          elif  self.r_battery_state == RNodeInterface.BATTERY_STATE_CHARGING:
1125              return "charging"
1126          elif self.r_battery_state == RNodeInterface.BATTERY_STATE_DISCHARGING:
1127              return "discharging"
1128          else:
1129              return "unknown"
1130  
1131      def get_battery_percent(self):
1132          return self.r_battery_percent
1133  
1134      def ble_receive(self, data):
1135          with self.ble_rx_lock:
1136              self.ble_rx_queue += data
1137  
1138      def ble_waiting(self):
1139          return len(self.ble_tx_queue) > 0
1140  
1141      def get_ble_waiting(self, n):
1142          with self.ble_tx_lock:
1143              data = self.ble_tx_queue[:n]
1144              self.ble_tx_queue = self.ble_tx_queue[n:]
1145              return data
1146  
1147      def __str__(self):
1148          return "RNodeInterface["+str(self.name)+"]"
1149  
1150  class BLEConnection():
1151      UART_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
1152      UART_RX_CHAR_UUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
1153      UART_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
1154      bleak = None
1155  
1156      SCAN_TIMEOUT = 2.0
1157      CONNECT_TIMEOUT = 5.0
1158  
1159      @property
1160      def is_open(self):
1161          return self.connected
1162  
1163      @property
1164      def in_waiting(self):
1165          buflen = len(self.owner.ble_rx_queue)
1166          return buflen > 0
1167  
1168      def write(self, data_bytes):
1169          with self.owner.ble_tx_lock:
1170              self.owner.ble_tx_queue += data_bytes
1171              return len(data_bytes)
1172  
1173      def read(self, n):
1174          with self.owner.ble_rx_lock:
1175              data = self.owner.ble_rx_queue[:n]
1176              self.owner.ble_rx_queue = self.owner.ble_rx_queue[n:]
1177              return data
1178  
1179      def close(self):
1180          if self.connected and self.ble_device:
1181              RNS.log(f"Disconnecting BLE device from {self.owner}", RNS.LOG_DEBUG)
1182              self.must_disconnect = True
1183  
1184              while self.connect_job_running:
1185                  time.sleep(0.1)
1186  
1187      def __init__(self, owner=None, target_name=None, target_bt_addr=None):
1188          self.owner = owner
1189          self.target_name = target_name
1190          self.target_bt_addr = target_bt_addr
1191          self.scan_timeout = BLEConnection.SCAN_TIMEOUT
1192          self.ble_device = None
1193          self.last_client = None
1194          self.connected = False
1195          self.running = False
1196          self.should_run = False
1197          self.must_disconnect = False
1198          self.connect_job_running = False
1199          self.device_disappeared = False
1200  
1201          import importlib.util
1202          if BLEConnection.bleak == None:
1203              if importlib.util.find_spec("bleak") != None:
1204                  import bleak
1205                  BLEConnection.bleak = bleak
1206                  
1207                  import asyncio
1208                  BLEConnection.asyncio = asyncio
1209              else:
1210                  RNS.log("Using the RNode interface over BLE requires a the \"bleak\" module to be installed.", RNS.LOG_CRITICAL)
1211                  RNS.log("You can install one with the command: python3 -m pip install bleak", RNS.LOG_CRITICAL)
1212                  RNS.panic()
1213  
1214          self.should_run = True
1215          self.connection_thread = threading.Thread(target=self.connection_job, daemon=True).start()
1216  
1217      def cleanup(self):
1218          try:
1219              if self.last_client != None:
1220                  self.asyncio.run(self.last_client.disconnect())
1221          except Exception as e:
1222              RNS.log(f"Error while disconnecting BLE device on cleanup for {self.owner}", RNS.LOG_ERROR)
1223  
1224          self.should_run = False
1225  
1226      def connection_job(self):
1227          while self.should_run:
1228              if self.ble_device == None:
1229                  self.ble_device = self.find_target_device()
1230  
1231              if type(self.ble_device) == self.bleak.backends.device.BLEDevice:
1232                  if not self.connected:
1233                      self.connect_device()
1234  
1235              time.sleep(1)
1236  
1237          self.cleanup()
1238          self.running = False
1239          RNS.log(f"BLE connection job for {self.owner} ended", RNS.LOG_DEBUG)
1240  
1241      def connect_device(self):
1242          if self.ble_device != None and type(self.ble_device) == self.bleak.backends.device.BLEDevice:
1243              RNS.log(f"Connecting BLE device {self.ble_device} for {self.owner}...", RNS.LOG_DEBUG)
1244  
1245              async def connect_job():
1246                  self.connect_job_running = True
1247                  async with self.bleak.BleakClient(self.ble_device, disconnected_callback=self.device_disconnected) as ble_client:
1248                      def handle_rx(device, data):
1249                          if self.owner != None:
1250                              self.owner.ble_receive(data)
1251  
1252                      self.connected = True
1253                      self.ble_device = ble_client
1254                      self.last_client = ble_client
1255                      self.owner.port = str(f"ble://{ble_client.address}")
1256  
1257                      loop = self.asyncio.get_running_loop()
1258                      uart_service = ble_client.services.get_service(BLEConnection.UART_SERVICE_UUID)
1259                      rx_characteristic = uart_service.get_characteristic(BLEConnection.UART_RX_CHAR_UUID)
1260                      await ble_client.start_notify(BLEConnection.UART_TX_CHAR_UUID, handle_rx)
1261  
1262                      while self.connected:
1263                          if self.owner != None and self.owner.ble_waiting():
1264                              outbound_data = self.owner.get_ble_waiting(rx_characteristic.max_write_without_response_size)
1265                              await ble_client.write_gatt_char(rx_characteristic, outbound_data, response=False)
1266                          elif self.must_disconnect:
1267                              await ble_client.disconnect()
1268                          else:
1269                              await self.asyncio.sleep(0.1)
1270  
1271  
1272              try:
1273                  self.asyncio.run(connect_job())
1274              except Exception as e:
1275                  RNS.log(f"Could not connect BLE device {self.ble_device} for {self.owner}. Possibly missing authentication.", RNS.LOG_ERROR)
1276  
1277              self.connect_job_running = False
1278  
1279      def device_disconnected(self, device):
1280          RNS.log(f"BLE device for {self.owner} disconnected", RNS.LOG_NOTICE)
1281          self.connected = False
1282          self.ble_device = None
1283          self.device_disappeared = True
1284  
1285      def find_target_device(self):
1286          RNS.log(f"Searching for attachable BLE device for {self.owner}...", RNS.LOG_EXTREME)
1287          def device_filter(device: self.bleak.backends.device.BLEDevice, adv: self.bleak.backends.scanner.AdvertisementData):
1288              if BLEConnection.UART_SERVICE_UUID.lower() in adv.service_uuids:
1289                  if self.device_bonded(device):
1290                      if self.target_bt_addr == None and self.target_name == None:
1291                          if device.name.startswith("RNode "):
1292                              return True
1293  
1294                      if self.target_bt_addr == None or (device.address != None and device.address == self.target_bt_addr):
1295                          if self.target_name == None or (device.name != None and device.name == self.target_name):
1296                              return True
1297  
1298                  else:
1299                      if self.target_bt_addr != None and device.address == self.target_bt_addr:
1300                          RNS.log(f"Can't connect to target device {self.target_bt_addr} over BLE, device is not bonded", RNS.LOG_ERROR)
1301                      
1302                      elif self.target_name != None and device.name == self.target_name:
1303                          RNS.log(f"Can't connect to target device {self.target_name} over BLE, device is not bonded", RNS.LOG_ERROR)
1304  
1305              return False
1306  
1307          device = None
1308          try:
1309              device = self.asyncio.run(self.bleak.BleakScanner.find_device_by_filter(device_filter, timeout=self.scan_timeout))
1310          except Exception as e:
1311              RNS.log(f"Error while finding BLE device for {self.owner}: {e}", RNS.LOG_ERROR)
1312              self.should_run = False
1313  
1314          return device
1315  
1316      def device_bonded(self, device):
1317          try:
1318              if hasattr(device, "details"):
1319                  if "props" in device.details and "Bonded" in device.details["props"]:
1320                      if device.details["props"]["Bonded"] == True:
1321                          return True
1322          
1323          except Exception as e:
1324              RNS.log(f"Error while determining device bond status for {device}, the contained exception was: {e}", RNS.LOG_ERROR)
1325  
1326          return False