├── .gitignore ├── README.md ├── cfg.py ├── config-example.yml ├── main.py ├── plugins-disabled ├── forzenbob.py ├── gpt-config-example.yml └── gpt.py ├── plugins ├── __init__.py ├── basic_commands.py ├── basic_events.py ├── info.py ├── libdiscordutil.py ├── libinfo.py ├── liblogger.py └── libmesh.py └── rev /.gitignore: -------------------------------------------------------------------------------- 1 | config.yml 2 | __pycache__/ 3 | plugins/**/__pycache__/ 4 | plugins/gpt-config.yml 5 | meshlinkvenv/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MeshLink (Beta) 2 | ## Features 3 | 4 | - Send messages to and from discord 5 | - Send packet information to discord 6 | - Plugin system 7 | 8 | ### Mesh only 9 | - Weather forecast 10 | - Ping 11 | - HF condition checker 12 | - Time 13 | - Mesh statistics 14 | 15 | ## Commands 16 | **prefix + command** 17 | ### Discord 18 | send (message) 19 | 20 | ### Mesh 21 | ping 22 | weather 23 | hf 24 | time 25 | mesh 26 | 27 | ## Setup 28 | 29 | 1. Download the python script and config-example.yml from Github 30 | 2. Rename config-example.yml to config.yml before editing (step 10) 31 | 3. Install the Meshtastic python CLI https://meshtastic.org/docs/software/python/cli/installation/ 32 | 4. Install discord py https://discordpy.readthedocs.io/en/latest/intro.html 33 | 5. Create a discord bot https://discord.com/developers (optional) 34 | 6. Give it admin permission in your server and give it read messages intent (google it if you don't know what to do) (optional) 35 | 7. Invite it to a server (optional) 36 | 8. Get the discord channel id (this is where the messages will go) (again google a tutorial if don't know how to get the channel id) (optional) 37 | 9. Get the discord bot token (optional) 38 | 10. Add your discord bot token and channel id(s) to config.yml (optional) 39 | 11. If you are using serial set `use_serial` to `True` otherwise get your nodes ip and put it into the `radio_ip` setting 40 | 12. configure config.yml to your liking 41 | 14. `python main.py` 42 | 43 | ## Updating 44 | You may receive a log in the console like this: 45 | `[INFO] New MeshLink update ready https://github.com/Murturtle/MeshLinkBeta` 46 | 47 | run `git pull https://github.com/Murturtle/MeshLinkBeta main` to pull the latest version without overriding config 48 | Make sure to increment the `rev` setting in `config.yml` or you will keep getting notified that there is an update! 49 | 50 | ## Suggestions/Feature Requests 51 | Put them in issues. 52 | -------------------------------------------------------------------------------- /cfg.py: -------------------------------------------------------------------------------- 1 | config = {} -------------------------------------------------------------------------------- /config-example.yml: -------------------------------------------------------------------------------- 1 | rev: 21 # increment after update! 2 | ignore_update_prompt: False # set this to True if you do not want to answer y/n every time there is an update (set to True if using auto restart/startup) 3 | max_message_length: 200 # max discord -> mesh message length 4 | info_channel_ids: [ 1234567890 ] # seperate using commas 5 | message_channel_ids: [ 1234567890 ] 6 | token: "SDFjLKJVLREkjslRVJRLKjVLRKJLRVjrLKVJ" # discord bot token - DO NOT SHARE 7 | prefix: "$" # mesh command prefix 8 | discord_prefix: "$" # discord prefix 9 | use_serial: True 10 | radio_ip: "192.168.1.100" 11 | send_channel_index: 0 12 | ignore_self: True # dont show your own node on discord 13 | send_packets: True # show all data not just messages 14 | verbose_packets: False # should the full packet data be shown in the console 15 | weather_lat: "45.516022" 16 | weather_long: "-122.681427" 17 | max_weather_hours: 8 # how many hours ahead to send weather info for 18 | ping_on_messages: True 19 | message_role: "@here" 20 | use_discord: True 21 | send_mesh_commands_to_discord: True 22 | send_start_stop: True # should the meshlink announcement be sent on connection and shut down 23 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from plugins import Base 2 | 3 | # dont change unless you are making a fork 4 | update_check_url = "https://raw.githubusercontent.com/Murturtle/MeshLinkBeta/main/rev" 5 | update_url = "https://github.com/Murturtle/MeshLinkBeta" 6 | import yaml 7 | import xml.dom.minidom 8 | import os 9 | from pubsub import pub 10 | import discord 11 | from meshtastic.tcp_interface import TCPInterface 12 | from meshtastic.serial_interface import SerialInterface 13 | import asyncio 14 | import time 15 | import requests 16 | import cfg 17 | import plugins.liblogger as logger 18 | import signal 19 | import plugins.libdiscordutil as DiscordUtil 20 | from datetime import datetime 21 | import math 22 | 23 | 24 | def handler(signum, frame): 25 | logger.infogreen("MeshLink is now stopping!") 26 | if(cfg.config["send_start_stop"]): 27 | interface.sendText("MeshLink is now stopping!",channelIndex = cfg.config["send_channel_index"]) 28 | exit(1) 29 | 30 | signal.signal(signal.SIGINT, handler) 31 | 32 | with open("./config.yml",'r') as file: 33 | cfg.config = yaml.safe_load(file) 34 | 35 | config_options = [ 36 | "rev", 37 | "ignore_update_prompt", 38 | "max_message_length", 39 | "message_channel_ids", 40 | "info_channel_ids", 41 | "token", 42 | "prefix", 43 | "discord_prefix", 44 | "use_serial", 45 | "radio_ip", 46 | "send_channel_index", 47 | "ignore_self", 48 | "send_packets", 49 | "verbose_packets", 50 | "weather_lat", 51 | "weather_long", 52 | "max_weather_hours", 53 | "ping_on_messages", 54 | "message_role", 55 | "use_discord", 56 | "send_mesh_commands_to_discord", 57 | "send_start_stop" 58 | ] 59 | 60 | for i in config_options: 61 | if i not in cfg.config: 62 | logger.infoimportant("Config option "+i+" missing in config.yml (check github for example)") 63 | exit() 64 | 65 | for i in cfg.config: 66 | if i not in config_options: 67 | logger.infoimportant("Config option "+i+" might not needed anymore") 68 | 69 | for plugin in Base.plugins: 70 | inst = plugin() 71 | inst.start() 72 | 73 | oversion = requests.get(update_check_url) 74 | if(oversion.ok): 75 | if(cfg.config["rev"] < int(oversion.text)): 76 | logger.infoimportant("New MeshLink update ready "+update_url) 77 | logger.warn("Remember after the update to double check the plugins you want disabled stay disabled") 78 | logger.warn("Also remember to increment rev in config.yml") 79 | if(not cfg.config["ignore_update_prompt"]): 80 | if(input(" Would you like to update MeshLink? y/n ")=="y"): 81 | logger.info("running: git pull "+update_url +" main") 82 | os.system("git pull "+update_url+" main") 83 | logger.infogreen("Start MeshLink to apply updates!") 84 | exit(1) 85 | else: 86 | logger.infoimportant("Ignoring update - use the following command to update: git pull "+update_url+" main") 87 | else: 88 | logger.warn("Failed to check for updates using url "+update_check_url+"\x1b[0m") 89 | 90 | intents = discord.Intents.default() 91 | intents.message_content = True 92 | if cfg.config["use_discord"]: 93 | client = discord.Client(intents=intents) 94 | else: 95 | client = None 96 | 97 | def onConnection(interface, topic=pub.AUTO_TOPIC): 98 | for p in Base.plugins: 99 | inst = p() 100 | inst.onConnect(interface,client) 101 | 102 | 103 | 104 | def onReceive(packet, interface): 105 | for p in Base.plugins: 106 | inst = p() 107 | inst.onReceive(packet,interface,client) 108 | 109 | def onDisconnect(interface): 110 | for p in Base.plugins: 111 | inst = p() 112 | inst.onDisconnect(interface,client) 113 | init_radio() 114 | 115 | pub.subscribe(onConnection, "meshtastic.connection.established") 116 | pub.subscribe(onDisconnect, "meshtastic.connection.lost") 117 | pub.subscribe(onReceive, "meshtastic.receive") 118 | 119 | def init_radio(): 120 | global interface 121 | logger.info("Connecting to node...") 122 | if (cfg.config["use_serial"]): 123 | interface = SerialInterface() 124 | 125 | else: 126 | interface = TCPInterface(hostname=cfg.config["radio_ip"], connectNow=True) 127 | 128 | init_radio() 129 | 130 | if cfg.config["use_discord"]: 131 | @client.event 132 | async def on_ready(): 133 | logger.info("Logged in as {0.user} on Discord".format(client)) 134 | #send_msg("ready") 135 | 136 | @client.event 137 | async def on_message(message): 138 | global interface 139 | if message.author == client.user: 140 | return 141 | if message.content.startswith(cfg.config["discord_prefix"]+'send'): 142 | if (message.channel.id in cfg.config["message_channel_ids"]): 143 | await message.channel.typing() 144 | trunk_message = message.content[len(cfg.config["discord_prefix"]+"send"):] 145 | final_message = message.author.name+">"+ trunk_message 146 | 147 | if(len(final_message) < cfg.config["max_message_length"] - 1): 148 | await message.reply(final_message) 149 | interface.sendText(final_message,channelIndex = cfg.config["send_channel_index"]) 150 | logger.infodiscord(final_message) 151 | #DiscordUtil.send_msg("DISCORD: "+final_message,client,cfg.config) 152 | else: 153 | await message.reply("(shortend) "+final_message[:cfg.config["max_message_length"]]) 154 | interface.sendText(final_message,channelIndex = cfg.config["send_channel_index"]) 155 | logger.infodiscord(final_message[:cfg.config["max_message_length"]]) 156 | #DiscordUtil.send_msg("DISCORD: "+final_message,client,cfg.config) 157 | 158 | 159 | #await message.delete() 160 | 161 | else: 162 | return 163 | 164 | try: 165 | if cfg.config["use_discord"]: 166 | client.run(cfg.config["token"],log_handler=None) 167 | else: 168 | while True: 169 | time.sleep(1) 170 | except discord.HTTPException as e: 171 | if e.status == 429: 172 | logger.warn("Discord: too many requests") -------------------------------------------------------------------------------- /plugins-disabled/forzenbob.py: -------------------------------------------------------------------------------- 1 | import plugins 2 | import plugins.libdiscordutil as DiscordUtil 3 | import cfg 4 | import plugins.liblogger as logger 5 | import plugins.libinfo as libinfo 6 | 7 | class pluginZenbob(plugins.Base): 8 | 9 | def __init__(self): 10 | pass 11 | 12 | def start(self): 13 | print("[INFO] Loading forzenbob") 14 | libinfo.info.append("zenbob - he will never give you up") 15 | 16 | def onReceive(self,packet,interface,client): 17 | final_message = "" 18 | if("decoded" in packet): 19 | if(packet["decoded"]["portnum"] == "TEXT_MESSAGE_APP"): 20 | text = packet["decoded"]["text"] 21 | if(text.startswith(cfg.config["prefix"])): 22 | noprefix = text[len(cfg.config["prefix"]):] 23 | 24 | if (noprefix.startswith("zenbob")): 25 | final_tehe = """Never gonna give you up 26 | Never gonna let you down 27 | Never gonna run around and desert you 28 | Never gonna make you cry 29 | Never gonna say goodbye 30 | Never gonna tell a lie and hurt you""" 31 | 32 | interface.sendText(final_tehe,channelIndex=cfg.config["send_channel_index"],destinationId=packet["toId"]) 33 | 34 | def onConnect(self,interface,client): 35 | pass 36 | 37 | def onDisconnect(self,interface,client): 38 | pass 39 | -------------------------------------------------------------------------------- /plugins-disabled/gpt-config-example.yml: -------------------------------------------------------------------------------- 1 | open_ai_token: "put open ai token here" 2 | -------------------------------------------------------------------------------- /plugins-disabled/gpt.py: -------------------------------------------------------------------------------- 1 | import plugins 2 | import plugins.libdiscordutil as DiscordUtil 3 | import cfg 4 | import yaml 5 | import plugins.liblogger as logger 6 | from openai import OpenAI 7 | import plugins.libmesh as LibMesh 8 | import plugins.libinfo as libinfo 9 | 10 | class gpt(plugins.Base): 11 | 12 | def __init__(self): 13 | pass 14 | 15 | def start(self): 16 | logger.info("Loading OpenAI") 17 | 18 | 19 | def gpt_setup(self): 20 | libinfo.info.append("gpt - use chatgpt") 21 | with open("./plugins/gpt-config.yml",'r') as file: 22 | cfg.gptconfig = yaml.safe_load(file) 23 | 24 | open_ai_token = cfg.gptconfig["open_ai_token"] 25 | return OpenAI(api_key=open_ai_token) 26 | 27 | def onReceive(self, packet, interface, client): 28 | """ 29 | Handles Meshtastic messages and sends a GPT response if needed. 30 | """ 31 | if "decoded" in packet and packet["decoded"].get("portnum") == "TEXT_MESSAGE_APP": 32 | incoming_message = packet["decoded"]["text"] 33 | #logger.info(f"Received message: {incoming_message}") 34 | 35 | if incoming_message.startswith(cfg.config['prefix'] + 'gpt'): 36 | prompt = incoming_message[len(cfg.config['prefix'] + 'gpt'):].strip() 37 | 38 | if prompt: 39 | ai_client = self.gpt_setup() 40 | response = ai_client.chat.completions.create( 41 | model="gpt-4o-mini", 42 | messages=[ 43 | {"role": "system", "content": "Make a short comment not exceeding 20 words" }, 44 | {"role": "user", "content": prompt} 45 | ], 46 | max_tokens=60 47 | ) 48 | 49 | gpt_response = response.choices[0].message.content.strip() 50 | 51 | # Send GPT response over Meshtastic 52 | LibMesh.sendReply(gpt_response,interface,packet) 53 | 54 | if(cfg.config["send_mesh_commands_to_discord"]): 55 | DiscordUtil.send_msg("`MeshLink`> "+gpt_response,client,cfg.config) 56 | 57 | logger.info(f"Sent GPT response: {gpt_response}") 58 | 59 | else: 60 | logger.info("No prompt provided after 'gpt' command") 61 | #else: 62 | #logger.info("Message does not contain the GPT trigger.") 63 | 64 | def onConnect(self,interface,client): 65 | pass 66 | 67 | def onDisconnect(self,interface,client): 68 | pass -------------------------------------------------------------------------------- /plugins/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import traceback 3 | from importlib import util 4 | 5 | 6 | class Base: 7 | """Basic resource class. Concrete resources will inherit from this one 8 | """ 9 | plugins = [] 10 | 11 | # For every class that inherits from the current, 12 | # the class name will be added to plugins 13 | def __init_subclass__(cls, **kwargs): 14 | super().__init_subclass__(**kwargs) 15 | cls.plugins.append(cls) 16 | 17 | 18 | # Small utility to automatically load modules 19 | def load_module(path): 20 | name = os.path.split(path)[-1] 21 | spec = util.spec_from_file_location(name, path) 22 | module = util.module_from_spec(spec) 23 | spec.loader.exec_module(module) 24 | return module 25 | 26 | 27 | # Get current path 28 | path = os.path.abspath(__file__) 29 | dirpath = os.path.dirname(path) 30 | 31 | for fname in os.listdir(dirpath): 32 | # Load only "real modules" 33 | if not fname.startswith('.') and ( not fname.startswith('__') ) and fname.endswith('.py') and not fname.startswith("lib"): 34 | try: 35 | load_module(os.path.join(dirpath, fname)) 36 | print("[INFO] Loaded file "+fname) 37 | except Exception: 38 | traceback.print_exc() -------------------------------------------------------------------------------- /plugins/basic_commands.py: -------------------------------------------------------------------------------- 1 | import plugins 2 | import plugins.libdiscordutil as DiscordUtil 3 | import xml.dom.minidom 4 | import cfg 5 | import requests 6 | import time 7 | import plugins.liblogger as logger 8 | import plugins.libinfo as libinfo 9 | from datetime import datetime 10 | import plugins.libmesh as LibMesh 11 | 12 | class basicCommands(plugins.Base): 13 | 14 | def __init__(self): 15 | pass 16 | 17 | def start(self): 18 | logger.info("Loading basic commands") 19 | libinfo.info.append("ping - pong!") 20 | libinfo.info.append("time - sends the time") 21 | libinfo.info.append("weather - gets the weather") 22 | libinfo.info.append("hf - get the hf radio conditions") 23 | libinfo.info.append("mesh - check chutil") 24 | libinfo.info.append("savepos - saves your position") 25 | logger.info("Added commands to info") 26 | 27 | def onReceive(self, packet, interface, client): 28 | if "decoded" in packet: 29 | if packet["decoded"]["portnum"] == "TEXT_MESSAGE_APP": 30 | final = None 31 | 32 | text = packet["decoded"]["text"] 33 | 34 | if text.startswith(cfg.config["prefix"]): 35 | noprefix = text[len(cfg.config["prefix"]):] 36 | 37 | if noprefix.startswith("ping"): 38 | final = "pong" 39 | LibMesh.sendReply(final,interface,packet) 40 | 41 | 42 | if noprefix.startswith("savepos"): 43 | lat, long, hasPos = LibMesh.getPosition(interface,packet) 44 | name = LibMesh.getUserLong(interface,packet) 45 | if(hasPos): 46 | final= f"{name} {lat} {long}" 47 | interface.sendWaypoint(name, description=datetime.now().strftime("%H:%M, %m/%d/%Y"),latitude=lat,longitude=long,channelIndex=cfg.config["send_channel_index"],expire=2147483647) # round(datetime.now().timestamp()+5000) 48 | else: 49 | final = "No position found!" 50 | 51 | 52 | elif noprefix.startswith("time"): 53 | final = time.strftime('%H:%M:%S') 54 | LibMesh.sendReply(final,interface,packet) 55 | 56 | 57 | elif noprefix.startswith("weather"): 58 | weather_data_res = requests.get("https://api.open-meteo.com/v1/forecast?latitude=" + cfg.config["weather_lat"] + "&longitude=" + cfg.config["weather_long"] + "&hourly=temperature_2m,precipitation_probability&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch&timeformat=unixtime&timezone=auto") 59 | weather_data = weather_data_res.json() 60 | final = "" 61 | if weather_data_res.ok: 62 | for j in range(cfg.config["max_weather_hours"]): 63 | i = j + int(time.strftime('%H')) 64 | final += str(int(i) % 24) + " " 65 | final += str(round(weather_data["hourly"]["temperature_2m"][i])) + "F " + str(weather_data["hourly"]["precipitation_probability"][i]) + "%🌧️\n" 66 | final = final[:-1] 67 | else: 68 | final += "error fetching" 69 | logger.info(final) 70 | LibMesh.sendReply(final,interface,packet) 71 | 72 | 73 | elif noprefix.startswith("hf"): 74 | final = "" 75 | solar = requests.get("https://www.hamqsl.com/solarxml.php") 76 | if solar.ok: 77 | solarxml = xml.dom.minidom.parseString(solar.text) 78 | for i in solarxml.getElementsByTagName("band"): 79 | final += i.getAttribute("time")[0] + i.getAttribute("name") + " " + str(i.childNodes[0].data) + "\n" 80 | final = final[:-1] 81 | else: 82 | final += "error fetching" 83 | logger.info(final) 84 | LibMesh.sendReply(final,interface,packet) 85 | 86 | 87 | 88 | elif noprefix.startswith("mesh"): 89 | final = "<- Mesh Stats ->" 90 | final += "\nWARNING NODES DO NOT REPORT CHUTIL WHEN CHUTIL IS HIGH" 91 | # channel utilization 92 | nodes_with_chutil = 0 93 | total_chutil = 0 94 | for i in interface.nodes: 95 | a = interface.nodes[i] 96 | if "deviceMetrics" in a: 97 | if "channelUtilization" in a['deviceMetrics']: 98 | nodes_with_chutil += 1 99 | total_chutil += a['deviceMetrics']["channelUtilization"] 100 | 101 | if nodes_with_chutil > 0: 102 | avg_chutil = total_chutil / nodes_with_chutil 103 | avg_chutil = round(avg_chutil, 1) # Round to the nearest tenth 104 | final += "\n chutil avg: " + str(avg_chutil) 105 | else: 106 | final += "\n chutil avg: N/A" 107 | 108 | 109 | 110 | LibMesh.sendReply(final,interface,packet) 111 | 112 | 113 | if(final): 114 | if cfg.config["send_mesh_commands_to_discord"]: 115 | DiscordUtil.send_msg("`MeshLink`> " + final, client, cfg.config) 116 | 117 | def onConnect(self, interface, client): 118 | pass 119 | 120 | def onDisconnect(self, interface, client): 121 | pass 122 | -------------------------------------------------------------------------------- /plugins/basic_events.py: -------------------------------------------------------------------------------- 1 | import plugins 2 | import plugins.libdiscordutil as DiscordUtil 3 | import cfg 4 | import plugins.liblogger as logger 5 | from meshtastic import mesh_interface 6 | 7 | class basicEvents(plugins.Base): 8 | 9 | def __init__(self): 10 | pass 11 | 12 | def start(self): 13 | logger.info("Loading basic events") 14 | 15 | def onReceive(self,packet,interface,client): 16 | if(cfg.config["verbose_packets"]): 17 | logger.info("############################################") 18 | logger.info(packet) 19 | logger.info("--------------------------------------------") 20 | final_message = "" 21 | if("decoded" in packet): 22 | if(cfg.config["verbose_packets"]): 23 | logger.info("Decoded") 24 | if(packet["decoded"]["portnum"] == "TEXT_MESSAGE_APP"): 25 | final_message += DiscordUtil.genUserName(interface,packet,details=False) 26 | text = packet["decoded"]["text"] 27 | 28 | if(packet["fromId"] != None): 29 | logger.infogreen(packet["fromId"]+"> "+text) 30 | else: 31 | logger.infogreen("Unknown ID> "+text) 32 | 33 | if(text.lower() == "meshlink"): 34 | interface.sendText("MeshLink is running on this node - rev "+str(cfg.config["rev"])+"\n\nuse "+cfg.config["prefix"]+"info for a list of commands",channelIndex = cfg.config["send_channel_index"]) 35 | 36 | final_message += " > "+text 37 | if(cfg.config["ping_on_messages"]): 38 | final_message += "\n||"+cfg.config["message_role"]+"||" 39 | DiscordUtil.send_msg(final_message,client,cfg.config) 40 | else: 41 | if(cfg.config["send_packets"]): 42 | try: 43 | if((packet["fromId"] == interface.getMyNodeInfo()["user"]["id"]) and cfg.config["ignore_self"]): 44 | if(cfg.config["verbose_packets"]): 45 | logger.info("Ignoring self") 46 | else: 47 | final_message+=DiscordUtil.genUserName(interface,packet)+"> "+str(packet["decoded"]["portnum"]) 48 | except TypeError as e: 49 | logger.infoimportant(f"TypeError: {e}. We don't have our own nodenum yet.") 50 | DiscordUtil.send_info(final_message,client,cfg.config) 51 | else: 52 | final_message+=DiscordUtil.genUserName(interface,packet)+" > encrypted/failed" 53 | DiscordUtil.send_info(final_message,client,cfg.config) 54 | if(cfg.config["verbose_packets"]): 55 | logger.infoimportant("Failed or encrypted") 56 | 57 | 58 | 59 | 60 | def onConnect(self,interface,client): 61 | logger.infogreen("Node connected") 62 | 63 | 64 | DiscordUtil.send_msg("MeshLink is now running - rev "+str(cfg.config["rev"]), client, cfg.config) 65 | if(cfg.config["send_start_stop"]): 66 | interface.sendText("MeshLink is now running - rev "+str(cfg.config["rev"])+"\n\nuse "+cfg.config["prefix"]+"info for a list of commands",channelIndex = cfg.config["send_channel_index"]) 67 | 68 | def onDisconnect(self,interface,client): 69 | logger.warn("Connection to node has been lost - attemping to reconnect") 70 | DiscordUtil.send_msg("# Connection to node has been lost",client, cfg.config) 71 | -------------------------------------------------------------------------------- /plugins/info.py: -------------------------------------------------------------------------------- 1 | import plugins 2 | import plugins.libdiscordutil as DiscordUtil 3 | import xml.dom.minidom 4 | import cfg 5 | import requests 6 | import time 7 | import plugins.liblogger as logger 8 | import plugins.libinfo as libinfo 9 | 10 | class pluginInfo(plugins.Base): 11 | 12 | def __init__(self): 13 | pass 14 | 15 | def start(self): 16 | logger.info("Loading info") 17 | 18 | def onReceive(self,packet,interface,client): 19 | final_message = "" 20 | if("decoded" in packet): 21 | if(packet["decoded"]["portnum"] == "TEXT_MESSAGE_APP"): 22 | text = packet["decoded"]["text"] 23 | if(text.startswith(cfg.config["prefix"])): 24 | noprefix = text[len(cfg.config["prefix"]):] 25 | 26 | if (noprefix.startswith("info")): 27 | final_info = "<- info ->" 28 | for i in libinfo.info: 29 | final_info+="\n"+i 30 | interface.sendText(final_info,channelIndex=cfg.config["send_channel_index"],destinationId=packet["toId"]) 31 | if(cfg.config["send_mesh_commands_to_discord"]): 32 | DiscordUtil.send_msg("`MeshLink`> "+final_info,client,cfg.config) 33 | 34 | def onConnect(self,interface,client): 35 | pass 36 | 37 | def onDisconnect(self,interface,client): 38 | pass -------------------------------------------------------------------------------- /plugins/libdiscordutil.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | def genUserName(interface,packet,details=True): 4 | if(packet["fromId"] in interface.nodes): 5 | if(interface.nodes[packet["fromId"]]["user"]): 6 | ret = "`"+str(interface.nodes[packet["fromId"]]["user"]["shortName"])+" " 7 | if details: 8 | ret+= packet["fromId"]+" " 9 | ret+= str(interface.nodes[packet["fromId"]]["user"]["longName"])+"`" 10 | else: 11 | ret = str(packet["fromId"]) 12 | 13 | if details: 14 | if("position" in interface.nodes[packet["fromId"]]): 15 | if("latitude" in interface.nodes[packet["fromId"]]["position"] and "longitude" in interface.nodes[packet["fromId"]]["position"]): 16 | ret +=" [map]()" 17 | if("hopLimit" in packet): 18 | if("hopStart" in packet): 19 | ret+=" `"+str(packet["hopStart"]-packet["hopLimit"])+"`/`"+str(packet["hopStart"])+"`" 20 | else: 21 | ret+=" `"+str(packet["hopLimit"])+"`" 22 | if("viaMqtt" in packet): 23 | if str(packet["viaMqtt"]) == "True": 24 | ret+=" `MQTT`" 25 | return ret 26 | else: 27 | return "`"+str(packet["fromId"])+"`" 28 | 29 | def send_msg(message,client,config): 30 | if config["use_discord"]: 31 | if (client.is_ready()): 32 | for i in config["message_channel_ids"]: 33 | asyncio.run_coroutine_threadsafe(client.get_channel(i).send(message),client.loop) 34 | 35 | def send_info(message,client,config): 36 | if config["use_discord"]: 37 | if (client.is_ready()): 38 | for i in config["info_channel_ids"]: 39 | asyncio.run_coroutine_threadsafe(client.get_channel(i).send(message),client.loop) -------------------------------------------------------------------------------- /plugins/libinfo.py: -------------------------------------------------------------------------------- 1 | info = ["info - show this screen"] -------------------------------------------------------------------------------- /plugins/liblogger.py: -------------------------------------------------------------------------------- 1 | def info(any): 2 | print("[INFO] "+str(any)) 3 | 4 | def warn(any): 5 | print("\x1b[31;49m[WARN] "+str(any)+"\x1b[0m") 6 | 7 | def infoimportant(any): 8 | print("\x1b[33;49m[INFO] "+str(any)+"\x1b[0m") 9 | 10 | def infogreen(any): 11 | print("\x1b[32;49m[INFO] "+str(any)+"\x1b[0m") 12 | 13 | def infodiscord(any): 14 | print("\x1b[35;49m[DISC] "+str(any)+"\x1b[0m") -------------------------------------------------------------------------------- /plugins/libmesh.py: -------------------------------------------------------------------------------- 1 | import cfg 2 | from meshtastic import util 3 | 4 | 5 | 6 | def getUserLong(interface,packet): 7 | ret=None 8 | node = getNode(interface,packet) 9 | if(node): 10 | if(node["user"]): 11 | ret = str(node["user"]["longName"]) 12 | else: 13 | ret = str(packet["fromId"]) 14 | 15 | return ret 16 | 17 | 18 | def getUserShort(interface,packet): 19 | ret=None 20 | node = getNode(interface,packet) 21 | if(node): 22 | if(node["user"]): 23 | ret = str(node["user"]["shortName"]) 24 | else: 25 | ret = str(packet["fromId"]) 26 | 27 | return ret 28 | 29 | 30 | def getNode(interface,packet): 31 | ret = None 32 | if(packet["fromId"] in interface.nodes): 33 | ret = interface.nodes[packet["fromId"]] 34 | return ret 35 | 36 | 37 | def getPosition(interface,packet): 38 | lat = None 39 | long = None 40 | hasPos = False 41 | 42 | node = getNode(interface,packet) 43 | if(packet["fromId"] in interface.nodes): 44 | if("position" in node): 45 | if("latitude" in node["position"] and "longitude" in node["position"]): 46 | lat = node["position"]["latitude"] 47 | long = node["position"]["longitude"] 48 | hasPos = True 49 | 50 | return lat, long, hasPos 51 | 52 | 53 | def sendReply(text, interface, packet, channelIndex = -1): 54 | ret = packet 55 | 56 | if(channelIndex == -1): 57 | channelIndex = cfg.config["send_channel_index"] 58 | 59 | to = 4294967295 # ^all 60 | 61 | if(packet["to"] == interface.localNode.nodeNum): 62 | to = packet["from"] 63 | interface.sendText(text=text,destinationId=to,channelIndex=channelIndex) 64 | 65 | return ret -------------------------------------------------------------------------------- /rev: -------------------------------------------------------------------------------- 1 | 20 2 | --------------------------------------------------------------------------------