From 19935d9f087b6d02d4965e31f658e986812ef7e8 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Sun, 7 Sep 2025 19:12:57 -0700 Subject: [PATCH 1/3] howfar initial idea goofin to see if this works --- mesh_bot.py | 11 ++++ modules/locationdata.py | 112 +++++++++++++++++++++++++++++++++++++++- modules/system.py | 2 +- 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/mesh_bot.py b/mesh_bot.py index dfd14d9..a31e32d 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -61,6 +61,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n "hangman": lambda: handleHangman(message, message_from_id, deviceID), "hfcond": hf_band_conditions, "history": lambda: handle_history(message, message_from_id, deviceID, isDM), + "howfar": lambda: handle_howfar(message_from_id, deviceID, channel_number), "joke": lambda: tell_joke(message_from_id), "lemonstand": lambda: handleLemonade(message, message_from_id, deviceID), "lheard": lambda: handle_lheard(message, message_from_id, deviceID, isDM), @@ -311,6 +312,16 @@ def handle_wxalert(message_from_id, deviceID, message): weatherAlert = weatherAlert[0] return weatherAlert +def handle_howfar(message_from_id, deviceID, channel_number): + location = get_node_location(message_from_id, deviceID) + lat = location[0] + lon = location[1] + if lat == latitudeValue and lon == longitudeValue: + logger.debug(f"System: HowFar: No GPS location for {message_from_id}") + return "No GPS location available" + msg = distance(lat,lon,message_from_id) + return msg + def handle_wiki(message, isDM): # location = get_node_location(message_from_id, deviceID) msg = "Wikipedia search function. \nUsage example:📲wiki: travelling gnome" diff --git a/modules/locationdata.py b/modules/locationdata.py index 0fd1535..2c3c575 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -8,8 +8,10 @@ import requests # pip install requests import bs4 as bs # pip install beautifulsoup4 import xml.dom.minidom from modules.log import * +import math -trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow", "valert", "earthquake") + +trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow", "valert", "earthquake", "howfar") def where_am_i(lat=0, lon=0, short=False, zip=False): whereIam = "" @@ -811,3 +813,111 @@ def checkUSGSEarthQuake(lat=0, lon=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}" + +howfarDB = {} +def distance(lat=0,lon=0,nodeID=0): + # part of the howfar function, calculates the distance between two lat/lon points + msg = "" + if lat == 0 and lon == 0: + return NO_DATA_NOGPS + if nodeID == 0: + return "No NodeID provided" + + if nodeID not in howfarDB: + #register first point NodeID, lat, lon, time, point + howfarDB[nodeID] = [{'lat': lat, 'lon': lon, 'time': datetime.now()}] + return "Starting Point Set" + else: + # calculate distance from last point in howfarDB + last_point = howfarDB[nodeID][-1] + lat1 = math.radians(last_point['lat']) + lon1 = math.radians(last_point['lon']) + lat2 = math.radians(lat) + lon2 = math.radians(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)) + 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 the speed if time difference is more than 1 minute + time_diff = datetime.now() - last_point['time'] + if time_diff.total_seconds() > 60: + hours = time_diff.total_seconds() / 3600 + if use_metric: + speed = distance_km / hours + speed_str = f"{speed:.2f} km/h" + else: + 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}°" + + # if points 3+ are within 30 meters of the first point add the area of the polygon + if len(howfarDB[nodeID]) >= 3: + points = [] + # loop the howfarDB to get all the points except the current nodeID + for key in howfarDB: + if key != nodeID: + points.append((howfarDB[key][-1]['lat'], howfarDB[key][-1]['lon'])) + # loop the howfarDB[nodeID] to get the points + for point in howfarDB[nodeID]: + points.append((point['lat'], point['lon'])) + # close the polygon by adding the first point to the end + points.append((howfarDB[nodeID][0]['lat'], howfarDB[nodeID][0]['lon'])) + # calculate the area of the polygon + area = 0.0 + for i in range(len(points)-1): + lat1 = math.radians(points[i][0]) + lon1 = math.radians(points[i][1]) + lat2 = math.radians(points[i+1][0]) + lon2 = math.radians(points[i+1][1]) + area += (lon2 - lon1) * (2 + math.sin(lat1) + math.sin(lat2)) + area = area * (6378137 ** 2) / 2.0 + area = abs(area) / 1e6 # convert to square kilometers + + if use_metric: + msg += f", Area Sq.Km: {area:.2f} sq.km (approx)" + else: + area_miles = area * 0.386102 + msg += f", Area Sq.Miles: {area_miles:.2f} sq.mi (approx)" + + #calculate the centroid of the polygon + x = 0.0 + y = 0.0 + z = 0.0 + for point in points[:-1]: + lat_rad = math.radians(point[0]) + lon_rad = math.radians(point[1]) + x += math.cos(lat_rad) * math.cos(lon_rad) + y += math.cos(lat_rad) * math.sin(lon_rad) + z += math.sin(lat_rad) + total_points = len(points) - 1 + x /= total_points + y /= total_points + z /= total_points + lon_centroid = math.atan2(y, x) + hyp = math.sqrt(x * x + y * y) + lat_centroid = math.atan2(z, hyp) + lat_centroid = math.degrees(lat_centroid) + lon_centroid = math.degrees(lon_centroid) + msg += f", Centroid: {lat_centroid:.5f}, {lon_centroid:.5f}" + + + # update the last point in howfarDB + howfarDB[nodeID].append({'lat': lat, 'lon': lon, 'time': datetime.now()}) + + return msg \ No newline at end of file diff --git a/modules/system.py b/modules/system.py index 201a34d..e0c4ebe 100644 --- a/modules/system.py +++ b/modules/system.py @@ -75,7 +75,7 @@ if enableCmdHistory: if location_enabled: from modules.locationdata import * # from the spudgunman/meshing-around repo trap_list = trap_list + trap_list_location - help_message = help_message + ", whereami, wx, rlist" + help_message = help_message + ", whereami, wx, rlist, howfar" if enableGBalerts and not enableDEalerts: from modules.globalalert import * # from the spudgunman/meshing-around repo logger.warning(f"System: GB Alerts not functional at this time need to find a source API") From ff9b76c9664d21a8ff9006695674a800d499a226 Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Tue, 9 Sep 2025 12:08:59 -0700 Subject: [PATCH 2/3] Update locationdata.py --- modules/locationdata.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/locationdata.py b/modules/locationdata.py index 2c3c575..b98c35c 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -828,6 +828,9 @@ def distance(lat=0,lon=0,nodeID=0): howfarDB[nodeID] = [{'lat': lat, 'lon': lon, 'time': datetime.now()}] return "Starting Point Set" 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" # calculate distance from last point in howfarDB last_point = howfarDB[nodeID][-1] lat1 = math.radians(last_point['lat']) From 5703cfb381f6b2bab8295e7fc9e555aae5dfc89b Mon Sep 17 00:00:00 2001 From: SpudGunMan Date: Tue, 9 Sep 2025 12:42:03 -0700 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=97=BA=EF=B8=8F=20howfar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + mesh_bot.py | 21 ++++++++++++++++++--- modules/locationdata.py | 13 ++++++++++--- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8654566..d351b7f 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ git clone https://github.com/spudgunman/meshing-around | `readnews` | returns the contents of a file (news.txt, by default) via the chunker on air | ✅ | | `satpass` | returns the pass info from API for defined NORAD ID in config or Example: `satpass 25544,33591`| | | `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` | +| `howfar` | returns the distance you have traveled since your last HowFar. `howfar reset` to start over | ✅ | ### CheckList | Command | Description | | diff --git a/mesh_bot.py b/mesh_bot.py index a31e32d..6077d44 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -61,7 +61,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n "hangman": lambda: handleHangman(message, message_from_id, deviceID), "hfcond": hf_band_conditions, "history": lambda: handle_history(message, message_from_id, deviceID, isDM), - "howfar": lambda: handle_howfar(message_from_id, deviceID, channel_number), + "howfar": lambda: handle_howfar(message, message_from_id, deviceID, isDM), "joke": lambda: tell_joke(message_from_id), "lemonstand": lambda: handleLemonade(message, message_from_id, deviceID), "lheard": lambda: handle_lheard(message, message_from_id, deviceID, isDM), @@ -312,14 +312,29 @@ def handle_wxalert(message_from_id, deviceID, message): weatherAlert = weatherAlert[0] return weatherAlert -def handle_howfar(message_from_id, deviceID, channel_number): +def handle_howfar(message, message_from_id, deviceID, isDM): + msg = '' location = get_node_location(message_from_id, deviceID) lat = location[0] lon = location[1] + # if ? in message + if "?" in message.lower(): + return "command returns the distance you have traveled since your last HowFar-command. Add 'reset' to reset your starting point." + + # if no GPS location return if lat == latitudeValue and lon == longitudeValue: logger.debug(f"System: HowFar: No GPS location for {message_from_id}") return "No GPS location available" - msg = distance(lat,lon,message_from_id) + + if "reset" in message.lower(): + msg = distance(lat,lon,message_from_id, reset=True) + else: + msg = distance(lat,lon,message_from_id) + + # if not a DM add the username to the beginning of msg + if not useDMForResponse and not isDM: + msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg + return msg def handle_wiki(message, isDM): diff --git a/modules/locationdata.py b/modules/locationdata.py index b98c35c..dcbe7c0 100644 --- a/modules/locationdata.py +++ b/modules/locationdata.py @@ -815,7 +815,7 @@ def checkUSGSEarthQuake(lat=0, lon=0): return f"{quake_count} quakes in last {history} days within {radius}km of you largest was {largest_mag}. {description_text}" howfarDB = {} -def distance(lat=0,lon=0,nodeID=0): +def distance(lat=0,lon=0,nodeID=0, reset=False): # part of the howfar function, calculates the distance between two lat/lon points msg = "" if lat == 0 and lon == 0: @@ -823,14 +823,21 @@ def distance(lat=0,lon=0,nodeID=0): if nodeID == 0: return "No NodeID provided" + if reset: + if nodeID in howfarDB: + del howfarDB[nodeID] + if nodeID not in howfarDB: #register first point NodeID, lat, lon, time, point howfarDB[nodeID] = [{'lat': lat, 'lon': lon, 'time': datetime.now()}] - return "Starting Point Set" + if reset: + return "Tracking reset, new starting point registered🗺️" + else: + return "Starting point registered🗺️" 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" + return "📍No movement detected yet" # calculate distance from last point in howfarDB last_point = howfarDB[nodeID][-1] lat1 = math.radians(last_point['lat'])