IRC skeleton improved and stripped down, meshtastic serial interfacing mostly complete

This commit is contained in:
Dionysus 2024-04-28 16:34:24 -04:00
parent ce9519afda
commit 3fabecb8aa
Signed by: acidvegas
GPG Key ID: EF4B922DB85DC9DE
3 changed files with 45 additions and 107 deletions

View File

@ -3,29 +3,26 @@
## WORK-IN-PROGRESS ## 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. ## 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...
## 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)*
- T-Deck using a custom MQTT with TLS & auth will cause a reboot loop *(Need to fix this ASAP)* - 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 - `event_node` event is called **AS** we are defining the interface, so using `self.interface` in that callback will error.
- `on_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 <message here>` 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) ###### 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)

View File

@ -4,7 +4,6 @@
import argparse import argparse
import asyncio import asyncio
import logging import logging
import logging.handlers
import ssl import ssl
import time import time
@ -33,6 +32,10 @@ grey = '14'
light_grey = '15' 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: def color(msg: str, foreground: str, background: str = None) -> str:
''' '''
Color a string with the specified foreground and background colors. 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}' 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(): class Bot():
def __init__(self): def __init__(self):
self.nickname = 'MESHTASTIC' self.nickname = 'MESHTASTIC'
self.username = 'MESHT' # MESHT@STIC
self.realname = 'git.acid.vegas/meshtastic'
self.connected = False
self.reader = None self.reader = None
self.writer = None self.writer = None
self.last = time.time() self.last = time.time()
@ -114,17 +97,14 @@ class Bot():
'host' : args.server, 'host' : args.server,
'port' : args.port, 'port' : args.port,
'limit' : 1024, 'limit' : 1024,
'ssl' : ssl_ctx() if args.ssl else None, 'ssl' : ssl._create_unverified_context() if args.ssl else None, # TODO: Do not use the args variable here
'family' : 10 if args.v6 else 2, 'family' : 2, # AF_INET = 2, AF_INET6 = 10
'local_addr' : args.vhost if args.vhost else None # Can we just leave this as args.vhost? 'local_addr' : None
} }
self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), 15) self.reader, self.writer = await asyncio.wait_for(asyncio.open_connection(**options), 15)
if args.password: await self.raw(f'USER MESHT 0 * :git.acid.vegas/meshtastic') # Static for now
await self.raw('PASS ' + args.password)
await self.raw(f'USER {self.username} 0 * :{self.realname}')
await self.raw('NICK ' + self.nickname) await self.raw('NICK ' + self.nickname)
while not self.reader.at_eof(): while not self.reader.at_eof():
@ -146,20 +126,13 @@ class Bot():
''' '''
parts = data.split() parts = data.split()
ident = parts[0][1:] ident = parts[0][1:]
nick = parts[0].split('!')[0][1:] nick = parts[0].split('!')[0][1:]
target = parts[2] target = parts[2]
msg = ' '.join(parts[3:])[1:] msg = ' '.join(parts[3:])[1:]
if target == self.nickname: if target == args.channel: # TODO: Don't use the args variable here
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 msg.startswith('!'): if msg.startswith('!'):
if time.time() - self.last < 3: if time.time() - self.last < 3:
if not self.slow: if not self.slow:
@ -167,33 +140,16 @@ class Bot():
await self.sendmsg(target, color('Slow down nerd!', red)) await self.sendmsg(target, color('Slow down nerd!', red))
else: else:
self.slow = False self.slow = False
if msg == '!help': parts = msg.split()
await self.action(target, 'explodes') if parts[0] == '!meshage' and len(parts) > 1:
elif msg == '!ping': message = ' '.join(parts[1:])
await self.sendmsg(target, 'Pong!') if len(message) > 255:
elif msg.startswith('!mesh') and len(msg.split()) > 1: await self.sendmsg(target, color('Message exceeds 255 bytes nerd!', red))
message = ' '.join(msg.split()[1:]) # TODO: Send a meshtastic message (We have to ensure our outbounds from IRC don't loop back into IRC)
# Implement outgoing meshtastic message here
#await self.sendmesh(message)
self.last = time.time() # Update the last command time if it starts with ! character to prevent command flooding 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): async def handle(self, data: str):
''' '''
Handle the data received from the IRC server. Handle the data received from the IRC server.
@ -206,16 +162,13 @@ class Bot():
try: try:
parts = data.split() parts = data.split()
if data.startswith('ERROR :Closing Link:'):
raise Exception('BANNED')
if parts[0] == 'PING': if parts[0] == 'PING':
await self.raw('PONG ' + parts[1]) await self.raw('PONG ' + parts[1])
elif parts[1] == '001': # RPL_WELCOME elif parts[1] == '001': # RPL_WELCOME
await self.raw(f'MODE {self.nickname} +B') await self.raw(f'MODE {self.nickname} +B')
await self.sendmsg('NickServ', f'IDENTIFY {self.nickname} simps0nsfan420') 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 ""}') await self.raw(f'JOIN {args.channel} {args.key if args.key else ""}')
elif parts[1] == '433': # ERR_NICKNAMEINUSE elif parts[1] == '433': # ERR_NICKNAMEINUSE
@ -245,36 +198,14 @@ class Bot():
logging.exception(f'Unknown error has occured! ({ex})') 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__': if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Connect to an IRC server.') parser = argparse.ArgumentParser(description='Connect to an IRC server.')
parser.add_argument('server', help='The IRC server address.') parser.add_argument('server', help='The IRC server address.')
parser.add_argument('channel', help='The IRC channel to join.') 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('--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('--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('--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() args = parser.parse_args()
if not args.channel.startswith('#'): if not args.channel.startswith('#'):
@ -282,11 +213,11 @@ if __name__ == '__main__':
if not args.port: if not args.port:
args.port = 6697 if args.ssl else 6667 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'})') 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() bot = Bot()
asyncio.run(bot.connect()) asyncio.run(bot.connect())

View File

@ -181,10 +181,14 @@ class MeshtasticClient(object):
''' '''
sender = packet['from'] sender = packet['from']
to = packet['to']
msg = packet['decoded']['payload'].decode('utf-8') msg = packet['decoded']['payload'].decode('utf-8')
id = self.nodes[sender]['user']['id'] if sender in self.nodes else '!unk ' 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' 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): def event_user(self, packet: dict, interface):
@ -258,3 +262,9 @@ if __name__ == '__main__':
finally: finally:
logging.info('Connection to radio lost') logging.info('Connection to radio lost')
'''
Notes:
conf = self.interface.localNode.localConfig
ok = interface.getNode('^local')
print(ok.channels)
'''