Prepared serial-based interfaces for live detach/attach

This commit is contained in:
Mark Qvist 2026-09-27 20:30:02 +02:00
parent 2c30a88e85
commit 7283cb417a
No known key found for this signature in database
6 changed files with 144 additions and 110 deletions

View file

@ -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+"]"
def __str__(self): return "AX25KISSInterface["+self.name+"]"

View file

@ -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+"]"
def __str__(self): return "KISSInterface["+self.name+"]"

View file

@ -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

View file

@ -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+"]"

View file

@ -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+"]"
def __str__(self): return "KISSInterface["+self.name+"]"

View file

@ -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+"]"