KISSInterface.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 RNS 37 38 class KISS(): 39 FEND = 0xC0 40 FESC = 0xDB 41 TFEND = 0xDC 42 TFESC = 0xDD 43 CMD_UNKNOWN = 0xFE 44 CMD_DATA = 0x00 45 CMD_TXDELAY = 0x01 46 CMD_P = 0x02 47 CMD_SLOTTIME = 0x03 48 CMD_TXTAIL = 0x04 49 CMD_FULLDUPLEX = 0x05 50 CMD_SETHARDWARE = 0x06 51 CMD_READY = 0x0F 52 CMD_RETURN = 0xFF 53 54 @staticmethod 55 def escape(data): 56 data = data.replace(bytes([0xdb]), bytes([0xdb, 0xdd])) 57 data = data.replace(bytes([0xc0]), bytes([0xdb, 0xdc])) 58 return data 59 60 class KISSInterface(Interface): 61 MAX_CHUNK = 32768 62 BITRATE_GUESS = 1200 63 DEFAULT_IFAC_SIZE = 8 64 65 owner = None 66 port = None 67 speed = None 68 databits = None 69 parity = None 70 stopbits = None 71 serial = None 72 73 def __init__(self, owner, configuration): 74 import importlib.util 75 if RNS.vendor.platformutils.is_android(): 76 self.on_android = True 77 if importlib.util.find_spec('usbserial4a') != None: 78 if importlib.util.find_spec('jnius') == None: 79 RNS.log("Could not load jnius API wrapper for Android, KISS interface cannot be created.", RNS.LOG_CRITICAL) 80 RNS.log("This probably means you are trying to use an USB-based interface from within Termux or similar.", RNS.LOG_CRITICAL) 81 RNS.log("This is currently not possible, due to this environment limiting access to the native Android APIs.", RNS.LOG_CRITICAL) 82 RNS.panic() 83 84 from usbserial4a import serial4a as serial 85 self.parity = "N" 86 87 else: 88 RNS.log("Could not load USB serial module for Android, KISS interface cannot be created.", RNS.LOG_CRITICAL) 89 RNS.log("You can install this module by issuing: pip install usbserial4a", RNS.LOG_CRITICAL) 90 RNS.panic() 91 else: 92 raise SystemError("Android-specific interface was used on non-Android OS") 93 94 super().__init__() 95 96 c = Interface.get_config_obj(configuration) 97 name = c["name"] 98 preamble = int(c["preamble"]) if "preamble" in c else None 99 txtail = int(c["txtail"]) if "txtail" in c else None 100 persistence = int(c["persistence"]) if "persistence" in c else None 101 slottime = int(c["slottime"]) if "slottime" in c else None 102 flow_control = c.as_bool("flow_control") if "flow_control" in c else False 103 port = c["port"] if "port" in c else None 104 speed = int(c["speed"]) if "speed" in c else 9600 105 databits = int(c["databits"]) if "databits" in c else 8 106 parity = c["parity"] if "parity" in c else "N" 107 stopbits = int(c["stopbits"]) if "stopbits" in c else 1 108 beacon_interval = int(c["beacon_interval"]) if "beacon_interval" in c and c["beacon_interval"] != None else None 109 beacon_data = c["beacon_data"] if "beacon_data" in c else None 110 111 self.HW_MTU = 564 112 113 if beacon_data == None: 114 beacon_data = "" 115 116 self.pyserial = serial 117 self.serial = None 118 self.owner = owner 119 self.name = name 120 self.port = port 121 self.speed = speed 122 self.databits = databits 123 self.parity = "N" 124 self.stopbits = stopbits 125 self.timeout = 100 126 self.online = False 127 self.beacon_i = beacon_interval 128 self.beacon_d = beacon_data.encode("utf-8") 129 self.first_tx = None 130 self.bitrate = KISSInterface.BITRATE_GUESS 131 132 self.packet_queue = [] 133 self.flow_control = flow_control 134 self.interface_ready = False 135 self.flow_control_timeout = 5 136 self.flow_control_locked = time.time() 137 138 self.preamble = preamble if preamble != None else 350; 139 self.txtail = txtail if txtail != None else 20; 140 self.persistence = persistence if persistence != None else 64; 141 self.slottime = slottime if slottime != None else 20; 142 143 if parity.lower() == "e" or parity.lower() == "even": 144 self.parity = "E" 145 146 if parity.lower() == "o" or parity.lower() == "odd": 147 self.parity = "O" 148 149 try: 150 self.open_port() 151 except Exception as e: 152 RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR) 153 raise e 154 155 if self.serial.is_open: 156 self.configure_device() 157 else: 158 raise IOError("Could not open serial port") 159 160 161 def open_port(self): 162 RNS.log("Opening serial port "+self.port+"...") 163 # Get device parameters 164 from usb4a import usb 165 device = usb.get_usb_device(self.port) 166 if device: 167 vid = device.getVendorId() 168 pid = device.getProductId() 169 170 # Driver overrides for speficic chips 171 proxy = self.pyserial.get_serial_port 172 if vid == 0x1A86 and pid == 0x55D4: 173 # Force CDC driver for Qinheng CH34x 174 RNS.log(str(self)+" using CDC driver for "+RNS.hexrep(vid)+":"+RNS.hexrep(pid), RNS.LOG_DEBUG) 175 from usbserial4a.cdcacmserial4a import CdcAcmSerial 176 proxy = CdcAcmSerial 177 178 self.serial = proxy( 179 self.port, 180 baudrate = self.speed, 181 bytesize = self.databits, 182 parity = self.parity, 183 stopbits = self.stopbits, 184 xonxoff = False, 185 rtscts = False, 186 timeout = None, 187 inter_byte_timeout = None, 188 # write_timeout = wtimeout, 189 dsrdtr = False, 190 ) 191 192 if vid == 0x0403: 193 # Hardware parameters for FTDI devices @ 115200 baud 194 self.serial.DEFAULT_READ_BUFFER_SIZE = 16 * 1024 195 self.serial.USB_READ_TIMEOUT_MILLIS = 100 196 self.serial.timeout = 0.1 197 elif vid == 0x10C4: 198 # Hardware parameters for SiLabs CP210x @ 115200 baud 199 self.serial.DEFAULT_READ_BUFFER_SIZE = 64 200 self.serial.USB_READ_TIMEOUT_MILLIS = 12 201 self.serial.timeout = 0.012 202 elif vid == 0x1A86 and pid == 0x55D4: 203 # Hardware parameters for Qinheng CH34x @ 115200 baud 204 self.serial.DEFAULT_READ_BUFFER_SIZE = 64 205 self.serial.USB_READ_TIMEOUT_MILLIS = 12 206 self.serial.timeout = 0.1 207 else: 208 # Default values 209 self.serial.DEFAULT_READ_BUFFER_SIZE = 1 * 1024 210 self.serial.USB_READ_TIMEOUT_MILLIS = 100 211 self.serial.timeout = 0.1 212 213 RNS.log(str(self)+" USB read buffer size set to "+RNS.prettysize(self.serial.DEFAULT_READ_BUFFER_SIZE), RNS.LOG_DEBUG) 214 RNS.log(str(self)+" USB read timeout set to "+str(self.serial.USB_READ_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG) 215 RNS.log(str(self)+" USB write timeout set to "+str(self.serial.USB_WRITE_TIMEOUT_MILLIS)+"ms", RNS.LOG_DEBUG) 216 217 def configure_device(self): 218 # Allow time for interface to initialise before config 219 sleep(2.0) 220 thread = threading.Thread(target=self.readLoop) 221 thread.daemon = True 222 thread.start() 223 self.online = True 224 RNS.log("Serial port "+self.port+" is now open") 225 RNS.log("Configuring KISS interface parameters...") 226 self.setPreamble(self.preamble) 227 self.setTxTail(self.txtail) 228 self.setPersistence(self.persistence) 229 self.setSlotTime(self.slottime) 230 self.setFlowControl(self.flow_control) 231 self.interface_ready = True 232 RNS.log("KISS interface configured") 233 234 def setPreamble(self, preamble): 235 preamble_ms = preamble 236 preamble = int(preamble_ms / 10) 237 if preamble < 0: 238 preamble = 0 239 if preamble > 255: 240 preamble = 255 241 242 kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND]) 243 written = self.serial.write(kiss_command) 244 if written != len(kiss_command): 245 raise IOError("Could not configure KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")") 246 247 def setTxTail(self, txtail): 248 txtail_ms = txtail 249 txtail = int(txtail_ms / 10) 250 if txtail < 0: 251 txtail = 0 252 if txtail > 255: 253 txtail = 255 254 255 kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND]) 256 written = self.serial.write(kiss_command) 257 if written != len(kiss_command): 258 raise IOError("Could not configure KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")") 259 260 def setPersistence(self, persistence): 261 if persistence < 0: 262 persistence = 0 263 if persistence > 255: 264 persistence = 255 265 266 kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND]) 267 written = self.serial.write(kiss_command) 268 if written != len(kiss_command): 269 raise IOError("Could not configure KISS interface persistence to "+str(persistence)) 270 271 def setSlotTime(self, slottime): 272 slottime_ms = slottime 273 slottime = int(slottime_ms / 10) 274 if slottime < 0: 275 slottime = 0 276 if slottime > 255: 277 slottime = 255 278 279 kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND]) 280 written = self.serial.write(kiss_command) 281 if written != len(kiss_command): 282 raise IOError("Could not configure KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")") 283 284 def setFlowControl(self, flow_control): 285 kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND]) 286 written = self.serial.write(kiss_command) 287 if written != len(kiss_command): 288 if (flow_control): 289 raise IOError("Could not enable KISS interface flow control") 290 else: 291 raise IOError("Could not enable KISS interface flow control") 292 293 294 def process_incoming(self, data): 295 self.rxb += len(data) 296 def af(): 297 self.owner.inbound(data, self) 298 threading.Thread(target=af, daemon=True).start() 299 300 def process_outgoing(self,data): 301 datalen = len(data) 302 if self.online: 303 if self.interface_ready: 304 if self.flow_control: 305 self.interface_ready = False 306 self.flow_control_locked = time.time() 307 308 data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd])) 309 data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc])) 310 frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND]) 311 312 written = self.serial.write(frame) 313 self.txb += datalen 314 315 if data == self.beacon_d: 316 self.first_tx = None 317 else: 318 if self.first_tx == None: 319 self.first_tx = time.time() 320 321 if written != len(frame): 322 raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) 323 324 else: 325 self.queue(data) 326 327 def queue(self, data): 328 self.packet_queue.append(data) 329 330 def process_queue(self): 331 if len(self.packet_queue) > 0: 332 data = self.packet_queue.pop(0) 333 self.interface_ready = True 334 self.process_outgoing(data) 335 elif len(self.packet_queue) == 0: 336 self.interface_ready = True 337 338 def readLoop(self): 339 try: 340 in_frame = False 341 escape = False 342 command = KISS.CMD_UNKNOWN 343 data_buffer = b"" 344 last_read_ms = int(time.time()*1000) 345 346 while self.serial.is_open: 347 serial_bytes = self.serial.read() 348 got = len(serial_bytes) 349 350 for byte in serial_bytes: 351 last_read_ms = int(time.time()*1000) 352 353 if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA): 354 in_frame = False 355 self.process_incoming(data_buffer) 356 elif (byte == KISS.FEND): 357 in_frame = True 358 command = KISS.CMD_UNKNOWN 359 data_buffer = b"" 360 elif (in_frame and len(data_buffer) < self.HW_MTU): 361 if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN): 362 # We only support one HDLC port for now, so 363 # strip off the port nibble 364 byte = byte & 0x0F 365 command = byte 366 elif (command == KISS.CMD_DATA): 367 if (byte == KISS.FESC): 368 escape = True 369 else: 370 if (escape): 371 if (byte == KISS.TFEND): 372 byte = KISS.FEND 373 if (byte == KISS.TFESC): 374 byte = KISS.FESC 375 escape = False 376 data_buffer = data_buffer+bytes([byte]) 377 elif (command == KISS.CMD_READY): 378 self.process_queue() 379 380 if got == 0: 381 time_since_last = int(time.time()*1000) - last_read_ms 382 if len(data_buffer) > 0 and time_since_last > self.timeout: 383 data_buffer = b"" 384 in_frame = False 385 command = KISS.CMD_UNKNOWN 386 escape = False 387 sleep(0.05) 388 389 if self.flow_control: 390 if not self.interface_ready: 391 if time.time() > self.flow_control_locked + self.flow_control_timeout: 392 RNS.log("Interface "+str(self)+" is unlocking flow control due to time-out. This should not happen. Your hardware might have missed a flow-control READY command, or maybe it does not support flow-control.", RNS.LOG_WARNING) 393 self.process_queue() 394 395 if self.beacon_i != None and self.beacon_d != None: 396 if self.first_tx != None: 397 if time.time() > self.first_tx + self.beacon_i: 398 RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.beacon_d.decode("utf-8")), RNS.LOG_DEBUG) 399 self.first_tx = None 400 401 # Pad to minimum length 402 frame = bytearray(self.beacon_d) 403 while len(frame) < 15: 404 frame.append(0x00) 405 406 self.process_outgoing(bytes(frame)) 407 408 except Exception as e: 409 self.online = False 410 RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) 411 RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) 412 413 if RNS.Reticulum.panic_on_interface_error: 414 RNS.panic() 415 416 RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) 417 418 self.online = False 419 self.serial.close() 420 self.reconnect_port() 421 422 def reconnect_port(self): 423 while not self.online: 424 try: 425 time.sleep(5) 426 RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) 427 self.open_port() 428 if self.serial.is_open: 429 self.configure_device() 430 except Exception as e: 431 RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) 432 433 RNS.log("Reconnected serial port for "+str(self)) 434 435 def should_ingress_limit(self): 436 return False 437 438 def __str__(self): 439 return "KISSInterface["+self.name+"]"