From 642738e3b6b2bb89d59508c37d9079a9d1822107 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Tue, 16 Sep 2025 17:09:47 -0700 Subject: [PATCH 1/8] noisyNodeLogging telemetry logger idea --- README.md | 24 +++++++++--------------- config.template | 3 +++ mesh_bot.py | 2 ++ modules/settings.py | 3 ++- modules/system.py | 21 +++++++++++++++++++++ 5 files changed, 37 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index d351b7f..c799ea3 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo ### Network Tools - **Build, Test Local Mesh**: Ping allow for message delivery testing with more realistic packets vs. telemetry - **Test Node Hardware**: `test` will send incremental sized data into the radio buffer for overall length of message testing +- **Network Monitoring**: Alert on noisy nodes, node locations, and best placment for relay nodes. ### Multi Radio/Node Support - **Simultaneous Monitoring**: Monitor up to nine networks at the same time. @@ -318,10 +319,9 @@ myRegionalKeysDE = 110000000000,120510000000 This uses the defined lat-long of the bot for collecting of data from the API. see [File-Monitoring](#File-Monitoring) for ideas to collect EAS alerts from a RTL-SDR. ```ini -# EAS Alert Broadcast -wxAlertBroadcastEnabled = True -# EAS Alert Broadcast Channels -wxAlertBroadcastCh = 2,4 + +wxAlertBroadcastEnabled = True # EAS Alert Broadcast +wxAlertBroadcastCh = 2,4 # EAS Alert Broadcast Channels ignoreEASenable = True # Ignore any headline that includes followig word list ignoreEASwords = test,advisory ``` @@ -438,19 +438,13 @@ training = True # Training mode will not send the hello message to new nodes, us In the config.ini enable the module ```ini [scheduler] -# enable or disable the scheduler module -enabled = False -# interface to send the message to -interface = 1 -# channel to send the message to +enabled = False # enable or disable the scheduler module +interface = 1 # channel to send the message to channel = 2 message = "MeshBot says Hello! DM for more info." -# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun -value = -# interval to use when time is not set (e.g. every 2 days) -interval = -# time of day in 24:00 hour format when value is 'day' and interval is not set -time = +value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun +interval = # interval to use when time is not set (e.g. every 2 days) +time = # time of day in 24:00 hour format when value is 'day' and interval is not set ``` The basic brodcast message can be setup in condig.ini. For advanced, See mesh_bot.py around the bottom of file, line [1491](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1491) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost. diff --git a/config.template b/config.template index 54a6a98..1fac30d 100644 --- a/config.template +++ b/config.template @@ -325,5 +325,8 @@ wantAck = False maxBuffer = 200 #Enable Extra logging of Hop count data enableHopLogs = False +# Noisy Node Telemetry Logging and packet threshold +noisyNodeLogging = False +noisyTelemetryLimit = 20 diff --git a/mesh_bot.py b/mesh_bot.py index 6077d44..0f741f2 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -1488,6 +1488,8 @@ async def start_rx(): logger.debug(f"System: CheckList Module Enabled") if ignoreChannels != []: logger.debug(f"System: Ignoring Channels: {ignoreChannels}") + if noisyNodeLogging: + logger.debug(f"System: Noisy Node Logging Enabled") if enableSMTP: if enableImap: logger.debug(f"System: SMTP Email Alerting Enabled using IMAP") diff --git a/modules/settings.py b/modules/settings.py index 19df687..bd4e6fc 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -367,7 +367,8 @@ try: wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False maxBuffer = config['messagingSettings'].getint('maxBuffer', 200) # default 200 enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False - + noisyNodeLogging = config['messagingSettings'].getboolean('noisyNodeLogging', False) # default False + noisyTelemetryLimit = config['messagingSettings'].getint('noisyTelemetryLimit', 20) # default 20 packets except KeyError as e: print(f"System: Error reading config file: {e}") print(f"System: Check the config.ini against config.template file for missing sections or values.") diff --git a/modules/system.py b/modules/system.py index e0c4ebe..20991aa 100644 --- a/modules/system.py +++ b/modules/system.py @@ -998,6 +998,13 @@ def consumeMetadata(packet, rxNode=0): # Remove the oldest entry oldest_nodeID = next(iter(positionMetadata)) del positionMetadata[oldest_nodeID] + + # add a packet count to the positionMetadata for the node + if 'packetCount' in positionMetadata[nodeID]: + positionMetadata[nodeID]['packetCount'] += 1 + else: + positionMetadata[nodeID]['packetCount'] = 1 + except Exception as e: logger.debug(f"System: POSITION_APP decode error: {e} packet {packet}") @@ -1040,6 +1047,17 @@ def consumeMetadata(packet, rxNode=0): logger.critical(f"System: Error consuming metadata: {e} Device:{rxNode}") logger.debug(f"System: Error Packet = {packet}") +def noisyTelemetryCheck(): + global positionMetadata + if len(positionMetadata) == 0: + return + # sort the positionMetadata by packetCount + sorted_positionMetadata = dict(sorted(positionMetadata.items(), key=lambda item: item[1].get('packetCount', 0), reverse=True)) + top_three = list(sorted_positionMetadata.items())[:3] + for nodeID, data in top_three: + if data.get('packetCount', 0) > noisyTelemetryLimit: + logger.warning(f"System: Noisy Telemetry Detected from NodeID:{nodeID} ShortName:{get_name_from_number(nodeID, 'short', 1)} Packets:{data.get('packetCount', 0)}") + def get_sysinfo(nodeID=0, deviceID=1): # Get the system telemetry data for return on the sysinfo command sysinfo = '' @@ -1249,6 +1267,9 @@ async def watchdog(): handleMultiPing(0, i) + if noisyNodeLogging: + noisyTelemetryCheck() + if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled or volcanoAlertBroadcastEnabled: handleAlertBroadcast(i) From 38d5006236a0cbc7e57388384f96e5ca305929f2 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Tue, 16 Sep 2025 17:24:55 -0700 Subject: [PATCH 2/8] Update system.py --- modules/system.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/system.py b/modules/system.py index 20991aa..0e640f9 100644 --- a/modules/system.py +++ b/modules/system.py @@ -1267,9 +1267,6 @@ async def watchdog(): handleMultiPing(0, i) - if noisyNodeLogging: - noisyTelemetryCheck() - if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled or volcanoAlertBroadcastEnabled: handleAlertBroadcast(i) @@ -1283,6 +1280,10 @@ async def watchdog(): await retry_interface(i) except Exception as e: logger.error(f"System: retrying interface{i}: {e}") + + # check for noisy telemetry + if noisyNodeLogging: + noisyTelemetryCheck() def exit_handler(): # Close the interface and save the BBS messages From 4e1d3e2b58f0737ff1bc7e35c586f193601042c4 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Wed, 17 Sep 2025 10:55:17 -0700 Subject: [PATCH 3/8] lower volume --- config.template | 2 +- modules/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.template b/config.template index 1fac30d..7a68ce7 100644 --- a/config.template +++ b/config.template @@ -327,6 +327,6 @@ maxBuffer = 200 enableHopLogs = False # Noisy Node Telemetry Logging and packet threshold noisyNodeLogging = False -noisyTelemetryLimit = 20 +noisyTelemetryLimit = 5 diff --git a/modules/settings.py b/modules/settings.py index bd4e6fc..3c72475 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -368,7 +368,7 @@ try: maxBuffer = config['messagingSettings'].getint('maxBuffer', 200) # default 200 enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False noisyNodeLogging = config['messagingSettings'].getboolean('noisyNodeLogging', False) # default False - noisyTelemetryLimit = config['messagingSettings'].getint('noisyTelemetryLimit', 20) # default 20 packets + noisyTelemetryLimit = config['messagingSettings'].getint('noisyTelemetryLimit', 5) # default 5 packets except KeyError as e: print(f"System: Error reading config file: {e}") print(f"System: Check the config.ini against config.template file for missing sections or values.") From 4e91801cb929a2cec4ce661fac5d4345a75d9ab8 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Wed, 17 Sep 2025 10:55:30 -0700 Subject: [PATCH 4/8] Update system.py reset alerts --- modules/system.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/system.py b/modules/system.py index 0e640f9..933a054 100644 --- a/modules/system.py +++ b/modules/system.py @@ -1057,6 +1057,8 @@ def noisyTelemetryCheck(): for nodeID, data in top_three: if data.get('packetCount', 0) > noisyTelemetryLimit: logger.warning(f"System: Noisy Telemetry Detected from NodeID:{nodeID} ShortName:{get_name_from_number(nodeID, 'short', 1)} Packets:{data.get('packetCount', 0)}") + # reset the packet count for the node + positionMetadata[nodeID]['packetCount'] = 0 def get_sysinfo(nodeID=0, deviceID=1): # Get the system telemetry data for return on the sysinfo command From c585f608827f4d5ee335afd968f4a0eda3ad3736 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Wed, 17 Sep 2025 10:55:38 -0700 Subject: [PATCH 5/8] Update README.md think this was a typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c799ea3..caf0173 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ git clone https://github.com/spudgunman/meshing-around | `golfsim` | Plays a 9-hole Golf Simulator | ✅ | | `hamtest` | FCC/ARRL Quiz `hamtest general` or `hamtest extra` and `score` | ✅ | | `hangman` | Plays the classic word guess game | ✅ | -| `joke` | Tells a joke | ✅ | +| `joke` | Tells a joke | | | `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ | | `mastermind` | Plays the classic code-breaking game | ✅ | | `videopoker` | Plays basic 5-card hold Video Poker | ✅ | From 8a64b8e7ad16a1a3eebd7a05e813f875fe7d264c Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Wed, 17 Sep 2025 11:41:35 -0700 Subject: [PATCH 6/8] Update locationdata.py this got lost somewhere --- modules/locationdata.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/modules/locationdata.py b/modules/locationdata.py index dcbe7c0..ffa8026 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -867,6 +867,27 @@ def distance(lat=0,lon=0,nodeID=0, reset=False): speed_mph = (distance_km * 0.621371) / hours speed_str = f"{speed_mph:.2f} mph" msg += f", travel time: {int(time_diff.total_seconds()//60)} min, Speed: {speed_str}" + + #calculate total distance traveled + total_distance_km = 0.0 + for i in range(1, len(howfarDB[nodeID])): + point1 = howfarDB[nodeID][i-1] + point2 = howfarDB[nodeID][i] + lat1 = math.radians(point1['lat']) + lon1 = math.radians(point1['lon']) + lat2 = math.radians(point2['lat']) + lon2 = math.radians(point2['lon']) + dlon = lon2 - lon1 + dlat = lat2 - lat1 + a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2 + c = 2 * math.asin(math.sqrt(a)) + distance_km_segment = c * r + total_distance_km += distance_km_segment + if use_metric and total_distance_km < 1: + msg += f", Total: {total_distance_km:.2f} km" + elif total_distance_km >= 1: + total_distance_miles = total_distance_km * 0.621371 + msg += f", Total: {total_distance_miles:.2f} miles" #calculate bearing x = math.sin(dlon) * math.cos(lat2) @@ -874,7 +895,7 @@ def distance(lat=0,lon=0,nodeID=0, reset=False): initial_bearing = math.atan2(x, y) initial_bearing = math.degrees(initial_bearing) compass_bearing = (initial_bearing + 360) % 360 - msg += f", Bearing from last point: {compass_bearing:.2f}°" + msg += f", 🧭Bearing from last: {compass_bearing:.2f}°" # if points 3+ are within 30 meters of the first point add the area of the polygon if len(howfarDB[nodeID]) >= 3: From 4fdfa49b87056344c2b3b4388e8ef4f3720c47fe Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Wed, 17 Sep 2025 11:43:22 -0700 Subject: [PATCH 7/8] Revert "Update locationdata.py" This reverts commit 8a64b8e7ad16a1a3eebd7a05e813f875fe7d264c. --- modules/locationdata.py | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/modules/locationdata.py b/modules/locationdata.py index ffa8026..dcbe7c0 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -867,27 +867,6 @@ def distance(lat=0,lon=0,nodeID=0, reset=False): speed_mph = (distance_km * 0.621371) / hours speed_str = f"{speed_mph:.2f} mph" msg += f", travel time: {int(time_diff.total_seconds()//60)} min, Speed: {speed_str}" - - #calculate total distance traveled - total_distance_km = 0.0 - for i in range(1, len(howfarDB[nodeID])): - point1 = howfarDB[nodeID][i-1] - point2 = howfarDB[nodeID][i] - lat1 = math.radians(point1['lat']) - lon1 = math.radians(point1['lon']) - lat2 = math.radians(point2['lat']) - lon2 = math.radians(point2['lon']) - dlon = lon2 - lon1 - dlat = lat2 - lat1 - a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2 - c = 2 * math.asin(math.sqrt(a)) - distance_km_segment = c * r - total_distance_km += distance_km_segment - if use_metric and total_distance_km < 1: - msg += f", Total: {total_distance_km:.2f} km" - elif total_distance_km >= 1: - total_distance_miles = total_distance_km * 0.621371 - msg += f", Total: {total_distance_miles:.2f} miles" #calculate bearing x = math.sin(dlon) * math.cos(lat2) @@ -895,7 +874,7 @@ def distance(lat=0,lon=0,nodeID=0, reset=False): initial_bearing = math.atan2(x, y) initial_bearing = math.degrees(initial_bearing) compass_bearing = (initial_bearing + 360) % 360 - msg += f", 🧭Bearing from last: {compass_bearing:.2f}°" + msg += f", Bearing from last point: {compass_bearing:.2f}°" # if points 3+ are within 30 meters of the first point add the area of the polygon if len(howfarDB[nodeID]) >= 3: From 39e348f7016268c17f88dc1d9ad5e879300a9418 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Wed, 17 Sep 2025 11:43:43 -0700 Subject: [PATCH 8/8] Update locationdata.py got lost somehow --- modules/locationdata.py | 116 ++++++++++++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 16 deletions(-) diff --git a/modules/locationdata.py b/modules/locationdata.py index dcbe7c0..d9699b0 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -808,16 +808,20 @@ def checkUSGSEarthQuake(lat=0, lon=0): largest_mag = mag_value # set description text description_text = event.getElementsByTagName("description")[0].getElementsByTagName("text")[0].childNodes[0].nodeValue - + largest_mag = round(largest_mag, 1) if quake_count == 0: return NO_ALERTS else: - return f"{quake_count} quakes in last {history} days within {radius}km of you largest was {largest_mag}. {description_text}" + return f"{quake_count} 🫨quakes in last {history} days within {radius} km. Largest: {largest_mag}M\n{description_text}" + howfarDB = {} def distance(lat=0,lon=0,nodeID=0, reset=False): # part of the howfar function, calculates the distance between two lat/lon points msg = "" + dupe = False + r = 6371 # Radius of earth in kilometers # haversine formula + if lat == 0 and lon == 0: return NO_DATA_NOGPS if nodeID == 0: @@ -837,7 +841,8 @@ def distance(lat=0,lon=0,nodeID=0, reset=False): else: #de-dupe points if same as last point if howfarDB[nodeID][-1]['lat'] == lat and howfarDB[nodeID][-1]['lon'] == lon: - return "📍No movement detected yet" + dupe = True + msg = "No New GPS📍 " # calculate distance from last point in howfarDB last_point = howfarDB[nodeID][-1] lat1 = math.radians(last_point['lat']) @@ -848,13 +853,21 @@ def distance(lat=0,lon=0,nodeID=0, reset=False): dlat = lat2 - lat1 a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2 c = 2 * math.asin(math.sqrt(a)) - r = 6371 # Radius of earth in kilometers + distance_km = c * r if use_metric: msg += f"{distance_km:.2f} km" else: distance_miles = distance_km * 0.621371 msg += f"{distance_miles:.2f} miles" + + #calculate bearing + x = math.sin(dlon) * math.cos(lat2) + y = math.cos(lat1) * math.sin(lat2) - (math.sin(lat1) * math.cos(lat2) * math.cos(dlon)) + initial_bearing = math.atan2(x, y) + initial_bearing = math.degrees(initial_bearing) + compass_bearing = (initial_bearing + 360) % 360 + msg += f" 🧭{compass_bearing:.2f}° Bearing from last📍" # calculate the speed if time difference is more than 1 minute time_diff = datetime.now() - last_point['time'] @@ -867,14 +880,28 @@ def distance(lat=0,lon=0,nodeID=0, reset=False): speed_mph = (distance_km * 0.621371) / hours speed_str = f"{speed_mph:.2f} mph" msg += f", travel time: {int(time_diff.total_seconds()//60)} min, Speed: {speed_str}" - - #calculate bearing - x = math.sin(dlon) * math.cos(lat2) - y = math.cos(lat1) * math.sin(lat2) - (math.sin(lat1) * math.cos(lat2) * math.cos(dlon)) - initial_bearing = math.atan2(x, y) - initial_bearing = math.degrees(initial_bearing) - compass_bearing = (initial_bearing + 360) % 360 - msg += f", Bearing from last point: {compass_bearing:.2f}°" + + # calculate total distance traveled including this point computed in distance_km from calculate distance from last point in howfarDB + total_distance_km = 0.0 + for i in range(1, len(howfarDB[nodeID])): + point1 = howfarDB[nodeID][i-1] + point2 = howfarDB[nodeID][i] + lat1 = math.radians(point1['lat']) + lon1 = math.radians(point1['lon']) + lat2 = math.radians(point2['lat']) + lon2 = math.radians(point2['lon']) + dlon = lon2 - lon1 + dlat = lat2 - lat1 + a = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2 + c = 2 * math.asin(math.sqrt(a)) + total_distance_km += c * r + # add the distance from last point to current point + total_distance_km += distance_km + if use_metric: + msg += f", Total: {total_distance_km:.2f} km" + else: + total_distance_miles = total_distance_km * 0.621371 + msg += f", Total: {total_distance_miles:.2f} miles" # if points 3+ are within 30 meters of the first point add the area of the polygon if len(howfarDB[nodeID]) >= 3: @@ -900,10 +927,10 @@ def distance(lat=0,lon=0,nodeID=0, reset=False): area = abs(area) / 1e6 # convert to square kilometers if use_metric: - msg += f", Area Sq.Km: {area:.2f} sq.km (approx)" + msg += f", Area: {area:.2f} sq.km (approx)" else: area_miles = area * 0.386102 - msg += f", Area Sq.Miles: {area_miles:.2f} sq.mi (approx)" + msg += f", Area: {area_miles:.2f} sq.mi (approx)" #calculate the centroid of the polygon x = 0.0 @@ -928,6 +955,63 @@ def distance(lat=0,lon=0,nodeID=0, reset=False): # update the last point in howfarDB - howfarDB[nodeID].append({'lat': lat, 'lon': lon, 'time': datetime.now()}) + if not dupe: + howfarDB[nodeID].append({'lat': lat, 'lon': lon, 'time': datetime.now()}) + + return msg + +def get_openskynetwork(lat=0, lon=0): + # get the latest aircraft data from OpenSky Network in the area + if lat == 0 and lon == 0: + return NO_ALERTS + # setup a bounding box of 50km around the lat/lon + box_size = 0.45 # approx 50km + # return limits for aircraft search + search_limit = 3 + lamin = lat - box_size + lamax = lat + box_size + lomin = lon - box_size + lomax = lon + box_size + + # fetch the aircraft data from OpenSky Network + opensky_url = f"https://opensky-network.org/api/states/all?lamin={lamin}&lomin={lomin}&lamax={lamax}&lomax={lomax}" + try: + aircraft_data = requests.get(opensky_url, timeout=urlTimeoutSeconds) + if not aircraft_data.ok: + logger.warning("Location:Error fetching aircraft data from OpenSky Network") + return ERROR_FETCHING_DATA + except (requests.exceptions.RequestException): + logger.warning("Location:Error fetching aircraft data from OpenSky Network") + return ERROR_FETCHING_DATA + aircraft_json = aircraft_data.json() + if 'states' not in aircraft_json or not aircraft_json['states']: + return NO_ALERTS + aircraft_list = aircraft_json['states'] + aircraft_report = "" + for aircraft in aircraft_list: + if len(aircraft_report.split("\n")) >= search_limit: + break + # extract values from JSON + try: + callsign = aircraft[1].strip() if aircraft[1] else "N/A" + origin_country = aircraft[2] + velocity = aircraft[9] + true_track = aircraft[10] + vertical_rate = aircraft[11] + sensors = aircraft[12] + geo_altitude = aircraft[13] + squawk = aircraft[14] if len(aircraft) > 14 else "N/A" + except Exception as e: + logger.debug("Location:Error extracting aircraft data from OpenSky Network") + continue + + # format the aircraft data + aircraft_report += f"{callsign} Alt:{int(geo_altitude) if geo_altitude else 'N/A'}m Vel:{int(velocity) if velocity else 'N/A'}m/s Heading:{int(true_track) if true_track else 'N/A'}°\n" + + # remove last newline + if aircraft_report.endswith("\n"): + aircraft_report = aircraft_report[:-1] + aircraft_report = abbreviate_noaa(aircraft_report) + return aircraft_report if aircraft_report else NO_ALERTS + - return msg \ No newline at end of file