|
|
|
@@ -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,57 +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, channel=publicChannel):
|
|
|
|
|
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']
|
|
|
|
|
|
|
|
|
|
#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
|
|
|
|
|
reqLocationEnabled = False
|
|
|
|
|
if reqLocationEnabled:
|
|
|
|
|
if interface.nodes:
|
|
|
|
|
for node in interface.nodes.values():
|
|
|
|
|
if 'position' in node:
|
|
|
|
|
try:
|
|
|
|
|
logger.debug(f"System: Requesting location data for {node['id']}, lastHeard: {node.get('lastHeard', 'N/A')}")
|
|
|
|
|
# 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)
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
|
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
|
|
|
|
|
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.
|
|
|
|
@@ -862,17 +866,23 @@ def save_bbsBanList():
|
|
|
|
|
|
|
|
|
|
def load_bbsBanList():
|
|
|
|
|
global bbs_ban_list
|
|
|
|
|
# load the bbs_ban_list from file
|
|
|
|
|
loaded_list = []
|
|
|
|
|
try:
|
|
|
|
|
with open('data/bbs_ban_list.txt', 'r') as f:
|
|
|
|
|
bbs_ban_list = [line.strip() for line in f.readlines() if line.strip()]
|
|
|
|
|
logger.debug("System: BBS ban list loaded")
|
|
|
|
|
loaded_list = [line.strip() for line in f if line.strip()]
|
|
|
|
|
logger.debug("System: BBS ban list loaded from file")
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',')
|
|
|
|
|
logger.debug("System: No BBS ban list found, starting with default")
|
|
|
|
|
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}")
|
|
|
|
|
bbs_ban_list = []
|
|
|
|
|
|
|
|
|
|
# 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
|
|
|
|
@@ -907,6 +917,7 @@ def handle_bbsban(message, message_from_id, isDM):
|
|
|
|
|
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:
|
|
|
|
@@ -1104,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()
|
|
|
|
@@ -1162,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)
|
|
|
|
@@ -1219,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 = {}
|
|
|
|
@@ -1248,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 = {}, {}, {}
|
|
|
|
|
|
|
|
|
@@ -1342,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:
|
|
|
|
@@ -1740,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")
|
|
|
|
@@ -1874,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
|
|
|
|
|
|
|
|
|
@@ -1928,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)
|
|
|
|
@@ -1941,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)
|
|
|
|
|
|
|
|
|
@@ -1958,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:
|
|
|
|
|