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 importlib.util.find_spec('serial') != None: 76 import serial 77 else: 78 RNS.log("Using the KISS interface requires a serial communication module to be installed.", RNS.LOG_CRITICAL) 79 RNS.log("You can install one with the command: python3 -m pip install pyserial", RNS.LOG_CRITICAL) 80 RNS.panic() 81 82 super().__init__() 83 84 c = Interface.get_config_obj(configuration) 85 name = c["name"] 86 preamble = int(c["preamble"]) if "preamble" in c else None 87 txtail = int(c["txtail"]) if "txtail" in c else None 88 persistence = int(c["persistence"]) if "persistence" in c else None 89 slottime = int(c["slottime"]) if "slottime" in c else None 90 flow_control = c.as_bool("flow_control") if "flow_control" in c else False 91 port = c["port"] if "port" in c else None 92 speed = int(c["speed"]) if "speed" in c else 9600 93 databits = int(c["databits"]) if "databits" in c else 8 94 parity = c["parity"] if "parity" in c else "N" 95 stopbits = int(c["stopbits"]) if "stopbits" in c else 1 96 beacon_interval = int(c["id_interval"]) if "id_interval" in c else None 97 beacon_data = c["id_callsign"] if "id_callsign" in c else None 98 99 if port == None: 100 raise ValueError("No port specified for serial interface") 101 102 self.HW_MTU = 564 103 104 if beacon_data == None: 105 beacon_data = "" 106 107 self.pyserial = serial 108 self.serial = None 109 self.owner = owner 110 self.name = name 111 self.port = port 112 self.speed = speed 113 self.databits = databits 114 self.parity = serial.PARITY_NONE 115 self.stopbits = stopbits 116 self.timeout = 100 117 self.online = False 118 self.beacon_i = beacon_interval 119 self.beacon_d = beacon_data.encode("utf-8") 120 self.first_tx = None 121 self.bitrate = KISSInterface.BITRATE_GUESS 122 123 self.packet_queue = [] 124 self.flow_control = flow_control 125 self.interface_ready = False 126 self.flow_control_timeout = 5 127 self.flow_control_locked = time.time() 128 129 self.preamble = preamble if preamble != None else 350; 130 self.txtail = txtail if txtail != None else 20; 131 self.persistence = persistence if persistence != None else 64; 132 self.slottime = slottime if slottime != None else 20; 133 134 if parity.lower() == "e" or parity.lower() == "even": 135 self.parity = serial.PARITY_EVEN 136 137 if parity.lower() == "o" or parity.lower() == "odd": 138 self.parity = serial.PARITY_ODD 139 140 try: 141 self.open_port() 142 except Exception as e: 143 RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR) 144 raise e 145 146 if self.serial.is_open: 147 self.configure_device() 148 else: 149 raise IOError("Could not open serial port") 150 151 152 def open_port(self): 153 RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE) 154 self.serial = self.pyserial.Serial( 155 port = self.port, 156 baudrate = self.speed, 157 bytesize = self.databits, 158 parity = self.parity, 159 stopbits = self.stopbits, 160 xonxoff = False, 161 rtscts = False, 162 timeout = 0, 163 inter_byte_timeout = None, 164 write_timeout = None, 165 dsrdtr = False, 166 ) 167 168 169 def configure_device(self): 170 # Allow time for interface to initialise before config 171 sleep(2.0) 172 thread = threading.Thread(target=self.readLoop) 173 thread.daemon = True 174 thread.start() 175 self.online = True 176 RNS.log("Serial port "+self.port+" is now open") 177 RNS.log("Configuring KISS interface parameters...") 178 self.setPreamble(self.preamble) 179 self.setTxTail(self.txtail) 180 self.setPersistence(self.persistence) 181 self.setSlotTime(self.slottime) 182 self.setFlowControl(self.flow_control) 183 self.interface_ready = True 184 RNS.log("KISS interface configured") 185 186 187 def setPreamble(self, preamble): 188 preamble_ms = preamble 189 preamble = int(preamble_ms / 10) 190 if preamble < 0: 191 preamble = 0 192 if preamble > 255: 193 preamble = 255 194 195 kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXDELAY])+bytes([preamble])+bytes([KISS.FEND]) 196 written = self.serial.write(kiss_command) 197 if written != len(kiss_command): 198 raise IOError("Could not configure KISS interface preamble to "+str(preamble_ms)+" (command value "+str(preamble)+")") 199 200 def setTxTail(self, txtail): 201 txtail_ms = txtail 202 txtail = int(txtail_ms / 10) 203 if txtail < 0: 204 txtail = 0 205 if txtail > 255: 206 txtail = 255 207 208 kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_TXTAIL])+bytes([txtail])+bytes([KISS.FEND]) 209 written = self.serial.write(kiss_command) 210 if written != len(kiss_command): 211 raise IOError("Could not configure KISS interface TX tail to "+str(txtail_ms)+" (command value "+str(txtail)+")") 212 213 def setPersistence(self, persistence): 214 if persistence < 0: 215 persistence = 0 216 if persistence > 255: 217 persistence = 255 218 219 kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_P])+bytes([persistence])+bytes([KISS.FEND]) 220 written = self.serial.write(kiss_command) 221 if written != len(kiss_command): 222 raise IOError("Could not configure KISS interface persistence to "+str(persistence)) 223 224 def setSlotTime(self, slottime): 225 slottime_ms = slottime 226 slottime = int(slottime_ms / 10) 227 if slottime < 0: 228 slottime = 0 229 if slottime > 255: 230 slottime = 255 231 232 kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_SLOTTIME])+bytes([slottime])+bytes([KISS.FEND]) 233 written = self.serial.write(kiss_command) 234 if written != len(kiss_command): 235 raise IOError("Could not configure KISS interface slot time to "+str(slottime_ms)+" (command value "+str(slottime)+")") 236 237 def setFlowControl(self, flow_control): 238 kiss_command = bytes([KISS.FEND])+bytes([KISS.CMD_READY])+bytes([0x01])+bytes([KISS.FEND]) 239 written = self.serial.write(kiss_command) 240 if written != len(kiss_command): 241 if (flow_control): 242 raise IOError("Could not enable KISS interface flow control") 243 else: 244 raise IOError("Could not enable KISS interface flow control") 245 246 247 def process_incoming(self, data): 248 self.rxb += len(data) 249 self.owner.inbound(data, self) 250 251 252 def process_outgoing(self,data): 253 datalen = len(data) 254 if self.online: 255 if self.interface_ready: 256 if self.flow_control: 257 self.interface_ready = False 258 self.flow_control_locked = time.time() 259 260 data = data.replace(bytes([0xdb]), bytes([0xdb])+bytes([0xdd])) 261 data = data.replace(bytes([0xc0]), bytes([0xdb])+bytes([0xdc])) 262 frame = bytes([KISS.FEND])+bytes([0x00])+data+bytes([KISS.FEND]) 263 264 written = self.serial.write(frame) 265 self.txb += datalen 266 267 if data == self.beacon_d: 268 self.first_tx = None 269 else: 270 if self.first_tx == None: 271 self.first_tx = time.time() 272 273 if written != len(frame): 274 raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) 275 276 else: 277 self.queue(data) 278 279 def queue(self, data): 280 self.packet_queue.append(data) 281 282 def process_queue(self): 283 if len(self.packet_queue) > 0: 284 data = self.packet_queue.pop(0) 285 self.interface_ready = True 286 self.process_outgoing(data) 287 elif len(self.packet_queue) == 0: 288 self.interface_ready = True 289 290 def readLoop(self): 291 try: 292 in_frame = False 293 escape = False 294 command = KISS.CMD_UNKNOWN 295 data_buffer = b"" 296 last_read_ms = int(time.time()*1000) 297 298 while self.serial.is_open: 299 if self.serial.in_waiting: 300 byte = ord(self.serial.read(1)) 301 last_read_ms = int(time.time()*1000) 302 303 if (in_frame and byte == KISS.FEND and command == KISS.CMD_DATA): 304 in_frame = False 305 self.process_incoming(data_buffer) 306 elif (byte == KISS.FEND): 307 in_frame = True 308 command = KISS.CMD_UNKNOWN 309 data_buffer = b"" 310 elif (in_frame and len(data_buffer) < self.HW_MTU): 311 if (len(data_buffer) == 0 and command == KISS.CMD_UNKNOWN): 312 # We only support one HDLC port for now, so 313 # strip off the port nibble 314 byte = byte & 0x0F 315 command = byte 316 elif (command == KISS.CMD_DATA): 317 if (byte == KISS.FESC): 318 escape = True 319 else: 320 if (escape): 321 if (byte == KISS.TFEND): 322 byte = KISS.FEND 323 if (byte == KISS.TFESC): 324 byte = KISS.FESC 325 escape = False 326 data_buffer = data_buffer+bytes([byte]) 327 elif (command == KISS.CMD_READY): 328 self.process_queue() 329 else: 330 time_since_last = int(time.time()*1000) - last_read_ms 331 if len(data_buffer) > 0 and time_since_last > self.timeout: 332 data_buffer = b"" 333 in_frame = False 334 command = KISS.CMD_UNKNOWN 335 escape = False 336 sleep(0.05) 337 338 if self.flow_control: 339 if not self.interface_ready: 340 if time.time() > self.flow_control_locked + self.flow_control_timeout: 341 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) 342 self.process_queue() 343 344 if self.beacon_i != None and self.beacon_d != None: 345 if self.first_tx != None: 346 if time.time() > self.first_tx + self.beacon_i: 347 RNS.log("Interface "+str(self)+" is transmitting beacon data: "+str(self.beacon_d.decode("utf-8")), RNS.LOG_DEBUG) 348 self.first_tx = None 349 350 # Pad to minimum length 351 frame = bytearray(self.beacon_d) 352 while len(frame) < 15: 353 frame.append(0x00) 354 355 self.process_outgoing(bytes(frame)) 356 357 except Exception as e: 358 self.online = False 359 RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) 360 RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) 361 362 if RNS.Reticulum.panic_on_interface_error: 363 RNS.panic() 364 365 RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) 366 367 self.online = False 368 self.serial.close() 369 self.reconnect_port() 370 371 def reconnect_port(self): 372 while not self.online: 373 try: 374 time.sleep(5) 375 RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) 376 self.open_port() 377 if self.serial.is_open: 378 self.configure_device() 379 except Exception as e: 380 RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) 381 382 RNS.log("Reconnected serial port for "+str(self)) 383 384 def should_ingress_limit(self): 385 return False 386 387 def __str__(self): 388 return "KISSInterface["+self.name+"]"