diff --git a/RNS/Interfaces/AX25KISSInterface.py b/RNS/Interfaces/AX25KISSInterface.py index 032b42ae..562de5d4 100644 --- a/RNS/Interfaces/AX25KISSInterface.py +++ b/RNS/Interfaces/AX25KISSInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2026 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -130,6 +130,7 @@ class AX25KISSInterface(Interface): self.flow_control = flow_control self.shared_medium = True self.interface_ready = False + self.reconnecting = False self.flow_control_timeout = 5 self.flow_control_locked = time.time() @@ -152,14 +153,18 @@ class AX25KISSInterface(Interface): try: self.open_port() + if self.serial.is_open: self.configure_device() + else: raise IOError("Could not open serial port") + except Exception as e: - RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) - raise e + RNS.log(f"Could not open serial port for {self}", RNS.LOG_ERROR) + RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR) + RNS.log(f"Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR) + if not self.detached and not self.reconnecting: + thread = threading.Thread(target=self.reconnect_port) + thread.daemon = True + thread.start() - if self.serial.is_open: - self.configure_device() - else: - raise IOError("Could not open serial port") def open_port(self): RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE) @@ -177,6 +182,12 @@ class AX25KISSInterface(Interface): dsrdtr = False, ) + def detach(self): + self.detached = True + self.online = False + try: self.serial.close() + except Exception as e: RNS.log(f"Could not close port for {self} at detach: {e}", RNS.LOG_ERROR) + def configure_device(self): # Allow time for interface to initialise before config sleep(2.0) @@ -373,30 +384,28 @@ class AX25KISSInterface(Interface): RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) - if RNS.Reticulum.panic_on_interface_error: - RNS.panic() + if RNS.Reticulum.panic_on_interface_error: RNS.panic() RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) self.online = False - self.serial.close() - self.reconnect_port() + try: self.serial.close() + except Exception as e: RNS.log(f"Could not close port for {self}", RNS.LOG_ERROR) + if not self.detached and not self.reconnecting: self.reconnect_port() def reconnect_port(self): - while not self.online: + self.reconnecting = True + while not self.online and not self.detached: try: time.sleep(5) RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) self.open_port() - if self.serial.is_open: - self.configure_device() - except Exception as e: - RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) + if self.serial.is_open: self.configure_device() + except Exception as e: RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.log("Reconnected serial port for "+str(self)) + self.reconnecting = False + if self.online: RNS.log("Reconnected serial port for "+str(self)) - def should_ingress_limit(self): - return False + def should_ingress_limit(self): return False - def __str__(self): - return "AX25KISSInterface["+self.name+"]" \ No newline at end of file + def __str__(self): return "AX25KISSInterface["+self.name+"]" \ No newline at end of file diff --git a/RNS/Interfaces/Android/KISSInterface.py b/RNS/Interfaces/Android/KISSInterface.py index 6ba32099..0bdbf00f 100644 --- a/RNS/Interfaces/Android/KISSInterface.py +++ b/RNS/Interfaces/Android/KISSInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2026 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -128,6 +128,7 @@ class KISSInterface(Interface): self.beacon_d = beacon_data.encode("utf-8") self.first_tx = None self.bitrate = KISSInterface.BITRATE_GUESS + self.reconnecting = False self.packet_queue = [] self.flow_control = flow_control @@ -149,15 +150,23 @@ class KISSInterface(Interface): try: self.open_port() + if self.serial.is_open: self.configure_device() + else: raise IOError("Could not open serial port") + except Exception as e: - RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR) - raise e - - if self.serial.is_open: - self.configure_device() - else: - raise IOError("Could not open serial port") + RNS.log(f"Could not open serial port for {self}", RNS.LOG_ERROR) + RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR) + RNS.log(f"Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR) + if not self.detached and not self.reconnecting: + thread = threading.Thread(target=self.reconnect_port) + thread.daemon = True + thread.start() + def detach(self): + self.detached = True + self.online = False + try: self.serial.close() + except Exception as e: RNS.log(f"Could not close port for {self} at detach: {e}", RNS.LOG_ERROR) def open_port(self): RNS.log("Opening serial port "+self.port+"...") @@ -417,24 +426,23 @@ class KISSInterface(Interface): RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) self.online = False - self.serial.close() - self.reconnect_port() + try: self.serial.close() + except Exception as e: RNS.log(f"Could not close port for {self}", RNS.LOG_ERROR) + if not self.detached and not self.reconnecting: self.reconnect_port() def reconnect_port(self): + self.reconnecting = True while not self.online: try: time.sleep(5) RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) self.open_port() - if self.serial.is_open: - self.configure_device() - except Exception as e: - RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) + if self.serial.is_open: self.configure_device() + except Exception as e: RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.log("Reconnected serial port for "+str(self)) + self.reconnecting = False + if self.online: RNS.log("Reconnected serial port for "+str(self)) - def should_ingress_limit(self): - return False + def should_ingress_limit(self): return False - def __str__(self): - return "KISSInterface["+self.name+"]" \ No newline at end of file + def __str__(self): return "KISSInterface["+self.name+"]" \ No newline at end of file diff --git a/RNS/Interfaces/Android/RNodeInterface.py b/RNS/Interfaces/Android/RNodeInterface.py index 0e744d61..aa999b9c 100644 --- a/RNS/Interfaces/Android/RNodeInterface.py +++ b/RNS/Interfaces/Android/RNodeInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2026 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/RNS/Interfaces/Android/SerialInterface.py b/RNS/Interfaces/Android/SerialInterface.py index 088b41fc..22c28765 100644 --- a/RNS/Interfaces/Android/SerialInterface.py +++ b/RNS/Interfaces/Android/SerialInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2026 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -109,6 +109,7 @@ class SerialInterface(Interface): self.timeout = 100 self.online = False self.bitrate = self.speed + self.reconnecting = False if parity.lower() == "e" or parity.lower() == "even": self.parity = "E" @@ -118,15 +119,23 @@ class SerialInterface(Interface): try: self.open_port() + if self.serial.is_open: self.configure_device() + else: raise IOError("Could not open serial port") + except Exception as e: - RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) - raise e - - if self.serial.is_open: - self.configure_device() - else: - raise IOError("Could not open serial port") + RNS.log(f"Could not open serial port for {self}", RNS.LOG_ERROR) + RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR) + RNS.log(f"Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR) + if not self.detached and not self.reconnecting: + thread = threading.Thread(target=self.reconnect_port) + thread.daemon = True + thread.start() + def detach(self): + self.detached = True + self.online = False + try: self.serial.close() + except Exception as e: RNS.log(f"Could not close port for {self} at detach: {e}", RNS.LOG_ERROR) def open_port(self): RNS.log("Opening serial port "+self.port+"...") @@ -258,24 +267,23 @@ class SerialInterface(Interface): RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) self.online = False - self.serial.close() - self.reconnect_port() + try: self.serial.close() + except Exception as e: RNS.log(f"Could not close port for {self}", RNS.LOG_ERROR) + if not self.detached and not self.reconnecting: self.reconnect_port() def reconnect_port(self): - while not self.online: + self.reconnecting = True + while not self.online and not self.detached: try: time.sleep(5) RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) self.open_port() - if self.serial.is_open: - self.configure_device() - except Exception as e: - RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) + if self.serial.is_open: self.configure_device() + except Exception as e: RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.log("Reconnected serial port for "+str(self)) + self.reconnecting = False + if self.online: RNS.log("Reconnected serial port for "+str(self)) - def should_ingress_limit(self): - return False + def should_ingress_limit(self): return False - def __str__(self): - return "SerialInterface["+self.name+"]" + def __str__(self): return "SerialInterface["+self.name+"]" diff --git a/RNS/Interfaces/KISSInterface.py b/RNS/Interfaces/KISSInterface.py index 298e20f7..9323ea6f 100644 --- a/RNS/Interfaces/KISSInterface.py +++ b/RNS/Interfaces/KISSInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2026 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -124,6 +124,7 @@ class KISSInterface(Interface): self.flow_control = flow_control self.shared_medium = True self.interface_ready = False + self.reconnecting = False self.flow_control_timeout = 5 self.flow_control_locked = time.time() @@ -140,15 +141,17 @@ class KISSInterface(Interface): try: self.open_port() + if self.serial.is_open: self.configure_device() + else: raise IOError("Could not open serial port") + except Exception as e: - RNS.log("Could not open serial port "+self.port, RNS.LOG_ERROR) - raise e - - if self.serial.is_open: - self.configure_device() - else: - raise IOError("Could not open serial port") - + RNS.log(f"Could not open serial port for {self}", RNS.LOG_ERROR) + RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR) + RNS.log(f"Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR) + if not self.detached and not self.reconnecting: + thread = threading.Thread(target=self.reconnect_port) + thread.daemon = True + thread.start() def open_port(self): RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE) @@ -166,6 +169,11 @@ class KISSInterface(Interface): dsrdtr = False, ) + def detach(self): + self.detached = True + self.online = False + try: self.serial.close() + except Exception as e: RNS.log(f"Could not close port for {self} at detach: {e}", RNS.LOG_ERROR) def configure_device(self): # Allow time for interface to initialise before config @@ -361,30 +369,28 @@ class KISSInterface(Interface): RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) - if RNS.Reticulum.panic_on_interface_error: - RNS.panic() + if RNS.Reticulum.panic_on_interface_error: RNS.panic() RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) self.online = False - self.serial.close() - self.reconnect_port() + try: self.serial.close() + except Exception as e: RNS.log(f"Could not close port for {self}", RNS.LOG_ERROR) + if not self.detached and not self.reconnecting: self.reconnect_port() def reconnect_port(self): - while not self.online: + self.reconnecting = True + while not self.online and not self.detached: try: time.sleep(5) RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) self.open_port() - if self.serial.is_open: - self.configure_device() - except Exception as e: - RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) + if self.serial.is_open: self.configure_device() + except Exception as e: RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.log("Reconnected serial port for "+str(self)) + self.reconnecting = False + if self.online: RNS.log("Reconnected serial port for "+str(self)) - def should_ingress_limit(self): - return False + def should_ingress_limit(self): return False - def __str__(self): - return "KISSInterface["+self.name+"]" \ No newline at end of file + def __str__(self): return "KISSInterface["+self.name+"]" \ No newline at end of file diff --git a/RNS/Interfaces/SerialInterface.py b/RNS/Interfaces/SerialInterface.py index e03cb1f0..0e8d7665 100755 --- a/RNS/Interfaces/SerialInterface.py +++ b/RNS/Interfaces/SerialInterface.py @@ -1,6 +1,6 @@ # Reticulum License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2026 Mark Qvist # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -97,6 +97,7 @@ class SerialInterface(Interface): self.timeout = 100 self.online = False self.bitrate = self.speed + self.reconnecting = False if parity.lower() == "e" or parity.lower() == "even": self.parity = serial.PARITY_EVEN @@ -106,15 +107,17 @@ class SerialInterface(Interface): try: self.open_port() + if self.serial.is_open: self.configure_device() + else: raise IOError("Could not open serial port") + except Exception as e: - RNS.log("Could not open serial port for interface "+str(self), RNS.LOG_ERROR) - raise e - - if self.serial.is_open: - self.configure_device() - else: - raise IOError("Could not open serial port") - + RNS.log(f"Could not open serial port for {self}", RNS.LOG_ERROR) + RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR) + RNS.log(f"Reticulum will attempt to bring up this interface periodically", RNS.LOG_ERROR) + if not self.detached and not self.reconnecting: + thread = threading.Thread(target=self.reconnect_port) + thread.daemon = True + thread.start() def open_port(self): RNS.log("Opening serial port "+self.port+"...", RNS.LOG_VERBOSE) @@ -132,6 +135,11 @@ class SerialInterface(Interface): dsrdtr = False, ) + def detach(self): + self.detached = True + self.online = False + try: self.serial.close() + except Exception as e: RNS.log(f"Could not close port for {self} at detach: {e}", RNS.LOG_ERROR) def configure_device(self): sleep(0.5) @@ -141,13 +149,11 @@ class SerialInterface(Interface): self.online = True RNS.log("Serial port "+self.port+" is now open", RNS.LOG_VERBOSE) - def process_incoming(self, data): if not data: return self.rxb += len(data) self.owner.inbound(data, self) - def process_outgoing(self,data): if self.online: data = bytes([HDLC.FLAG])+HDLC.escape(data)+bytes([HDLC.FLAG]) @@ -156,7 +162,6 @@ class SerialInterface(Interface): if written != len(data): raise IOError("Serial interface only wrote "+str(written)+" bytes of "+str(len(data))) - def readLoop(self): try: in_frame = False @@ -200,30 +205,28 @@ class SerialInterface(Interface): RNS.log("A serial port error occurred, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The interface "+str(self)+" experienced an unrecoverable error and is now offline.", RNS.LOG_ERROR) - if RNS.Reticulum.panic_on_interface_error: - RNS.panic() + if RNS.Reticulum.panic_on_interface_error: RNS.panic() RNS.log("Reticulum will attempt to reconnect the interface periodically.", RNS.LOG_ERROR) self.online = False - self.serial.close() - self.reconnect_port() + try: self.serial.close() + except Exception as e: RNS.log(f"Could not close port for {self}", RNS.LOG_ERROR) + if not self.detached and not self.reconnecting: self.reconnect_port() def reconnect_port(self): - while not self.online: + self.reconnecting = True + while not self.online and not self.detached: try: time.sleep(5) RNS.log("Attempting to reconnect serial port "+str(self.port)+" for "+str(self)+"...", RNS.LOG_VERBOSE) self.open_port() - if self.serial.is_open: - self.configure_device() - except Exception as e: - RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) + if self.serial.is_open: self.configure_device() + except Exception as e: RNS.log("Error while reconnecting port, the contained exception was: "+str(e), RNS.LOG_ERROR) - RNS.log("Reconnected serial port for "+str(self)) + self.reconnecting = False + if self.online: RNS.log("Reconnected serial port for "+str(self)) - def should_ingress_limit(self): - return False + def should_ingress_limit(self): return False - def __str__(self): - return "SerialInterface["+self.name+"]" + def __str__(self): return "SerialInterface["+self.name+"]"