Compare commits

...

23 Commits

Author SHA1 Message Date
SpudGunMan 50fdcf486d Update system.py 2025-10-13 23:21:50 -07:00
SpudGunMan eab5afccc8 Update system.py
helps to hit save
2025-10-13 21:31:10 -07:00
SpudGunMan ea9db47c2d refactor sysinfo local telemetry 2025-10-13 21:29:45 -07:00
SpudGunMan cf3a9c5b43 Update filemon.py 2025-10-13 19:49:43 -07:00
SpudGunMan adedaa092c Update mesh_bot.py
fixLocaStats and sysinfo
2025-10-13 19:49:24 -07:00
SpudGunMan f204237a63 Update mesh_bot.py 2025-10-13 19:27:58 -07:00
SpudGunMan 057a400041 Update mesh_bot.py 2025-10-13 19:26:53 -07:00
SpudGunMan 4cdf68f074 fixLocaStats and sysinfo 2025-10-13 19:24:37 -07:00
SpudGunMan 003a11c557 fixReportingEngine
This data is used by the webReporting engine
2025-10-13 17:57:20 -07:00
SpudGunMan 8d309fa579 Update README.md 2025-10-13 17:42:35 -07:00
SpudGunMan 232f9c24db aaahhhrrg 2025-10-13 17:27:51 -07:00
SpudGunMan 39dccd149b Update mesh_bot.py 2025-10-13 17:26:45 -07:00
SpudGunMan b921c73fa7 Update mesh_bot.py 2025-10-13 17:26:08 -07:00
SpudGunMan f3ec1cbe93 enhance 2025-10-13 17:23:49 -07:00
SpudGunMan a6bcfda0ac enhance 2025-10-13 17:20:56 -07:00
SpudGunMan 51cd2002af Update system.py 2025-10-13 17:13:37 -07:00
SpudGunMan b40f41f41c bannode
bad node! this isnt saving to .ini
2025-10-13 17:12:27 -07:00
SpudGunMan 4c33b30f14 addMessageData
Co-Authored-By: Martin Bogomolni <martinbogo@igotu.com>
2025-10-13 15:22:29 -07:00
SpudGunMan b7490afb99 Update llm.py 2025-10-13 15:03:42 -07:00
SpudGunMan 8b57ed727c Update mesh_bot.py 2025-10-13 13:50:07 -07:00
SpudGunMan fd5d64b9fb 🫖
enhance
2025-10-13 13:14:32 -07:00
SpudGunMan 00af152c2c Update system.py
slowing this a bit
2025-10-13 12:28:41 -07:00
SpudGunMan 31f0abc8c8 requestPosition
alsoRequesting feedback if this works well? you will need to edit the file find the `reqLocationEnabled` and set True. save and test it out
2025-10-13 12:00:36 -07:00
9 changed files with 292 additions and 107 deletions
+2
View File
@@ -114,6 +114,7 @@ git clone https://github.com/spudgunman/meshing-around
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
| `echo` | Echo string back, disabled by default | ✅ |
| `bannode` | Admin option to prevent a node from using bot. `bannode list` will load and use the data/bbs_ban_list.txt db | ✅ |
### Radio Propagation & Weather Forecasting
| Command | Description | |
@@ -259,6 +260,7 @@ lon = -123.0
fuzzConfigLocation = True
# Fuzz all values in all data
fuzzItAll = False
UseMeteoWxAPI = True
coastalEnabled = False # NOAA Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
+1
View File
@@ -127,6 +127,7 @@ alert_interface = 1
[sentry]
# detect anyone close to the bot
SentryEnabled = True
reqLocationEnabled = False
emailSentryAlerts = False
# radius in meters to detect someone close to the bot
SentryRadius = 100
+12 -7
View File
@@ -29,6 +29,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
"askai": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
"bannode": lambda: handle_bbsban(message, message_from_id, isDM),
"bbsack": lambda: bbs_sync_posts(message, message_from_id, deviceID),
"bbsdelete": lambda: handle_bbsdelete(message, message_from_id),
"bbshelp": bbs_help,
@@ -152,7 +153,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
else:
bot_response = restrictedResponse
else:
logger.debug(f"System: Bot detected Commands:{cmds} From: {get_name_from_number(message_from_id)}")
logger.debug(f"System: Bot detected Commands:{cmds} From: {get_name_from_number(message_from_id)} isDM:{isDM}")
# run the first command after sorting
bot_response = command_handler[cmds[0]['cmd']]()
# append the command to the cmdHistory list for lheard and history
@@ -314,7 +315,7 @@ def handle_echo(message, message_from_id, deviceID, isDM, channel_number):
parts = message.lower().split("echo ", 1)
if len(parts) > 1 and parts[1].strip() != "":
echo_msg = parts[1]
if channel_number != echoChannel:
if channel_number != echoChannel and not isDM:
echo_msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + echo_msg
return echo_msg
else:
@@ -402,7 +403,7 @@ def handle_howtall(message, message_from_id, deviceID, isDM):
shadow_length = float(message.lower().split("howtall ")[1].split(" ")[0])
except:
return f"Please provide a shadow length in {measure} example: howtall 5.5"
# get data
msg = measureHeight(lat, lon, shadow_length)
@@ -1152,7 +1153,7 @@ def sysinfo(message, message_from_id, deviceID):
if enable_runShellCmd and file_monitor_enabled:
# get the system information from the shell script
# this is an example of how to run a shell script and return the data
shellData = call_external_script(None, "script/sysEnv.sh")
shellData = call_external_script('', "script/sysEnv.sh")
# check if the script returned data
if shellData == "" or shellData == None:
# no data returned from the script
@@ -1536,7 +1537,11 @@ def onReceive(packet, interface):
#print (f"calculated hop count: {hop_start} - {hop_limit} = {hop_count}")
hop = f"{hop_count} hops"
# check with stringSafeChecker if the message is safe
if stringSafeCheck(message_string) is False:
logger.warning(f"System: Possibly Unsafe Message from {get_name_from_number(message_from_id, 'long', rxNode)}")
if help_message in message_string or welcome_message in message_string or "CMD?:" in message_string:
# ignore help and welcome messages
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
@@ -1745,7 +1750,7 @@ async def start_rx():
if wikipedia_enabled:
if use_kiwix_server:
logger.debug(f"System: Wikipedia search Enabled using Kiwix server at {kiwix_server_address}")
logger.debug(f"System: Wikipedia search Enabled using Kiwix server at {kiwix_url}")
else:
logger.debug("System: Wikipedia search Enabled")
@@ -1756,7 +1761,7 @@ async def start_rx():
logger.debug(f"System: MOTD Enabled using {MOTD} scheduler:{schedulerMotd}")
if sentry_enabled:
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel} requestLOC:{reqLocationEnabled}")
if highfly_enabled:
logger.debug(f"System: HighFly Enabled using {highfly_altitude}m limit reporting to channel:{highfly_channel}")
+6 -2
View File
@@ -88,7 +88,11 @@ def bbs_delete_message(messageID = 0, fromNode = 0):
else:
return "Please specify a message number to delete."
def bbs_post_message(subject, message, fromNode):
def bbs_post_message(subject, message, fromNode, threadID=0, replytoID=0):
# post a message to the bbsdb
now = today.strftime('%Y-%m-%d %H:%M:%S')
thread = threadID
replyto = replytoID
# post a message to the bbsdb and assign a messageID
messageID = len(bbs_messages) + 1
@@ -106,7 +110,7 @@ def bbs_post_message(subject, message, fromNode):
return "Message posted. ID is: " + str(messageID)
# validate its not overlength by keeping in chunker limit
# append the message to the list
bbs_messages.append([messageID, subject, message, fromNode])
bbs_messages.append([messageID, subject, message, fromNode, now, thread, replyto])
logger.info(f"System: NEW Message Posted, subject: {subject}, message: {message} from {fromNode}")
# save the bbsdb
+9 -3
View File
@@ -72,7 +72,6 @@ async def watch_file():
def call_external_script(message, script="script/runShell.sh"):
# Call an external script with the message as an argument this is a example only
try:
# Debugging: Print the current working directory and resolved script path
current_working_directory = os.getcwd()
script_path = os.path.join(current_working_directory, script)
@@ -82,8 +81,15 @@ def call_external_script(message, script="script/runShell.sh"):
if not os.path.exists(script_path):
logger.warning(f"FileMon: Script not found: {script_path}")
return "sorry I can't do that"
output = os.popen(f"bash {script_path} {message}").read().encode('utf-8').decode('utf-8')
# Use subprocess.run for better resource management
result = subprocess.run(
["bash", script_path, message],
capture_output=True,
text=True,
timeout=10
)
output = result.stdout.strip()
return output
except Exception as e:
logger.warning(f"FileMon: Error calling external script: {e}")
+44 -17
View File
@@ -3,6 +3,8 @@
# 2025
from modules.log import *
import random
import time
# to molly and jake, I miss you both so much.
if disable_emojis_in_games:
@@ -47,6 +49,10 @@ class TicTacToe:
ret += self.show_board(id)
ret += "Pick 1-9:"
return ret
def rndTeaPrice(self, tea=42):
"""Return a random tea between 0 and tea."""
return random.uniform(0, tea)
def show_board(self, id):
"""Display compact board with move numbers"""
@@ -90,19 +96,30 @@ class TicTacToe:
return True
def bot_move(self, id):
"""AI makes a move"""
"""AI makes a move: tries to win, block, or pick random"""
g = self.game[id]
# Simple AI: Try to win, block, or pick random
move = self.find_winning_move(id, O) # Try to win
if move == -1:
move = self.find_winning_move(id, X) # Block player
if move == -1:
move = self.find_random_move(id) # Random move
board = g["board"]
# Try to win
move = self.find_winning_move(id, O)
if move != -1:
g["board"][move] = O
return move
board[move] = O
return move
# Try to block player
move = self.find_winning_move(id, X)
if move != -1:
board[move] = O
return move
# Pick random move
move = self.find_random_move(id)
if move != -1:
board[move] = O
return move
# No moves possible
return -1
def find_winning_move(self, id, player):
"""Find a winning move for the given player"""
@@ -117,12 +134,22 @@ class TicTacToe:
return i
board[i] = " "
return -1
def find_random_move(self, id):
"""Find a random empty position"""
g = self.game[id]
empty = [i for i in range(9) if g["board"][i] == " "]
return random.choice(empty) if empty else -1
def find_random_move(self, id: str, tea_price: float = 42.0) -> int:
"""Find a random empty position, using time and tea_price for extra randomness."""
board = self.game[id]["board"]
empty = [i for i, cell in enumerate(board) if cell == " "]
current_time = time.time()
from_china = self.rndTeaPrice(time.time() % 7) # Correct usage
tea_price = from_china
tea_price = (42 * 7) - (13 / 2) + (tea_price % 5)
if not empty:
return -1
# Combine time and tea_price for a seed
seed = int(current_time * 1000) ^ int(tea_price * 1000)
local_random = random.Random(seed)
local_random.shuffle(empty)
return empty[0]
def check_winner_on_board(self, board):
"""Check winner on given board state"""
+4
View File
@@ -85,6 +85,10 @@ def llm_query(input, nodeID=0, location_name=None):
if input == " " and rawLLMQuery:
logger.warning("System: These LLM models lack a traditional system prompt, they can be verbose and not very helpful be advised.")
input = meshbotAIinit
else:
input = input.strip()
# classic model for gemma2, deepseek-r1, etc
logger.debug(f"System: Using classic LLM model framework, ideally for gemma2, deepseek-r1, etc")
if not location_name:
location_name = "no location provided "
+1
View File
@@ -268,6 +268,7 @@ try:
highfly_ignoreList = config['sentry'].get('highFlyingIgnoreList', '').split(',') # default empty
highfly_check_openskynetwork = config['sentry'].getboolean('highflyOpenskynetwork', True) # default True check with OpenSkyNetwork if highfly detected
detctionSensorAlert = config['sentry'].getboolean('detectionSensorAlert', False) # default False
reqLocationEnabled = config['sentry'].getboolean('reqLocationEnabled', False) # default False
# location
location_enabled = config['location'].getboolean('enabled', True)
+213 -78
View File
@@ -14,7 +14,7 @@ import io # for suppressing output on watchdog
from modules.log import *
# Global Variables
trap_list = ("cmd","cmd?") # default trap list
trap_list = ("cmd","cmd?","bannode",) # base commands
help_message = "Bot CMD?:"
asyncLoop = asyncio.new_event_loop()
games_enabled = False
@@ -546,7 +546,8 @@ def get_node_location(nodeID, nodeInt=1, channel=0, round_digits=2):
if fuzzItAll:
latitude = round(latitude, round_digits)
longitude = round(longitude, round_digits)
logger.debug(f"System: Fuzzed location data for {nodeID}")
logger.debug(f"System: Fuzzed location data for {nodeID} is {latitude}, {longitude}")
logger.debug(f"System: Location data for {nodeID} is {latitude}, {longitude}")
return [latitude, longitude]
except Exception as e:
logger.warning(f"System: Error processing position for node {nodeID}: {e}")
@@ -557,49 +558,60 @@ def get_node_location(nodeID, nodeInt=1, channel=0, round_digits=2):
else:
return config_position
def get_closest_nodes(nodeInt=1,returnCount=3):
interface = globals()[f'interface{nodeInt}']
node_list = []
async def get_closest_nodes(nodeInt=1,returnCount=3, channel=publicChannel):
interface = globals()[f'interface{nodeInt}']
node_list = []
if interface.nodes:
for node in interface.nodes.values():
if 'position' in node:
try:
nodeID = node['num']
latitude = node['position']['latitude']
longitude = node['position']['longitude']
if interface.nodes:
for node in interface.nodes.values():
if 'position' in node:
try:
nodeID = node['num']
latitude = node['position']['latitude']
longitude = node['position']['longitude']
#lastheard time in unix time
lastheard = node.get('lastHeard', 0)
#if last heard is over 24 hours ago, ignore the node
if lastheard < (time.time() - 86400):
continue
#lastheard time in unix time
lastheard = node.get('lastHeard', 0)
#if last heard is over 24 hours ago, ignore the node
if lastheard < (time.time() - 86400):
continue
# Calculate distance to node from config.ini location
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
if (distance < sentry_radius):
if (nodeID not in [globals().get(f'myNodeNum{i}') for i in range(1, 10)]) and str(nodeID) not in sentryIgnoreList:
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
except Exception as e:
pass
# else:
# # request location data
# try:
# logger.debug(f"System: Requesting location data for {node['id']}")
# interface.sendPosition(destinationId=node['id'], wantResponse=False, channelIndex=publicChannel)
# except Exception as e:
# logger.error(f"System: Error requesting location data for {node['id']}. Error: {e}")
# sort by distance closest
#node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2)
node_list.sort(key=lambda x: x['distance'])
# return the first 3 closest nodes by default
return node_list[:returnCount]
else:
logger.warning(f"System: No nodes found in closest_nodes on interface {nodeInt}")
return ERROR_FETCHING_DATA
# Calculate distance to node from config.ini location
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
if (distance < sentry_radius):
if (nodeID not in [globals().get(f'myNodeNum{i}') for i in range(1, 10)]) and str(nodeID) not in sentryIgnoreList:
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
except Exception as e:
pass
else:
# request location data currently blocking needs to be async
if reqLocationEnabled:
try:
logger.debug(f"System: Requesting location data for {node['id']}, lastHeard: {node.get('lastHeard', 'N/A')}")
# if not a interface node
if node['num'] in [globals().get(f'myNodeNum{i}') for i in range(1, 10)]:
ignore = True
else:
# one idea is to send a ping to the node to request location data for if or when, ask again later
interface.sendPosition(destinationId=node['id'], wantResponse=False, channelIndex=channel)
# wait a bit
time.sleep(3)
# send a traceroute request
interface.sendTraceRoute(destinationId=node['id'], channelIndex=channel, wantResponse=False)
# wait a bit
time.sleep(1)
except Exception as e:
logger.error(f"System: Error requesting location data for {node['id']}. Error: {e}")
# sort by distance closest
#node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2)
node_list.sort(key=lambda x: x['distance'])
# return the first 3 closest nodes by default
return node_list[:returnCount]
else:
logger.warning(f"System: No nodes found in closest_nodes on interface {nodeInt}")
return ERROR_FETCHING_DATA
def handleFavoriteNode(nodeInt=1, nodeID=0, aor=False):
# Add or remove a favorite node for the given interface. aor: True to add, False to remove.
@@ -826,6 +838,119 @@ def messageTrap(msg):
return True
return False
def stringSafeCheck(s):
# Check if a string is safe to use, no control characters or non-printable characters
soFarSoGood = True
if not all(c.isprintable() or c.isspace() for c in s):
return False
if any(ord(c) < 32 and c not in '\n\r\t' for c in s):
return False
if any(c in s for c in ['\x0b', '\x0c', '\x1b']):
return False
if len(s) > 1000:
return False
injection_chars = [';', '|', '../']
if any(char in s for char in injection_chars):
return False
return soFarSoGood
def save_bbsBanList():
# save the bbs_ban_list to file
try:
with open('data/bbs_ban_list.txt', 'w') as f:
for node in bbs_ban_list:
f.write(f"{node}\n")
logger.debug("System: BBS ban list saved")
except Exception as e:
logger.error(f"System: Error saving BBS ban list: {e}")
def load_bbsBanList():
global bbs_ban_list
loaded_list = []
try:
with open('data/bbs_ban_list.txt', 'r') as f:
loaded_list = [line.strip() for line in f if line.strip()]
logger.debug("System: BBS ban list loaded from file")
except FileNotFoundError:
config_val = config['bbs'].get('bbs_ban_list', '')
if config_val:
loaded_list = [x.strip() for x in config_val.split(',') if x.strip()]
logger.debug("System: No BBS ban list file found, loaded from config or started empty")
except Exception as e:
logger.error(f"System: Error loading BBS ban list: {e}")
# Merge loaded_list into bbs_ban_list, only adding new entries
for node in loaded_list:
if node not in bbs_ban_list:
bbs_ban_list.append(node)
def isNodeAdmin(nodeID):
# check if the nodeID is in the bbs_admin_list
if bbs_admin_list != ['']:
for admin in bbs_admin_list:
if str(nodeID) == admin:
return True
else:
return True
return False
def isNodeBanned(nodeID):
# check if the nodeID is in the bbs_ban_list
for banned in bbs_ban_list:
if str(nodeID) == banned:
return True
return False
def handle_bbsban(message, message_from_id, isDM):
msg = ""
if not isDM:
return "🤖only available in a Direct Message📵"
if not isNodeAdmin(message_from_id):
return NO_ALERTS
if "?" in message:
return "Ban or unban a node from posting to the BBS. Example: bannode add 1234567890 or bannode remove 1234567890"
parts = message.lower().split()
if len(parts) < 2 or parts[0] != "bannode":
return "Please specify add, remove, or list. Example: bannode add 1234567890"
action = parts[1]
if action == "list":
load_bbsBanList() # Always reload from file for latest list
if bbs_ban_list:
return "BBS Ban List:\n" + "\n".join(bbs_ban_list)
else:
return "The BBS ban list is currently empty."
if len(parts) < 3:
return "Please specify add or remove and a node number. Example: bannode add 1234567890"
node_id = parts[2].strip()
if not node_id.isdigit():
return "Invalid node number. Please provide a numeric node ID."
if action == "add":
if node_id not in bbs_ban_list:
bbs_ban_list.append(node_id)
save_bbsBanList()
logger.warning(f"System: {message_from_id} added {node_id} to the BBS ban list")
msg = f"Node {node_id} added to the BBS ban list"
else:
msg = f"Node {node_id} is already in the BBS ban list"
elif action == "remove":
if node_id in bbs_ban_list:
bbs_ban_list.remove(node_id)
save_bbsBanList()
logger.warning(f"System: {message_from_id} removed {node_id} from the BBS ban list")
msg = f"Node {node_id} removed from the BBS ban list"
else:
msg = f"Node {node_id} is not in the BBS ban list"
else:
msg = "Invalid action. Please use 'add', 'remove', or 'list'."
return msg
def handleMultiPing(nodeID=0, deviceID=1):
global multiPingList
if len(multiPingList) > 1:
@@ -990,12 +1115,13 @@ def onDisconnect(interface):
interface.close()
# Telemetry Functions
telemetryData = {}
localTelemetryData = {}
def initialize_telemetryData():
telemetryData[0] = {f'interface{i}': 0 for i in range(1, 10)}
telemetryData[0].update({f'lastAlert{i}': '' for i in range(1, 10)})
global localTelemetryData
localTelemetryData[0] = {f'interface{i}': 0 for i in range(1, 10)}
localTelemetryData[0].update({f'lastAlert{i}': '' for i in range(1, 10)})
for i in range(1, 10):
telemetryData[i] = {'numPacketsTx': 0, 'numPacketsRx': 0, 'numOnlineNodes': 0, 'numPacketsTxErr': 0, 'numPacketsRxErr': 0, 'numTotalNodes': 0}
localTelemetryData[i] = {'numPacketsTx': 0, 'numPacketsRx': 0, 'numOnlineNodes': 0, 'numPacketsTxErr': 0, 'numPacketsRxErr': 0, 'numTotalNodes': 0}
# indented to be called from the main loop
initialize_telemetryData()
@@ -1048,23 +1174,26 @@ def compileFavoriteList(getInterfaceIDs=True):
def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
interface = globals()[f'interface{rxNode}']
myNodeNum = globals().get(f'myNodeNum{rxNode}')
global telemetryData
global localTelemetryData
# throttle the telemetry requests to prevent spamming the device
if 1 <= rxNode <= 9:
if time.time() - telemetryData[0][f'interface{rxNode}'] < 600 and not userRequested:
if time.time() - localTelemetryData[0][f'interface{rxNode}'] < 600 and not userRequested:
return -1
telemetryData[0][f'interface{rxNode}'] = time.time()
localTelemetryData[0][f'interface{rxNode}'] = time.time()
# some telemetry data is not available in python-meshtastic?
# bring in values from the last telemetry dump for the node
numPacketsTx = telemetryData[rxNode]['numPacketsTx']
numPacketsRx = telemetryData[rxNode]['numPacketsRx']
numPacketsTxErr = telemetryData[rxNode]['numPacketsTxErr']
numPacketsRxErr = telemetryData[rxNode]['numPacketsRxErr']
numTotalNodes = telemetryData[rxNode]['numTotalNodes']
totalOnlineNodes = telemetryData[rxNode]['numOnlineNodes']
numPacketsTx = localTelemetryData[rxNode].get('numPacketsTx', 0)
numPacketsRx = localTelemetryData[rxNode].get('numPacketsRx', 0)
numPacketsTxErr = localTelemetryData[rxNode].get('numPacketsTxErr', 0)
numPacketsRxErr = localTelemetryData[rxNode].get('numPacketsRxErr', 0)
numTotalNodes = localTelemetryData[rxNode].get('numTotalNodes', 0)
totalOnlineNodes = localTelemetryData[rxNode].get('numOnlineNodes', 0)
numRXDupes = localTelemetryData[rxNode].get('numRXDupes', 0)
numTxRelays = localTelemetryData[rxNode].get('numTxRelays', 0)
heapFreeBytes = localTelemetryData[rxNode].get('heapFreeBytes', 0)
heapTotalBytes = localTelemetryData[rxNode].get('heapTotalBytes', 0)
# get the telemetry data for a node
chutil = round(interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("channelUtilization", 0), 1)
airUtilTx = round(interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("airUtilTx", 0), 1)
@@ -1105,6 +1234,16 @@ def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
send_message(f"Low Battery Level: {batteryLevel}{emji} on Device: {rxNode}", {secure_channel}, 0, {secure_interface})
elif batteryLevel < 10:
logger.critical(f"System: Critical Battery Level: {batteryLevel}{emji} on Device: {rxNode}")
# if numRXDupes,numTxRelays,heapFreeBytes,heapTotalBytes are available loge them
if numRXDupes != 0:
dataResponse += f" RXDupes:{numRXDupes}"
if numTxRelays != 0:
dataResponse += f" TxRelays:{numTxRelays}"
if heapFreeBytes != 0 and heapTotalBytes != 0:
logger.debug(f"System: Device {rxNode} Heap Memory Free:{heapFreeBytes} Total:{heapTotalBytes}")
#dataResponse += f" Heap:{heapFreeBytes}/{heapTotalBytes}"
return dataResponse
positionMetadata = {}
@@ -1134,7 +1273,7 @@ def initializeMeshLeaderboard():
initializeMeshLeaderboard()
def consumeMetadata(packet, rxNode=0, channel=-1):
global positionMetadata, telemetryData, meshLeaderboard
global positionMetadata, localTelemetryData, meshLeaderboard
uptime = battery = temp = iaq = nodeID = 0
deviceMetrics, envMetrics, localStats = {}, {}, {}
@@ -1228,31 +1367,26 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
except Exception as e:
logger.debug(f"System: TELEMETRY_APP iaq error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
# Collect localStats for telemetryData
# Update localStats in telemetryData
if telemetry_packet.get('localStats'):
localStats = telemetry_packet['localStats']
try:
# Check if 'numPacketsTx' and 'numPacketsRx' exist and are not zero
if localStats.get('numPacketsTx') is not None and localStats.get('numPacketsRx') is not None and localStats['numPacketsTx'] != 0:
# Assign the values to the telemetry dictionary
keys = [
'numPacketsTx', 'numPacketsRx', 'numOnlineNodes',
'numOfflineNodes', 'numPacketsTxErr', 'numPacketsRxErr', 'numTotalNodes']
for key in keys:
if localStats.get(key) is not None:
telemetryData[rxNode][key] = localStats.get(key)
# Only store keys where value is not 0
filtered_stats = {k: v for k, v in localStats.items() if v != 0}
localTelemetryData[rxNode].update(filtered_stats)
except Exception as e:
logger.debug(f"System: TELEMETRY_APP localStats error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
#POSITION_APP packets
if packet_type == 'POSITION_APP':
try:
if debugMetadata and 'POSITION_APP' not in metadataFilter:
print(f"DEBUG POSITION_APP: {packet}\n\n")
keys = ['altitude', 'groundSpeed', 'precisionBits']
position_stats_keys = ['altitude', 'groundSpeed', 'precisionBits']
position_data = packet['decoded']['position']
if nodeID not in positionMetadata:
positionMetadata[nodeID] = {}
for key in keys:
for key in position_stats_keys:
positionMetadata[nodeID][key] = position_data.get(key, 0)
# Track fastest speed 🚓
if position_data.get('groundSpeed') is not None:
@@ -1626,7 +1760,7 @@ def get_sysinfo(nodeID=0, deviceID=1):
# Get the system telemetry data for return on the sysinfo command
sysinfo = ''
stats = str(displayNodeTelemetry(nodeID, deviceID, userRequested=True)) + " 🤖👀" + str(len(seenNodes))
if "numPacketsRx:0" in stats or stats == -1:
if "numPacketsTx:0" in stats or stats == -1:
return "Gathering Telemetry try again later⏳"
# replace Telemetry with Int in string
stats = stats.replace("Telemetry", "Int")
@@ -1760,7 +1894,7 @@ async def handleSentinel(deviceID):
global handleSentinel_spotted, handleSentinel_loop
detectedNearby = ""
resolution = "unknown"
closest_nodes = get_closest_nodes(deviceID)
closest_nodes = await get_closest_nodes(deviceID)
closest_node = closest_nodes[0]['id'] if closest_nodes != ERROR_FETCHING_DATA and closest_nodes else None
closest_distance = closest_nodes[0]['distance'] if closest_nodes != ERROR_FETCHING_DATA and closest_nodes else None
@@ -1814,7 +1948,7 @@ async def process_vox_queue():
time.sleep(responseDelay)
async def watchdog():
global telemetryData, retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
global localTelemetryData, retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
logger.debug("System: Watchdog started")
while True:
await asyncio.sleep(20)
@@ -1827,14 +1961,15 @@ async def watchdog():
for i in range(1, 10):
interface = globals().get(f'interface{i}')
retry_int = globals().get(f'retry_int{i}')
if interface is not None and not retry_int and globals().get(f'interface{i}_enabled'):
int_enabled = globals().get(f'interface{i}_enabled')
if interface is not None and not retry_int and int_enabled:
try:
firmware = getNodeFirmware(0, i)
except Exception as e:
logger.error(f"System: communicating with interface{i}, trying to reconnect: {e}")
globals()[f'retry_int{i}'] = True
if not globals()[f'retry_int{i}']:
if not retry_int and int_enabled:
if sentry_enabled:
await handleSentinel(i)
@@ -1844,11 +1979,11 @@ async def watchdog():
handleAlertBroadcast(i)
intData = displayNodeTelemetry(0, i)
if intData != -1 and telemetryData[0][f'lastAlert{i}'] != intData:
if intData != -1 and localTelemetryData[0][f'lastAlert{i}'] != intData:
logger.debug(intData + f" Firmware:{firmware}")
telemetryData[0][f'lastAlert{i}'] = intData
localTelemetryData[0][f'lastAlert{i}'] = intData
if globals()[f'retry_int{i}'] and globals()[f'interface{i}_enabled']:
if retry_int and int_enabled:
try:
await retry_interface(i)
except Exception as e: