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)