diff --git a/README.md b/README.md index b3b8502..add0c92 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ Along with network testing, this bot has a lot of other features, like simple ma The bot is also capable of using dual radio/nodes, so you can monitor two networks at the same time and send messages to nodes using the same `bbspost @nodeNumber #message` or `bbspost @nodeShportName #message` function. There is a small message board to fit in the constraints of Meshtastic for posting bulletin messages with `bbspost $subject #message`. +The bot will report on anyone who is getting close to the device if in a remote location. + Store and forward-like message re-play with `messages`, and there is a repeater module for dual radio bots to cross post messages. Messages are also logged locally to disk. The bot can also be used to monitor a frequency and let you know when activity is seen. Using Hamlib to watch the S meter on a connected radio. You can send alerts to channels when a frequency is detected for 20 seconds within the thresholds set in config.ini @@ -98,6 +100,17 @@ enabled = False DadJokes = False StoreForward = False ``` +Sentry Bot detects anyone comeing close to the bot-node +``` +# detect anyone close to the bot +SentryEnabled = True +# holdoff time multiplied by minutes(20) of the watchdog +SentryChannel = 9 +# channel to send a message to when the watchdog is triggered +SentryHoldoff = 2 +# list of ignored nodes numbers ex: 2813308004,4258675309 +sentryIgnoreList = +``` The BBS has admin and block lists; see the [config.template](config.template) A repeater function for two different nodes and cross-posting messages. The'repeater_channels` is a list of repeater channel(s) that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. With great power comes great responsibility; danger could lurk in the use of this feature! If you have the two nodes in the same radio configuration, you could create a feedback loop!!! @@ -140,6 +153,7 @@ pip install geopy pip install maidenhead pip install beautifulsoup4 pip install dadjokes +pip install geopy ``` The following is needed for open-meteo use ``` diff --git a/config.template b/config.template index 0e71c2f..85d4bcd 100644 --- a/config.template +++ b/config.template @@ -41,6 +41,16 @@ zuluTime = True URL_TIMEOUT = 10 # logging to file of the non Bot messages LogMessagesToFile = False +# detect anyone close to the bot +SentryEnabled = True +# radius in meters to detect someone close to the bot +SentryRadius = 100 +# holdoff time multiplied by minutes(20) of the watchdog +SentryChannel = 9 +# channel to send a message to when the watchdog is triggered +SentryHoldoff = 2 +# list of ignored nodes numbers ex: 2813308004,4258675309 +sentryIgnoreList = [bbs] enabled = True diff --git a/mesh_bot.py b/mesh_bot.py index e552d7a..c8deac8 100755 --- a/mesh_bot.py +++ b/mesh_bot.py @@ -360,6 +360,8 @@ async def start_rx(): logger.debug(f"System: Location Telemetry Enabled using NOAA API") if dad_jokes_enabled: logger.debug(f"System: Dad Jokes Enabled!") + if sentry_enabled: + logger.debug(f"System: Sentry Mode Enabled") if store_forward_enabled: logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}") if useDMForResponse: diff --git a/modules/settings.py b/modules/settings.py index 30e93c7..a79ef43 100644 --- a/modules/settings.py +++ b/modules/settings.py @@ -75,6 +75,11 @@ try: dad_jokes_enabled = config['general'].getboolean('DadJokes', False) store_forward_enabled = config['general'].getboolean('StoreForward', False) log_messages_to_file = config['general'].getboolean('LogMessagesToFile', True) # default True + sentry_enabled = config['general'].getboolean('SentryEnabled', True) # default True + secure_channel = config['general'].getint('SentryChannel', 2) # default 2 + sentry_holdoff = config['general'].getint('SentryHoldoff', 9) # default 9 + sentryIgnoreList = config['general'].get('sentryIgnoreList', '').split(',') + sentry_radius = config['general'].getint('SentryRadius', 100) # default 100 meters config['general'].get('motd', MOTD) urlTimeoutSeconds = config['general'].getint('URL_TIMEOUT', 10) # default 10 seconds forecastDuration = config['general'].getint('NOAAforecastDuration', 4) # NOAA forcast days @@ -90,6 +95,7 @@ try: signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN + 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 29f4592..402a341 100644 --- a/modules/system.py +++ b/modules/system.py @@ -56,6 +56,10 @@ if dad_jokes_enabled: trap_list = trap_list + ("joke",) help_message = help_message + ", joke" +if sentry_enabled: + from math import sqrt + import geopy.distance # pip install geopy + # Store and Forward Configuration if store_forward_enabled: trap_list = trap_list + ("messages",) @@ -288,6 +292,72 @@ def get_node_location(number, nodeInt=1, channel=0): else: logger.warning(f"System: No nodes found") return position + +def get_closest_nodes(nodeInt=1,returnCount=3): + node_list = [] + if nodeInt == 1: + if interface1.nodes: + for node in interface1.nodes.values(): + if 'position' in node: + try: + nodeID = node['num'] + latitude = node['position']['latitude'] + longitude = node['position']['longitude'] + + # set radius around BOT position + distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2) + + if (distance < sentry_radius): + if nodeID != myNodeNum1 and myNodeNum2 and str(nodeID) not in sentryIgnoreList: + node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance}) + # calculate distance to node and report + + except Exception as e: + pass + # else: + # # request location data + # try: + # logger.debug(f"System: Requesting location data for {node['id']}") + # interface1.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.error(f"System: No nodes found in closest_nodes on interface {nodeInt}") + return ERROR_FETCHING_DATA + if nodeInt == 2: + if interface2.nodes: + for node in interface2.nodes.values(): + if 'position' in node: + try: + nodeID = node['num'] + latitude = node['position']['latitude'] + longitude = node['position']['longitude'] + + # set radius around BOT position + distance = geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m + + if (distance < sentry_radius): + if nodeID != myNodeNum1 and myNodeNum2 and str(nodeID) not in sentryIgnoreList: + node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance}) + # calculate distance to node and report + + except Exception as e: + pass + + #sort by distance closest to lattitudeValue, longitudeValue + node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2) + + # return the first 3 closest nodes by default + return node_list[:returnCount] + else: + logger.error(f"System: No nodes found in closest_nodes on interface {nodeInt}") + return ERROR_FETCHING_DATA def send_message(message, ch, nodeid=0, nodeInt=1): if message == "": @@ -508,6 +578,13 @@ def suppress_stdout(): async def watchdog(): global retry_int1, retry_int2 + if sentry_enabled: + sentry_loop = 0 + lastSpotted = "" + enemySpotted = "" + sentry_loop2 = 0 + lastSpotted2 = "" + enemySpotted2 = "" # watchdog for connection to the interface while True: await asyncio.sleep(20) @@ -521,6 +598,28 @@ async def watchdog(): logger.error(f"System: communicating with interface1, trying to reconnect: {e}") retry_int1 = True + # Locate Closest Nodes and report them to a secure channel + if sentry_enabled: + try: + closest_nodes1 = get_closest_nodes(1) + if closest_nodes1 != ERROR_FETCHING_DATA: + if closest_nodes1[0]['id'] is not None: + enemySpotted = get_name_from_number(closest_nodes1[0]['id'], 'long', 1) + enemySpotted += ", " + get_name_from_number(closest_nodes1[0]['id'], 'short', 1) + enemySpotted += ", " + str(closest_nodes1[0]['id']) + enemySpotted += ", " + decimal_to_hex(closest_nodes1[0]['id']) + enemySpotted += f" at {closest_nodes1[0]['distance']}m" + except Exception as e: + pass + + if sentry_loop >= sentry_holdoff and lastSpotted != enemySpotted: + logger.warning(f"System: {enemySpotted} is close to your location on Interface1") + send_message(f"Sentry1: {enemySpotted}", secure_channel, 0, 1) + sentry_loop = 0 + lastSpotted = enemySpotted + else: + sentry_loop += 1 + if retry_int1: try: await retry_interface(1) @@ -535,10 +634,31 @@ async def watchdog(): except Exception as e: logger.error(f"System: communicating with interface2, trying to reconnect: {e}") retry_int2 = True + + # Locate Closest Nodes and report them to a secure channel + if sentry_enabled: + try: + closest_nodes2 = get_closest_nodes(2) + if closest_nodes2 != ERROR_FETCHING_DATA: + if closest_nodes2[0]['id'] is not None: + enemySpotted2 = get_name_from_number(closest_nodes2[0]['id'], 'long', 2) + enemySpotted2 += ", " + get_name_from_number(closest_nodes2[0]['id'], 'short', 2) + enemySpotted2 += ", " + str(closest_nodes2[0]['id']) + enemySpotted2 += ", " + decimal_to_hex(closest_nodes2[0]['id']) + enemySpotted += f" at {closest_nodes1[0]['distance']}m" + except Exception as e: + pass + + if sentry_loop2 >= sentry_holdoff and lastSpotted2 != enemySpotted2: + logger.warning(f"System: {enemySpotted2} is close to your location on Interface2") + send_message(f"Sentry2: {enemySpotted2}", secure_channel, 0, 2) + sentry_loop2 = 0 + lastSpotted2 = enemySpotted2 + else: + sentry_loop2 += 1 if retry_int2: try: await retry_interface(2) except Exception as e: logger.error(f"System: retrying interface2: {e}") - diff --git a/modules/wx_meteo.py b/modules/wx_meteo.py index 88d04c5..af65088 100644 --- a/modules/wx_meteo.py +++ b/modules/wx_meteo.py @@ -1,5 +1,5 @@ import openmeteo_requests # pip install openmeteo-requests -from retry_requests import retry # pip install retry-requests +from retry_requests import retry # pip install retry_requests #import requests_cache from modules.log import * diff --git a/pong_bot.py b/pong_bot.py index 9f8b32e..4804c50 100755 --- a/pong_bot.py +++ b/pong_bot.py @@ -221,6 +221,8 @@ async def start_rx(): f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}") if log_messages_to_file: logger.debug(f"System: Logging Messages to disk") + if sentry_enabled: + logger.debug(f"System: Sentry Enabled") if store_forward_enabled: logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}") if useDMForResponse: diff --git a/requirements.txt b/requirements.txt index c6b205a..dd20ae8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ beautifulsoup4 dadjokes openmeteo_requests retry_requests -numpy \ No newline at end of file +numpy +geopy \ No newline at end of file