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