Merge pull request #38 from SpudGunMan/sentry

Sentry Mode
This commit is contained in:
Kelly
2024-08-11 23:04:33 -07:00
committed by GitHub
8 changed files with 158 additions and 3 deletions
+14
View File
@@ -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
```
+10
View File
@@ -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
+2
View File
@@ -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:
+6
View File
@@ -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.")
+121 -1
View File
@@ -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}")
+1 -1
View File
@@ -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 *
+2
View File
@@ -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:
+2 -1
View File
@@ -9,4 +9,5 @@ beautifulsoup4
dadjokes
openmeteo_requests
retry_requests
numpy
numpy
geopy