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