diff --git a/README.md b/README.md index b2d23e8..a8fea95 100644 --- a/README.md +++ b/README.md @@ -3,29 +3,26 @@ ## WORK-IN-PROGRESS -Here I will be just throw up random bits of code I write as I experiment more with these Meshtastic devices. +## Information +This repository serves as a collection of resources created in my journey to learn & utilize [LoRa](https://en.wikipedia.org/wiki/LoRa) based communications with [Meshtastic](https://meshtastic.org). -Currently using a [Lilygo T-Deck](https://www.lilygo.cc/products/t-deck) & a [Heltec Lora 32 v3](https://heltec.org/project/wifi-lora-32-v3/) for testing. +The goal here is to create simple & clean modules to interface with the hardware in a way that can be used to expand the possibilities of the devices capabilities. -I am waiting on a [Lilygo T-Beam](https://www.lilygo.cc/products/t-beam-v1-1-esp32-lora-module) & a [RAK Wireless 4631](https://store.rakwireless.com/products/wisblock-core-modules?variant=42440631419078) to arrive for expanding my network & conducting more testing. +The hardware I am experimenting with: [Lilygo T-Deck](https://www.lilygo.cc/products/t-deck), [Lilygo T-Beam](https://www.lilygo.cc/products/t-beam-v1-1-esp32-lora-module), [Heltec Lora 32 v3](https://heltec.org/project/wifi-lora-32-v3/), and [RAK Wireless 4631](https://store.rakwireless.com/products/wisblock-core-modules?variant=42440631419078) -Reading device packets over serial or TCP allows you to see the decoded data easily & fast. I have plans to add MQTT interfacing to this repository, but note that this requires you to decrypt incomming packet payloads using the channels PSK. We will get to that in the future. -The goal is to experiment with the possibilities of Python as a means of interfacing with a Meshtastic device, playing with basic I/O operations, etc. My first project is going to be a relay for IRC & Meshtastic to communicate. - -## Links -- [T-Deck 3D Printed Case](https://www.printables.com/model/741124-lilygo-t-deck-case) - -# Notes -- T-Deck is must have Wifi turned off when going mobiel *(Unstable UI & connection)* +## Hardware & Software related Issues +- T-Deck must have Wifi turned off when going mobile. Upon leaving my house with WiFi still enabled, the UI & connection was EXTREMELY laggy & poor. Couldn't even type well... - T-Deck using a custom MQTT with TLS & auth will cause a reboot loop *(Need to fix this ASAP)* -- Add support for interfacing over MQTT with decryption support -- `on_node` event is called **AS** we are defining the interface, so using `self.interface` in that callback will error +- `event_node` event is called **AS** we are defining the interface, so using `self.interface` in that callback will error. + +## Roadmap +- Asyncronous meshtastic interface *(Priority)* +- MQTT interface with working decryption +- Documentation on MQTT bridging for high availability +- Create a simple setup script to provision new devices over serial +- Bridge for IRC to allow channel messages to relay over Meshtastic & all Meshtastic events to relay into IRC. *(IRC to Meshtastic will require a command like `!mesh ` to avoid overloading the traffic over LoRa)* -## 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/meshirc.py b/meshirc.py index 71d995b..002759f 100644 --- a/meshirc.py +++ b/meshirc.py @@ -4,7 +4,6 @@ import argparse import asyncio import logging -import logging.handlers import ssl import time @@ -33,6 +32,10 @@ grey = '14' light_grey = '15' +# Logging Configuration +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(funcName)s - %(message)s') + + def color(msg: str, foreground: str, background: str = None) -> str: ''' Color a string with the specified foreground and background colors. @@ -45,29 +48,9 @@ def color(msg: str, foreground: str, background: str = None) -> str: return f'\x03{foreground},{background}{msg}{reset}' if background else f'\x03{foreground}{msg}{reset}' -def ssl_ctx(verify: bool = False, cert_path: str = None, cert_pass: str = None) -> ssl.SSLContext: - ''' - Create a SSL context for the connection. - - :param verify: Verify the SSL certificate. - :param cert_path: The path to the SSL certificate. - :param cert_pass: The password for the SSL certificate. - ''' - - ctx = ssl.create_default_context() if verify else ssl._create_unverified_context() - - if cert_path: - ctx.load_cert_chain(cert_path) if not cert_pass else ctx.load_cert_chain(cert_path, cert_pass) - - return ctx - - class Bot(): def __init__(self): self.nickname = 'MESHTASTIC' - self.username = 'MESHT' # MESHT@STIC - self.realname = 'git.acid.vegas/meshtastic' - self.connected = False self.reader = None self.writer = None self.last = time.time() @@ -114,17 +97,14 @@ class Bot(): 'host' : args.server, 'port' : args.port, 'limit' : 1024, - 'ssl' : ssl_ctx() if args.ssl else None, - 'family' : 10 if args.v6 else 2, - 'local_addr' : args.vhost if args.vhost else None # Can we just leave this as args.vhost? + 'ssl' : ssl._create_unverified_context() if args.ssl else None, # TODO: Do not use the args variable here + 'family' : 2, # AF_INET = 2, AF_INET6 = 10 + 'local_addr' : None } self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), 15) - if args.password: - await self.raw('PASS ' + args.password) - - await self.raw(f'USER {self.username} 0 * :{self.realname}') + await self.raw(f'USER MESHT 0 * :git.acid.vegas/meshtastic') # Static for now await self.raw('NICK ' + self.nickname) while not self.reader.at_eof(): @@ -146,20 +126,13 @@ class Bot(): ''' parts = data.split() + ident = parts[0][1:] nick = parts[0].split('!')[0][1:] target = parts[2] msg = ' '.join(parts[3:])[1:] - if target == self.nickname: - if ident == 'acidvegas!stillfree@big.dick.acid.vegas': - if msg.startswith('!raw') and len(msg.split()) > 1: - option = ' '.join(msg.split()[1:]) - await self.raw(option) - else: - await self.sendmsg(nick, 'Do NOT message me!') - - if target.startswith('#'): + if target == args.channel: # TODO: Don't use the args variable here if msg.startswith('!'): if time.time() - self.last < 3: if not self.slow: @@ -167,33 +140,16 @@ class Bot(): await self.sendmsg(target, color('Slow down nerd!', red)) else: self.slow = False - if msg == '!help': - await self.action(target, 'explodes') - elif msg == '!ping': - await self.sendmsg(target, 'Pong!') - elif msg.startswith('!mesh') and len(msg.split()) > 1: - message = ' '.join(msg.split()[1:]) - # Implement outgoing meshtastic message here - #await self.sendmesh(message) + parts = msg.split() + if parts[0] == '!meshage' and len(parts) > 1: + message = ' '.join(parts[1:]) + if len(message) > 255: + await self.sendmsg(target, color('Message exceeds 255 bytes nerd!', red)) + # TODO: Send a meshtastic message (We have to ensure our outbounds from IRC don't loop back into IRC) + self.last = time.time() # Update the last command time if it starts with ! character to prevent command flooding - async def eventConnect(self): - '''Callback function for radio connection established.''' - - self.connected = True - - await self.action(args.channel, 'connected to the radio!') - - - async def eventDisconnect(): - '''Callback function for radio connection lost.''' - - self.connected = False - - await self.action(args.channel, 'disconnected from the radio!') - - async def handle(self, data: str): ''' Handle the data received from the IRC server. @@ -206,16 +162,13 @@ class Bot(): try: parts = data.split() - if data.startswith('ERROR :Closing Link:'): - raise Exception('BANNED') - if parts[0] == 'PING': await self.raw('PONG ' + parts[1]) elif parts[1] == '001': # RPL_WELCOME await self.raw(f'MODE {self.nickname} +B') await self.sendmsg('NickServ', f'IDENTIFY {self.nickname} simps0nsfan420') - await asyncio.sleep(10) + await asyncio.sleep(10) # Wait for NickServ to identify or any channel join delays await self.raw(f'JOIN {args.channel} {args.key if args.key else ""}') elif parts[1] == '433': # ERR_NICKNAMEINUSE @@ -245,36 +198,14 @@ class Bot(): logging.exception(f'Unknown error has occured! ({ex})') -def setup_logger(log_filename: str, to_file: bool = False): - ''' - Set up logging to console & optionally to file. - - :param log_filename: The filename of the log file - :param to_file: Whether or not to log to a file - ''' - - sh = logging.StreamHandler() - sh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(message)s', '%I:%M %p')) - if to_file: - fh = logging.handlers.RotatingFileHandler(log_filename+'.log', maxBytes=250000, backupCount=3, encoding='utf-8') # Max size of 250KB, 3 backups - fh.setFormatter(logging.Formatter('%(asctime)s | %(levelname)9s | %(filename)s.%(funcName)s.%(lineno)d | %(message)s', '%Y-%m-%d %I:%M %p')) # We can be more verbose in the log file - logging.basicConfig(level=logging.NOTSET, handlers=(sh,fh)) - else: - logging.basicConfig(level=logging.NOTSET, handlers=(sh,)) - - if __name__ == '__main__': parser = argparse.ArgumentParser(description='Connect to an IRC server.') parser.add_argument('server', help='The IRC server address.') parser.add_argument('channel', help='The IRC channel to join.') - parser.add_argument('--password', help='The password for the IRC server.') parser.add_argument('--port', type=int, help='The port number for the IRC server.') parser.add_argument('--ssl', action='store_true', help='Use SSL for the connection.') - parser.add_argument('--v4', action='store_true', help='Use IPv4 for the connection.') - parser.add_argument('--v6', action='store_true', help='Use IPv6 for the connection.') parser.add_argument('--key', default='', help='The key (password) for the IRC channel, if required.') - parser.add_argument('--vhost', help='The VHOST to use for connection.') args = parser.parse_args() if not args.channel.startswith('#'): @@ -282,11 +213,11 @@ if __name__ == '__main__': if not args.port: args.port = 6697 if args.ssl else 6667 + elif args.port < 1 or args.port > 65535: + raise ValueError('Port must be between 1 and 65535.') print(f'Connecting to {args.server}:{args.port} (SSL: {args.ssl}) and joining {args.channel} (Key: {args.key or 'None'})') - setup_logger('skeleton', to_file=True) - bot = Bot() asyncio.run(bot.connect()) \ No newline at end of file diff --git a/meshtastic_serial.py b/meshtastic_serial.py index 7fabec2..9236a04 100644 --- a/meshtastic_serial.py +++ b/meshtastic_serial.py @@ -181,10 +181,14 @@ class MeshtasticClient(object): ''' sender = packet['from'] + to = packet['to'] 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}') + target = self.nodes[to]['user']['longName'] if to in self.nodes else 'UNK' + + logging.info(f'{id} {name} -> {target}: {msg}') + print(packet) def event_user(self, packet: dict, interface): @@ -258,3 +262,9 @@ if __name__ == '__main__': finally: logging.info('Connection to radio lost') +''' +Notes: + conf = self.interface.localNode.localConfig + ok = interface.getNode('^local') + print(ok.channels) +''' \ No newline at end of file