meshtastic

- Unnamed repository; edit this file 'description' to name the repository.
git clone git://git.acid.vegas/-c.git
Log | Files | Refs | Archive | README | LICENSE

commit 4134e8f86695d7f9262cd8fbf680bc6629681ddd
parent 8eb954124f33845286e0f3bcb942ac42dd541ada
Author: acidvegas <acid.vegas@acid.vegas>
Date: Sat, 27 Apr 2024 18:28:32 -0400

Improving serial connection for stability, have working reconnection on disconnection now, sorted out pubsub events throwing errors due to triggering the on_node event prior to on_connect

Diffstat:
Mmeshtastic_serial.py | 173++++++++++++++++++++++++++++++++-----------------------------------------------

1 file changed, 69 insertions(+), 104 deletions(-)

diff --git a/meshtastic_serial.py b/meshtastic_serial.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Meshtastic Serial Interface - Developed by Acidvegas in Python (https://git.acid.vegas)
+# Meshtastic Serial Interface - Developed by Acidvegas in Python (https://git.acid.vegas/meshtastic)
 
 import argparse
 import logging
@@ -10,7 +10,7 @@ try:
 	import meshtastic
 	from meshtastic.serial_interface import SerialInterface
 	from meshtastic.util             import findPorts
-   	from meshtastic.tcp_interface    import TCPInterface		
+	from meshtastic.tcp_interface    import TCPInterface
 except ImportError:
 	raise ImportError('meshtastic library not found (pip install meshtastic)')
 
@@ -33,57 +33,44 @@ def now():
 class MeshtasticClient(object):
 	def __init__(self):
 		self.interface = None # We will define the interface in the connect() function
+		self.me        = {}   # We will populate this with the event_connect() callback
 		self.nodes     = {}   # Nodes will populate with the event_node() callback
 
-  
-  	def connect(self, option: str, value: str):
+
+	def connect(self, option: str, value: str):
 		'''
   		Connect to the Meshtastic interface
-    
-    	:param option: The interface option to connect to
+
+		:param option: The interface option to connect to
 		:param value:  The value of the interface option
-     	'''
-  
-		if option == 'serial':
-			if devices := findPorts():
-				if not os.path.exists(args.serial) or not args.serial in devices:
-					raise SystemExit(f'Invalid serial device: {args.serial} (Available: {devices})') # Show available devices if the specified device is invalid
+	 	'''
+
+		while True:
+			try:
+				if option == 'serial':
+					if devices := findPorts():
+						if not os.path.exists(args.serial) or not args.serial in devices:
+							raise Exception(f'Invalid serial port specified: {args.serial} (Available: {devices})')
+					else:
+						raise Exception('No serial devices found')
+					self.interface = SerialInterface(value)
+
+				elif option == 'tcp':
+					self.interface = TCPInterface(value)
+
+				else:
+					raise SystemExit('Invalid interface option')
+
+			except Exception as e:
+				logging.error(f'Failed to connect to the Meshtastic interface: {e}')
+				logging.error('Retrying in 10 seconds...')
+				time.sleep(10)
+
 			else:
-				raise SystemExit('No serial devices found')
-			self.interface = SerialInterface(value)
-   
-		elif option == 'tcp':
-			self.interface = TCPInterface(value)
-
-		elif option == 'mqtt':
-			raise NotImplementedError('MQTT interface not implemented yet')
-
-		else:
-			raise SystemExit('Invalid interface option')
-
-  		logging.info(f'Connected to radio over {option} from {value}:')
-    
-		logging.debug(self.interface.nodes[self.interface.myInfo.my_node_num]) # Print the node info of the connected radio
-
-
-	def disconnect(self):
-		'''Disconnect from the Meshtastic interface'''
-
-		if pub.getDefaultTopicMgr().hasSubscribers(): 
-			pub.unsubAll()
-			logging.info('Unsubscribed from all Meshtastic topics')
-		else:
-			logging.warning('No Meshtastic topics to unsubscribe from')
-
-		if self.interface:
-			self.interface.close()
-			logging.info('Meshtastic interface closed')
-		else:
-			logging.warning('No Meshtastic interface to close')
-   
-		logging.info('Disconnected from radio')
-  
-  
+				self.me = self.interface.getMyNodeInfo()
+				break
+
+
 	def send(self, message: str):
 		'''
 		Send a message to the Meshtastic interface
@@ -105,7 +92,7 @@ class MeshtasticClient(object):
 
 		pub.subscribe(self.event_connect,    'meshtastic.connection.established')
 		pub.subscribe(self.event_disconnect, 'meshtastic.connection.lost')
-		pub.subscribe(self.event_node,       'meshtastic.node.updated')
+		pub.subscribe(self.event_node,       'meshtastic.node')
 		pub.subscribe(self.event_packet,     'meshtastic.receive')
 
 		logging.debug('Listening for Meshtastic events...')
@@ -116,7 +103,7 @@ class MeshtasticClient(object):
 		# pub.subscribe(self.on_user,      'meshtastic.receive.user')
 		# pub.subscribe(self.on_data,      'meshtastic.receive.data.portnum')
 
-		
+
 	def event_connect(self, interface, topic=pub.AUTO_TOPIC):
 		'''
 		Callback function for connection established
@@ -125,9 +112,8 @@ class MeshtasticClient(object):
 		:param topic:     PubSub topic
 		'''
 
-		me = interface.nodes[interface.myInfo.my_node_num]['user']['longName']
-  
-		logging.info(f'Connected to \'{me}\' radio')
+		logging.info(f'Connected to the {self.me["user"]["longName"]} radio on {self.me["user"]["hwModel"]} hardware')
+		logging.info(f'Found a total of {len(self.nodes):,} nodes')
 
 
 	def event_disconnect(self, interface, topic=pub.AUTO_TOPIC):
@@ -138,30 +124,26 @@ class MeshtasticClient(object):
 		:param topic:     PubSub topic
 		'''
 
-		logging.warning('Lost connection to radio')
-  
-  
-  def event_node(self, interface, topic=pub.AUTO_TOPIC):
+		logging.warning('Lost connection to radio!')
+		logging.info('Reconnecting in 10 seconds...')
+
+		time.sleep(10)
+
+		 # TODO: Consider storing the interface option and value in a class variable since we don't want to reference the args object inside the class
+		self.connect('serial' if args.serial else 'tcp', args.serial if args.serial else args.tcp)
+
+
+
+	def event_node(self, node):
 		'''
 		Callback function for node updates
 
-		:param interface: Meshtastic interface
-		:param topic:     PubSub topic
+		:param node: Node information
 		'''
 
-		if not interface.nodes:
-			logging.warning('No nodes found')
-			return
-
-		for node in interface.nodes.values():
-			short = node['user']['shortName']
-			long  = node['user']['longName'].encode('ascii', 'ignore').decode().rstrip()
-			num   = node['num']
-			id    = node['user']['id']
-			mac   = node['user']['macaddr']
-			hw    = node['user']['hwModel']
+		self.nodes[node['num']] = node
 
-			self.nodes[num] = long # we store the node updates in a dictionary so we can parse the names of who sent incomming messages
+		logging.info(f'Node found: {node["user"]["id"]} - {node["user"]["shortName"].ljust(4)} - {node["user"]["longName"]}')
 
 
 	def event_packet(self, packet: dict):
@@ -188,54 +170,38 @@ class MeshtasticClient(object):
 			else:
 				# TODO: Trigger request for node update here
 				print(f'{now()} UNK: {msg}')
-    
-    
-	def event_position(self, packet: dict):
-		'''
-		Callback function for received position packets
-
-		:param packet: Packet received
-		'''
-
-		# Handle incoming position messages
-		pass
-
-
-	
 
 
 
 if __name__ == '__main__':
-	parser = argparse.ArgumentParser(description='Meshtastic Interface')
-
-	# Interface options
-	parser.add_argument('--serial', help='Use serial interface')
-	parser.add_argument('--tcp',    help='Use TCP interface')
-	parser.add_argument('--mqtt',   help='Use MQTT interface')
-
+	parser = argparse.ArgumentParser(description='Meshtastic Interfacing Tool')
+	parser.add_argument('--serial', help='Use serial interface') # Typically /dev/ttyUSB0 or /dev/ttyACM0
+	parser.add_argument('--tcp',    help='Use TCP interface')    # Can be an IP address or hostname (meshtastic.local)
 	args = parser.parse_args()
- 
-	if not args.serial and not args.tcp and not args.mqtt:
-		raise SystemExit('No interface specified')
 
-	if (args.serial and args.tcp) or (args.serial and args.mqtt) or (args.tcp and args.mqtt):
-		raise SystemExit('Only one interface option can be specified (--serial, --tcp, or --mqtt)')
+	# Ensure one interface is specified
+	if (not args.serial and not args.tcp) or (args.serial and args.tcp):
+		raise SystemExit('Must specify either --serial or --tcp interface')
 
 	# Initialize the Meshtastic client
 	mesh = MeshtasticClient()
 
-	# Determine the interface option and value
-	option = 'serial' if args.serial else 'tcp' if args.tcp else 'mqtt'
-	value  = args.serial if args.serial else args.tcp if args.tcp else args.mqtt
+	# Listen for Meshtastic events
+	mesh.listen()
 
-	# Start the Meshtastic interface
-	mesh.connect(option, value)
+	# Connect to the Meshtastic interface
+	mesh.connect('serial' if args.serial else 'tcp', args.serial if args.serial else args.tcp)
 
 	# Keep-alive loop
 	try:
 		while True:
 			time.sleep(60)
 	except KeyboardInterrupt:
-		pass
+		pass # Exit the loop on Ctrl+C
 	finally:
-		mesh.disconnect()
-\ No newline at end of file
+		if mesh.interface:
+			try:
+				mesh.interface.close()
+				logging.info('Connection to radio interface closed!')
+			except:
+				pass