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