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