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

meshtastic_serial.py (7562B)

      1 #!/usr/bin/env python
      2 # Meshtastic Serial Interface - Developed by Acidvegas in Python (https://git.acid.vegas)
      3 
      4 import argparse
      5 import logging
      6 import os
      7 import time
      8 
      9 try:
     10 	import meshtastic
     11 	from meshtastic.serial_interface import SerialInterface
     12 	from meshtastic.util             import findPorts
     13 	from meshtastic.tcp_interface    import TCPInterface		
     14 except ImportError:
     15 	raise ImportError('meshtastic library not found (pip install meshtastic)')
     16 
     17 try:
     18 	from pubsub import pub
     19 except ImportError:
     20 	raise ImportError('pubsub library not found (pip install pypubsub)')
     21 
     22 
     23 # Initialize logging
     24 logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(levelname)9s | %(funcName)s | %(message)s', datefmt='%Y-%m-%d %I:%M:%S')
     25 
     26 
     27 class MeshtasticClient(object):
     28 	def __init__(self):
     29 		self.interface = None
     30 		self.me        = {} 
     31 		self.nodes     = {}
     32 
     33 
     34 	def connect(self, option: str, value: str):
     35 		'''
     36   		Connect to the Meshtastic interface
     37     
     38     	:param option: The interface option to connect to
     39 		:param value:  The value of the interface option
     40      	'''
     41   
     42 		while True:
     43 			try:
     44 				if option == 'serial':
     45 					if devices := findPorts():
     46 						if not os.path.exists(args.serial) or not args.serial in devices:
     47 							raise Exception(f'Invalid serial port specified: {args.serial} (Available: {devices})')
     48 					else:
     49 						raise Exception('No serial devices found')
     50 					self.interface = SerialInterface(value)
     51 		
     52 				elif option == 'tcp':
     53 					self.interface = TCPInterface(value)
     54 
     55 				else:
     56 					raise SystemExit('Invalid interface option')
     57 
     58 			except Exception as e:
     59 				logging.error(f'Failed to connect to the Meshtastic interface: {e}')
     60 				logging.error('Retrying in 10 seconds...')
     61 				time.sleep(10)
     62 
     63 			else:
     64 				self.me = self.interface.getMyNodeInfo()
     65 				break
     66 
     67 
     68 	def sendmsg(self, message: str, destination: int, channelIndex: int = 0):
     69 		'''
     70 		Send a message to the Meshtastic interface
     71 
     72 		:param message: The message to send
     73 		'''
     74 
     75 		if len(message) > 255:
     76 			logging.warning('Message exceeds 255 characters')
     77 			message = message[:255]
     78 
     79 		self.interface.sendText(message, destination, wantAck=True, channelIndex=channelIndex) # Do we need wantAck?
     80 
     81 		logging.info(f'Sent broadcast message: {message}')
     82 
     83 
     84 	def listen(self):
     85 		'''Create the Meshtastic callback subscriptions'''
     86 
     87 		pub.subscribe(self.event_connect,    'meshtastic.connection.established')
     88 		pub.subscribe(self.event_data,       'meshtastic.receive.data.portnum')
     89 		pub.subscribe(self.event_disconnect, 'meshtastic.connection.lost')
     90 		pub.subscribe(self.event_node,       'meshtastic.node')
     91 		pub.subscribe(self.event_position,   'meshtastic.receive.position')
     92 		pub.subscribe(self.event_text,       'meshtastic.receive.text')
     93 		pub.subscribe(self.event_user,       'meshtastic.receive.user')
     94 
     95 		logging.debug('Listening for Meshtastic events...')
     96 		
     97 		
     98 	def event_connect(self, interface, topic=pub.AUTO_TOPIC):
     99 		'''
    100 		Callback function for connection established
    101 
    102 		:param interface: Meshtastic interface
    103 		:param topic:     PubSub topic
    104 		'''
    105 
    106 		logging.info(f'Connected to the {self.me["user"]["longName"]} radio on {self.me["user"]["hwModel"]} hardware')
    107 		logging.info(f'Found a total of {len(self.nodes):,} nodes')
    108 
    109 
    110 	def event_data(self, packet: dict, interface):
    111 		'''
    112 		Callback function for data updates
    113 
    114 		:param packet: Data information
    115 		:param interface: Meshtastic interface
    116 		'''
    117 
    118 		logging.info(f'Data update: {packet}')
    119 
    120 
    121 	def event_disconnect(self, interface, topic=pub.AUTO_TOPIC):
    122 		'''
    123 		Callback function for connection lost
    124 
    125 		:param interface: Meshtastic interface
    126 		:param topic:     PubSub topic
    127 		'''
    128 
    129 		logging.warning('Lost connection to radio!')
    130 
    131 		time.sleep(10)
    132 
    133 		 # 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
    134 		self.connect('serial' if args.serial else 'tcp', args.serial if args.serial else args.tcp)
    135 
    136 	
    137 	def event_node(self, node):
    138 		'''
    139 		Callback function for node updates
    140 
    141 		:param node: Node information
    142 		'''
    143 
    144 		self.nodes[node['num']] = node
    145 
    146 		logging.info(f'Node recieved: {node["user"]["id"]} - {node["user"]["shortName"].ljust(4)} - {node["user"]["longName"]}')
    147 
    148 
    149 	def event_position(self, packet: dict, interface):
    150 		'''
    151 		Callback function for position updates
    152 
    153 		:param packet: Position information
    154 		:param interface: Meshtastic interface
    155 		'''
    156 
    157 		sender    = packet['from']
    158 		msg       = packet['decoded']['payload'].hex() # What exactly is contained in this payload?
    159 		id        = self.nodes[sender]['user']['id']       if sender in self.nodes else '!unk   '
    160 		name      = self.nodes[sender]['user']['longName'] if sender in self.nodes else 'UNK'
    161 		longitude = packet['decoded']['position']['longitudeI'] / 1e7
    162 		latitude  = packet['decoded']['position']['latitudeI'] / 1e7
    163 		altitude  = packet['decoded']['position']['altitude']
    164 		snr	      = packet['rxSnr']
    165 		rssi	  = packet['rxRssi']
    166 
    167 		logging.info(f'Position recieved: {id} - {name}: {longitude}, {latitude}, {altitude}m (SNR: {snr}, RSSI: {rssi}) - {msg}')
    168 
    169 
    170 	def event_text(self, packet: dict, interface):
    171 		'''
    172 		Callback function for received packets
    173 
    174 		:param packet: Packet received
    175 		'''
    176 
    177 		sender = packet['from']
    178 		to     = packet['to'] 
    179 		msg    = packet['decoded']['payload'].decode('utf-8')
    180 		id     = self.nodes[sender]['user']['id']       if sender in self.nodes else '!unk   '
    181 		name   = self.nodes[sender]['user']['longName'] if sender in self.nodes else 'UNK'
    182 		target = self.nodes[to]['user']['longName']     if to     in self.nodes else 'UNK'
    183 
    184 		logging.info(f'Message recieved: {id} {name} -> {target}: {msg}')
    185 		print(packet)
    186 
    187 
    188 	def event_user(self, packet: dict, interface):
    189 		'''
    190 		Callback function for user updates
    191 
    192 		:param user: User information
    193 		'''
    194 
    195 		'''
    196 		{
    197 			'from' : 862341900,
    198 			'to'   : 4294967295,
    199 			'decoded' : {
    200 				'portnum'      : 'NODEINFO_APP',
    201 				'payload'      : b'\n\t!33664b0c\x12\x08HELLDIVE\x1a\x04H3LL"\x06d\xe83fK\x0c(+8\x03',
    202 				'wantResponse' : True,
    203 				'user' : {
    204 					'id'        : '!33664b0c',
    205 					'longName'  : 'HELLDIVE',
    206 					'shortName' : 'H3LL',
    207 					'macaddr'   : 'ZOgzZksM',
    208 					'hwModel'   : 'HELTEC_V3',
    209 					'role'      : 'ROUTER_CLIENT',
    210 					'raw'       : 'rm this'
    211 				}
    212 			},
    213 			'id'       : 1612906268,
    214 			'rxTime'   : 1714279638,
    215 			'rxSnr'    : 6.25,
    216 			'hopLimit' : 3,
    217 			'rxRssi'   : -38,
    218 			'hopStart' : 3,
    219 			'raw'      : 'rm this'
    220 		}
    221 		'''
    222 
    223 		# Not sure what to do with this yet...
    224 		pass
    225 
    226 
    227 
    228 if __name__ == '__main__':
    229 	parser = argparse.ArgumentParser(description='Meshtastic Interfacing Tool')
    230 	parser.add_argument('--serial', help='Use serial interface') # Typically /dev/ttyUSB0 or /dev/ttyACM0
    231 	parser.add_argument('--tcp',    help='Use TCP interface')    # Can be an IP address or hostname (meshtastic.local)
    232 	args = parser.parse_args()
    233  
    234 	# Ensure one interface is specified
    235 	if (not args.serial and not args.tcp) or (args.serial and args.tcp):
    236 		raise SystemExit('Must specify either --serial or --tcp interface')
    237 	
    238 	# Initialize the Meshtastic client
    239 	mesh = MeshtasticClient()
    240 
    241 	# Listen for Meshtastic events
    242 	mesh.listen()
    243 
    244 	# Connect to the Meshtastic interface
    245 	mesh.connect('serial' if args.serial else 'tcp', args.serial if args.serial else args.tcp)
    246 
    247 	# Keep-alive loop
    248 	try:
    249 		while True:
    250 			time.sleep(60)
    251 	except KeyboardInterrupt:
    252 		try:
    253 			mesh.interface.close()
    254 		except:
    255 			pass
    256 	finally:
    257 		logging.info('Connection to radio lost')
    258 
    259 '''
    260 Notes:
    261 		conf = self.interface.localNode.localConfig
    262 		ok = interface.getNode('^local')
    263 		print(ok.channels)
    264 '''