Compare commits

...

23 Commits

Author SHA1 Message Date
SpudGunMan
409d07436e Update settings.py 2025-06-10 11:22:32 -07:00
SpudGunMan
5ab0001f2b Update system.py
https://github.com/SpudGunMan/meshing-around/issues/154
2025-06-08 20:32:28 -07:00
SpudGunMan
5e34537af7 fix
reference https://github.com/SpudGunMan/meshing-around/issues/154
2025-06-08 19:26:51 -07:00
SpudGunMan
1764bdf4f3 enhance
the database page to include qrz.db
2025-06-07 21:05:49 -07:00
SpudGunMan
2290f07351 Update locationdata.py 2025-06-06 17:54:31 -07:00
SpudGunMan
ee01051cf7 Update config.template 2025-06-06 17:54:29 -07:00
Kelly
de50a52fa6 Merge pull request #153 from rfschmid/allow-dms-to-numeric-short-names 2025-06-05 19:38:44 -07:00
SpudGunMan
8eabfaa9c4 enhance ignore logic 2025-06-04 10:37:58 -07:00
Russell Schmidt
ca7114b058 Allow DMs to numeric short names 2025-06-04 11:55:30 -05:00
Kelly
8b94dc8111 Merge pull request #152 from SudoRand/main 2025-06-01 19:03:01 -07:00
SudoRand
5b26aabb00 Config for whether ollama responds to non-commands
The current behavior is that whenever ollama is enabled the LLM replies
to all non-command message. This setting allows limiting it the LLM to
run only in response to the "ask:" and "askai" commands.

Default is True to keep consistent with previous behavior.
2025-06-01 15:32:08 -06:00
SudoRand
67b3c67348 Fix bug that always enabled news_random_line_only
This looks like a simple typo that accidentally used read_news_enabled
for the news_random_line_only parameter. As a result, the
news_random_line_only setting was always treated as True (since this
line it only executed if read_news_enabled was True). Now it obeys the
configuration value.
2025-06-01 14:02:19 -06:00
SpudGunMan
860cceec59 Update locationdata.py 2025-05-28 15:20:33 -07:00
Kelly
53a0535e55 Merge pull request #149 from SpudGunMan/lab
High Altitude Alerts
2025-05-23 19:14:51 -07:00
SpudGunMan
621f4ad916 enhance 2025-05-23 19:10:15 -07:00
SpudGunMan
118857ec15 Update system.py 2025-05-21 16:52:01 -07:00
SpudGunMan
1be13be92a highfly enhancements 2025-05-21 16:43:56 -07:00
SpudGunMan
895fc3fd37 Update README.md 2025-05-21 16:36:55 -07:00
SpudGunMan
0e0bda60ad Update system.py 2025-05-21 16:36:52 -07:00
SpudGunMan
903767f4b3 Update settings.py 2025-05-21 16:36:46 -07:00
SpudGunMan
f54d362ea0 Update config.template 2025-05-21 16:36:43 -07:00
SpudGunMan
60bb68c6b5 Update system.py 2025-05-21 16:20:54 -07:00
SpudGunMan
feb9a1d9b3 FlightDetector
high alt detection
2025-05-21 16:18:46 -07:00
9 changed files with 92 additions and 20 deletions

View File

@@ -38,6 +38,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
### Proximity Alerts
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
- **High Flying Alerts**: Get notified when nodes with high altitude are seen on mesh
### CheckList / Check In Out
- **Asset Tracking**: Maintain a list of node/asset checkin and checkout. Usefull for accountability of people, assets. Radio-Net, FEMA, Trailhead.
@@ -181,6 +182,8 @@ SentryRadius = 100 # radius in meters to detect someone close to the bot
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
highFlyingAlert = True # HighFlying Node alert
highFlyingAlertAltitude = 2000 # Altitude in meters to trigger the alert
```
### E-Mail / SMS Settings
@@ -479,6 +482,7 @@ I used ideas and snippets from other responder bots and want to call them out!
- **Josh**: For more bashing on installer!
- **dj505**: trying it on windows!
- **mikecarper**: ideas, and testing. hamtest
- **c.merphy360**: high altitude alerts
- **Cisien, bitflip, **Woof**, **propstg**, **trs2982**, **Josh** and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **Meshtastic Discord Community**: For tossing out ideas and testing code.

View File

@@ -60,6 +60,9 @@ ollama = False
# ollamaModel = llama3.1
# server instance to use (defaults to local machine install)
ollamaHostName = http://localhost:11434
# Produce LLM replies to messages that aren't commands?
# If False, the LLM only replies to the "ask:" and "askai" commands.
llmReplyToNonCommands = True
# StoreForward Enabled and Limits
StoreForward = True
@@ -103,6 +106,12 @@ SentryChannel = 2
SentryHoldoff = 9
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
# HighFlying Node alert
highFlyingAlert = True
# Altitude in meters to trigger the alert
highFlyingAlertAltitude = 2000
# Channel to send Alert when the high flying node is detected
highFlyingAlertChannel = 2
[bbs]
enabled = True
@@ -190,7 +199,7 @@ reverse_in_out = False
# QRZ Hello to new nodes with message
enabled = False
qrz_db = data/qrz.db
qrz_hello_string = "send CMD or DM me for more info."
qrz_hello_string = "MeshBot says Hello! DM for more info."
# Training mode will not send the hello message to new nodes
training = True
@@ -291,7 +300,7 @@ splitDelay = 0.0
MESSAGE_CHUNK_SIZE = 160
# Request Acknowledgement of message OTA
wantAck = False
# Max limit buffer for radio testing. 233 is hard limit 2.5+ firmware
# Max limit buffer for radio testing
maxBuffer = 200
#Enable Extra logging of Hop count data
enableHopLogs = False

View File

@@ -350,7 +350,8 @@ def get_database_info():
os.path.join(base_dir, 'mmind_hs.pkl'),
os.path.join(base_dir, 'golfsim_hs.pkl'),
os.path.join(base_dir, 'bbsdb.pkl'),
os.path.join(base_dir, 'bbsdm.pkl')]
os.path.join(base_dir, 'bbsdm.pkl'),
os.path.join(base_dir, 'qrz.db')]
for file in databaseFiles:
try:
@@ -371,6 +372,16 @@ def get_database_info():
bbsdb = pickle.load(f)
elif 'bbsdm' in file:
bbsdm = pickle.load(f)
elif 'qrz.db' in file:
# open the qrz.db sqllite file
import sqlite3
conn = sqlite3.connect(file)
cursor = conn.cursor()
cursor.execute("SELECT * FROM qrz")
qrz_db = cursor.fetchall()
# convert to a list of strings
qrz_db = [f"{row[0]}: {row[1]} {row[2]}" for row in qrz_db]
conn.close()
except Exception as e:
print(f"Warning issue reading database file: {str(e)}")
if 'lemonstand' in file:
@@ -425,7 +436,8 @@ def get_database_info():
'golfsim_score': golfsim_score,
'banList': banList,
'adminList': adminList,
'sentryIgnoreList': sentryIgnoreList
'sentryIgnoreList': sentryIgnoreList,
'qrz_db': qrz_db if 'qrz_db' in locals() else "no data"
}
def generate_main_html(log_data, system_info):
@@ -913,6 +925,11 @@ def generate_database_html(database_info):
<tr><td>Mastermind</td><td>${mmind_score}</td></tr>
<tr><td>Golf Simulator</td><td>${golfsim_score}</td></tr>
</table>
<h1>QRZ Database</h1>
<p>QRZ Database holds heard nodeID and Shortname</p>
<table>
<tr><td>${qrz_db}</td></tr>
</table>
</body>
</html>
"""

View File

@@ -359,7 +359,8 @@ def get_database_info():
os.path.join(base_dir, 'mmind_hs.pkl'),
os.path.join(base_dir, 'golfsim_hs.pkl'),
os.path.join(base_dir, 'bbsdb.pkl'),
os.path.join(base_dir, 'bbsdm.pkl')]
os.path.join(base_dir, 'bbsdm.pkl'),
os.path.join(base_dir, 'qrz.db')]
for file in databaseFiles:
try:
@@ -380,6 +381,16 @@ def get_database_info():
bbsdb = pickle.load(f)
elif 'bbsdm' in file:
bbsdm = pickle.load(f)
elif 'qrz.db' in file:
#open the qrz.db sqllite file
import sqlite3
conn = sqlite3.connect(file)
cursor = conn.cursor()
cursor.execute("SELECT * FROM qrz")
qrz_db = cursor.fetchall()
# convert to a list of strings
qrz_db = [f"{row[0]}: {row[1]} {row[2]}" for row in qrz_db]
conn.close()
except Exception as e:
print(f"Warning issue reading database file: {str(e)}")
if 'lemonstand' in file:
@@ -434,7 +445,8 @@ def get_database_info():
'golfsim_score': golfsim_score,
'banList': banList,
'adminList': adminList,
'sentryIgnoreList': sentryIgnoreList
'sentryIgnoreList': sentryIgnoreList,
'qrz_db': qrz_db if 'qrz_db' in locals() else "no data"
}
def generate_main_html(log_data, system_info):
@@ -1207,6 +1219,11 @@ def generate_database_html(database_info):
<tr><td>Mastermind</td><td>${mmind_score}</td></tr>
<tr><td>Golf Simulator</td><td>${golfsim_score}</td></tr>
</table>
<h1>QRZ Database</h1>
<p>QRZ Database holds heard nodeID and Shortname</p>
<table>
<tr><td>${qrz_db}</td></tr>
</table>
</body>
</html>
"""

View File

@@ -804,7 +804,7 @@ def handle_bbspost(message, message_from_id, deviceID):
toNode = int(toNode.strip("!"),16)
except ValueError as e:
toNode = 0
elif toNode.isalpha() or not toNode.isnumeric():
elif toNode.isalpha() or not toNode.isnumeric() or len(toNode) < 5:
# try short name
toNode = get_num_from_short_name(toNode, deviceID)
@@ -1243,7 +1243,7 @@ def onReceive(packet, interface):
playingGame = False
if not playingGame:
if llm_enabled:
if llm_enabled and llmReplyToNonCommands:
# respond with LLM
llm = handle_llm(message_from_id, channel_number, rxNode, message_string, publicChannel)
send_message(llm, channel_number, message_from_id, rxNode)
@@ -1406,6 +1406,8 @@ async def start_rx():
logger.debug(f"System: MOTD Enabled using {MOTD}")
if sentry_enabled:
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
if highfly_enabled:
logger.debug(f"System: HighFly Enabled using {highfly_altitude}m limit reporting to channel:{highfly_channel}")
if store_forward_enabled:
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
if useDMForResponse:

View File

@@ -31,7 +31,7 @@ def read_file(file_monitor_file_path, random_line_only=False):
def read_news():
# read the news file on demand
return read_file(news_file_path, read_news_enabled)
return read_file(news_file_path, news_random_line_only)
def write_news(content, append=False):

View File

@@ -467,6 +467,7 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
# get the latest IPAWS alert from FEMA
alert = ''
alerts = []
linked_data = ''
# set the API URL for IPAWS
namespace = "urn:oasis:names:tc:emergency:cap:1.2"
@@ -496,15 +497,22 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
link += "?pin=" + ipawsPIN
# get the linked alert data from FEMA
linked_data = requests.get(link, timeout=urlTimeoutSeconds)
if not linked_data.ok:
if not linked_data.ok or not linked_data.text.strip():
# if the linked data is not ok, skip this alert
#logger.warning(f"System: iPAWS Error fetching linked alert data from {link}")
continue
else:
linked_xml = xml.dom.minidom.parseString(linked_data.text)
# this alert is a full CAP alert
except (requests.exceptions.RequestException):
logger.warning(f"System: iPAWS Error fetching embedded alert data from {link}")
continue
# this alert is a full CAP alert
linked_xml = xml.dom.minidom.parseString(linked_data.text)
except xml.parsers.expat.ExpatError:
logger.warning(f"System: iPAWS Error parsing XML from {link}")
continue
except Exception as e:
logger.debug(f"System: iPAWS Error processing alert data from {link}: {e}")
continue
for info in linked_xml.getElementsByTagName("info"):
# extract values from XML
@@ -539,10 +547,15 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
if (sameVal in mySAME) or (geocode_value in mySAME):
# ignore the FEMA test alerts
if ignoreFEMAenable:
ignore_alert = False
for word in ignoreFEMAwords:
if word.lower() in headline.lower():
logger.debug(f"System: Ignoring FEMA Alert: {headline} containing {word} at {areaDesc}")
continue
ignore_alert = True
break
if ignore_alert:
continue
# add to alerts list
alerts.append({
@@ -632,10 +645,10 @@ def get_volcano_usgs(lat=0, lon=0):
try:
volcano_data = requests.get(usgs_volcano_url, timeout=urlTimeoutSeconds)
if not volcano_data.ok:
logger.warning("System: USGS fetching volcano alerts from USGS")
logger.warning("System: Issue with fetching volcano alerts from USGS")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("System: USGS fetching volcano alerts from USGS")
logger.warning("System: Issue with fetching volcano alerts from USGS")
return ERROR_FETCHING_DATA
volcano_json = volcano_data.json()
# extract alerts from main feed

View File

@@ -222,6 +222,7 @@ try:
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
llmReplyToNonCommands = config['general'].getboolean('llmReplyToNonCommands', True)
# emergency response
emergency_responder_enabled = config['emergencyHandler'].getboolean('enabled', False)
emergency_responder_alert_channel = config['emergencyHandler'].getint('alert_channel', 2) # default 2
@@ -235,6 +236,9 @@ try:
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
email_sentry_alerts = config['sentry'].getboolean('emailSentryAlerts', False) # default False
highfly_enabled = config['sentry'].getboolean('highFlyingAlert', True) # default True
highfly_altitude = config['sentry'].getint('highFlyingAlertAltitude', 2000) # default 2000 meters
highfly_channel = config['sentry'].getint('highFlyingAlertChannel', 2) # default 2
# location
location_enabled = config['location'].getboolean('enabled', True)
@@ -285,7 +289,7 @@ try:
# qrz hello
qrz_hello_enabled = config['qrz'].getboolean('enabled', False)
qrz_db = config['qrz'].get('qrz_db', 'data/qrz.db')
qrz_hello_string = config['qrz'].get('qrz_hello_string', 'send CMD or DM me for more info.')
qrz_hello_string = config['qrz'].get('qrz_hello_string', 'MeshBot says Hello! DM for more info.')
train_qrz = config['qrz'].getboolean('training', True)
# E-Mail Settings
@@ -330,7 +334,7 @@ try:
# file monitor
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
file_monitor_broadcastCh = config['fileMon'].get('broadcastCh', '2').split(',') # default Channel 2
read_news_enabled = config['fileMon'].getboolean('enable_read_news', False) # default disabled
news_file_path = config['fileMon'].get('news_file_path', 'news.txt') # default news.txt
news_random_line_only = config['fileMon'].getboolean('news_random_line', False) # default False

View File

@@ -978,6 +978,12 @@ def consumeMetadata(packet, rxNode=0):
for key in keys:
positionMetadata[nodeID][key] = position_data.get(key, 0)
# if altitude is over 2000 send a log and message for high-flying nodes
if position_data.get('altitude', 0) > highfly_altitude and highfly_enabled:
logger.info(f"System: High Altitude {position_data['altitude']}m on Device: {rxNode} NodeID: {nodeID}")
send_message(f"High Altitude {position_data['altitude']}m on Device:{rxNode} Node:{get_name_from_number(nodeID,'short',rxNode)}", highfly_channel, 0, rxNode)
time.sleep(responseDelay)
# Keep the positionMetadata dictionary at a maximum size of 20
if len(positionMetadata) > 20:
@@ -1096,7 +1102,7 @@ async def handleFileWatcher():
# if fileWatchBroadcastCh list contains multiple channels, broadcast to all
if type(file_monitor_broadcastCh) is list:
for ch in file_monitor_broadcastCh:
if antiSpam and ch != publicChannel:
if antiSpam and int(ch) != publicChannel:
send_message(msg, int(ch), 0, 1)
time.sleep(responseDelay)
if multiple_interface:
@@ -1151,7 +1157,7 @@ async def retry_interface(nodeID):
if interface_type == 'serial':
globals()[f'interface{nodeID}'] = meshtastic.serial_interface.SerialInterface(globals().get(f'port{nodeID}'))
elif interface_type == 'tcp':
globals()[f'interface{nodeID}'] = meshtastic.tcp_interface.TCPInterface(globals().get(f'hostname{nodeID}'))
globals()[f'interface{nodeID}'] = meshtastic.tcp_interface.TCPInterface(globals().get(f'host{nodeID}'))
elif interface_type == 'ble':
globals()[f'interface{nodeID}'] = meshtastic.ble_interface.BLEInterface(globals().get(f'mac{nodeID}'))
logger.debug(f"System: Interface{nodeID} Opened!")