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