diff --git a/README.md b/README.md index 063887b..b2d23e8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Meshtastic +# Meshtastic Utilities > Experiments with Meshtastic, MQTT, Lora, & more.... ## WORK-IN-PROGRESS @@ -25,6 +25,7 @@ The goal is to experiment with the possibilities of Python as a means of interfa ## Updates - Threw in an IRC skeleton where the serial controller will interface with. Will have to consider how handle asyncronous comms over serial... - Working reconnection on disconnection! +- Most events are handled and outputted to stdout, debugging and testing finished ___ ###### Mirrors for this repository: [acid.vegas](https://git.acid.vegas/meshtastic) • [SuperNETs](https://git.supernets.org/acidvegas/meshtastic) • [GitHub](https://github.com/acidvegas/meshtastic) • [GitLab](https://gitlab.com/acidvegas/meshtastic) • [Codeberg](https://codeberg.org/acidvegas/meshtastic) \ No newline at end of file diff --git a/meshtastic_serial.py b/meshtastic_serial.py index 7a4f717..7fabec2 100644 --- 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) +# Meshtastic Serial Interface - Developed by Acidvegas in Python (https://git.acid.vegas) 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)') @@ -21,7 +21,7 @@ except ImportError: # Initialize logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(levelname)9s | %(funcName)s | %(message)s') +logging.basicConfig(level=logging.INFO, format='%(asctime)s | %(levelname)9s | %(funcName)s | %(message)s', datefmt='%Y-%m-%d %I:%M:%S') def now(): @@ -40,11 +40,11 @@ class MeshtasticClient(object): 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 - ''' - + ''' + while True: try: if option == 'serial': @@ -54,7 +54,7 @@ class MeshtasticClient(object): else: raise Exception('No serial devices found') self.interface = SerialInterface(value) - + elif option == 'tcp': self.interface = TCPInterface(value) @@ -91,19 +91,16 @@ class MeshtasticClient(object): '''Create the Meshtastic callback subscriptions''' pub.subscribe(self.event_connect, 'meshtastic.connection.established') + pub.subscribe(self.event_data, 'meshtastic.receive.data.portnum') pub.subscribe(self.event_disconnect, 'meshtastic.connection.lost') pub.subscribe(self.event_node, 'meshtastic.node') - pub.subscribe(self.event_packet, 'meshtastic.receive') + pub.subscribe(self.event_position, 'meshtastic.receive.position') + pub.subscribe(self.event_text, 'meshtastic.receive.text') + pub.subscribe(self.event_user, 'meshtastic.receive.user') logging.debug('Listening for Meshtastic events...') - - # The meshtastic.receive topics can be broken down further: - # pub.subscribe(self.on_text, 'meshtastic.receive.text') - # pub.subscribe(self.on_position, 'meshtastic.receive.position') - # 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 @@ -116,6 +113,17 @@ class MeshtasticClient(object): logging.info(f'Found a total of {len(self.nodes):,} nodes') + def event_data(self, packet: dict, interface): + ''' + Callback function for data updates + + :param packet: Data information + :param interface: Meshtastic interface + ''' + + logging.info(f'Data update: {data}') + + def event_disconnect(self, interface, topic=pub.AUTO_TOPIC): ''' Callback function for connection lost @@ -125,15 +133,13 @@ class MeshtasticClient(object): ''' 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 @@ -146,30 +152,78 @@ class MeshtasticClient(object): logging.info(f'Node found: {node["user"]["id"]} - {node["user"]["shortName"].ljust(4)} - {node["user"]["longName"]}') - def event_packet(self, packet: dict): + def event_position(self, packet: dict, interface): + ''' + Callback function for position updates + + :param packet: Position information + :param interface: Meshtastic interface + ''' + + sender = packet['from'] + msg = packet['decoded']['payload'].hex() + id = self.nodes[sender]['user']['id'] if sender in self.nodes else '!unk ' + name = self.nodes[sender]['user']['longName'] if sender in self.nodes else 'UNK' + longitude = packet['decoded']['position']['longitudeI'] / 1e7 + latitude = packet['decoded']['position']['latitudeI'] / 1e7 + altitude = packet['decoded']['position']['altitude'] + snr = packet['rxSnr'] + rssi = packet['rxRssi'] + + logging.info(f'{id} - {name}: {longitude}, {latitude}, {altitude}m (SNR: {snr}, RSSI: {rssi}) - {msg}') + + + def event_text(self, packet: dict, interface): ''' Callback function for received packets :param packet: Packet received ''' - # Handle incoming text messages - if packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP': - sender = packet['from'] - msg = packet['decoded']['payload'].decode('utf-8') + sender = packet['from'] + msg = packet['decoded']['payload'].decode('utf-8') + id = self.nodes[sender]['user']['id'] if sender in self.nodes else '!unk ' + name = self.nodes[sender]['user']['longName'] if sender in self.nodes else 'UNK' + logging.info(f'{id} - {name}: {msg}') - # Message from self - if sender == self.interface.myInfo.my_node_num: - print(f'{now()} {self.nodes[sender]}: {msg}') # Can do custom formatting here or ignore the message, just an example - # Message from others - if sender in self.nodes: - print(f'{now()} {self.nodes[sender]}: {msg}') + def event_user(self, packet: dict, interface): + ''' + Callback function for user updates - # Unknown sender - else: - # TODO: Trigger request for node update here - print(f'{now()} UNK: {msg}') + :param user: User information + ''' + + ''' + { + 'from' : 862341900, + 'to' : 4294967295, + 'decoded' : { + 'portnum' : 'NODEINFO_APP', + 'payload' : b'\n\t!33664b0c\x12\x08HELLDIVE\x1a\x04H3LL"\x06d\xe83fK\x0c(+8\x03', + 'wantResponse' : True, + 'user' : { + 'id' : '!33664b0c', + 'longName' : 'HELLDIVE', + 'shortName' : 'H3LL', + 'macaddr' : 'ZOgzZksM', + 'hwModel' : 'HELTEC_V3', + 'role' : 'ROUTER_CLIENT', + 'raw' : 'rm this' + } + }, + 'id' : 1612906268, + 'rxTime' : 1714279638, + 'rxSnr' : 6.25, + 'hopLimit' : 3, + 'rxRssi' : -38, + 'hopStart' : 3, + 'raw' : 'rm this' + } + ''' + + # Not sure what to do with this yet... + pass @@ -178,11 +232,11 @@ if __name__ == '__main__': 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() - + # 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() @@ -197,11 +251,10 @@ if __name__ == '__main__': while True: time.sleep(60) except KeyboardInterrupt: - pass # Exit the loop on Ctrl+C + try: + mesh.interface.close() + except: + pass finally: - if mesh.interface: - try: - mesh.interface.close() - logging.info('Connection to radio interface closed!') - except: - pass + logging.info('Connection to radio lost') +