Compare commits

..

60 Commits

Author SHA1 Message Date
SpudGunMan a8b2aefa28 Update locationdata.py 2025-10-21 22:31:57 -07:00
SpudGunMan 849565cacb Update joke.py 2025-10-21 21:40:20 -07:00
SpudGunMan fdec3a6754 Update README.md 2025-10-21 20:56:47 -07:00
SpudGunMan f3a97bc567 Update README.md 2025-10-21 20:45:40 -07:00
SpudGunMan 02625ad0f2 Update install.sh 2025-10-21 20:18:55 -07:00
SpudGunMan b4ba4b0daf Update locationdata.py 2025-10-21 18:52:40 -07:00
SpudGunMan fd7f8a94f5 Update locationdata.py 2025-10-21 18:47:29 -07:00
SpudGunMan d252250edd last_alert_time 🚀
throttle sending alerts for the same node more than once every 30 minutes
2025-10-21 16:42:36 -07:00
SpudGunMan d6410e0461 Update joke.py 2025-10-21 15:47:06 -07:00
SpudGunMan 050b4ab3ce Update joke.py 2025-10-21 15:46:48 -07:00
SpudGunMan 8ac1a1eed7 Update joke.py 2025-10-21 15:45:30 -07:00
SpudGunMan 370a417ce6 Update config.template 2025-10-21 15:10:13 -07:00
SpudGunMan 378b05df35 PingRefactor
anyone notice this?
2025-10-21 14:50:06 -07:00
SpudGunMan d002c5ede8 remove LastHop 2025-10-21 14:26:43 -07:00
SpudGunMan cd03cc56b4 🐇 2025-10-21 14:24:24 -07:00
SpudGunMan d4fd484706 sentry_alert.sh
this enhances the sentry to optionally run a shell command you would create in the script/directory which will fire every time the alert fires. sentry_alert_near.sh and sentry_alert_far.sh are the needed files. it will error and remind you it cant find them.
2025-10-21 14:21:09 -07:00
SpudGunMan 82d519279e servicePackAttack
enhance for armbian builds
2025-10-21 13:34:38 -07:00
SpudGunMan 09302e8c91 ntp 2025-10-21 13:17:15 -07:00
SpudGunMan 91fc4605ec enhance 2025-10-21 12:57:10 -07:00
SpudGunMan abc6c07ee3 Update locationdata.py 2025-10-21 12:52:34 -07:00
SpudGunMan bbf8b04bd3 Update locationdata.py 2025-10-21 12:45:29 -07:00
SpudGunMan 5fd293c990 only seen with soft nodes
## FIXME needs better like a default interface setting or hash lookup
2025-10-21 12:33:13 -07:00
SpudGunMan a9a65a6c6d refactor rxInt 2025-10-21 11:00:26 -07:00
SpudGunMan 34e95c86d6 log IP if there 2025-10-21 10:52:28 -07:00
Kelly 80891090c3 Merge pull request #222 from pdxlocations/allow-port-numbers
Missed a Spot to support hostname:port
thanks!
2025-10-21 10:49:00 -07:00
SpudGunMan 1b098fbf7b Update pong_bot.py 2025-10-21 10:47:42 -07:00
SpudGunMan 165d76cf8d add IP 2025-10-21 10:45:44 -07:00
Kelly 045c9d433b Merge pull request #223 from SpudGunMan/copilot/add-pylint-disable-comment 2025-10-21 10:28:51 -07:00
SpudGunMan fbe5e008de Revert "Update joke.py"
This reverts commit 004adc7d9a.
2025-10-21 10:28:02 -07:00
Kelly 004adc7d9a Update joke.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-21 10:23:12 -07:00
copilot-swe-agent[bot] 9a2033452f Add Pylint disable comment above variable on line 58
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-21 17:08:51 +00:00
copilot-swe-agent[bot] 5638204f82 Initial plan 2025-10-21 17:05:27 +00:00
pdxlocations e5c3b0cceb refactor 2025-10-21 09:33:01 -07:00
pdxlocations 18ac53b230 Refactor TCP interface handling to support hostname:poert 2025-10-21 09:17:54 -07:00
SpudGunMan 4aa65dad6a Update mesh_bot.py
https://github.com/SpudGunMan/meshing-around/issues/220
2025-10-21 09:03:00 -07:00
SpudGunMan f65a7b7934 refactorMwx 2025-10-21 06:56:03 -07:00
SpudGunMan 886293087a Update greetings.yml 2025-10-20 23:10:25 -07:00
SpudGunMan e05e6f3451 Update greetings.yml 2025-10-20 23:07:31 -07:00
SpudGunMan 4b5dd934e9 enhance 2025-10-20 23:07:03 -07:00
Kelly 008ddfb5a2 Merge pull request #221 from SpudGunMan/lab
DependaBot
2025-10-20 23:02:13 -07:00
SpudGunMan e1330b9b9e DependaBot
DependaBot
2025-10-20 22:56:34 -07:00
SpudGunMan 7b43213094 Update README.md 2025-10-20 21:57:46 -07:00
SpudGunMan b17c2b17ee Update system.py 2025-10-20 20:20:25 -07:00
SpudGunMan b6505ee577 Update system.py 2025-10-20 20:19:43 -07:00
SpudGunMan f7379b7ca5 Update system.py 2025-10-20 20:19:30 -07:00
SpudGunMan ec9a1d88db Update system.py 2025-10-20 20:15:29 -07:00
SpudGunMan a339570afe Update system.py 2025-10-20 20:10:08 -07:00
SpudGunMan f5af9f419a Update system.py 2025-10-20 20:06:39 -07:00
SpudGunMan ad5c1c90da Update system.py 2025-10-20 19:55:57 -07:00
SpudGunMan eb1e0c82ea enhance Sentinel
# list of watched nodes numbers
sentryWatchList =
monitors for INSIDE or OUTSIDE the zone
2025-10-20 19:50:06 -07:00
SpudGunMan 8d2277bc59 Update SECURITY.md 2025-10-20 17:46:57 -07:00
Kelly dc9908a72c Revise security support information and reporting process
Updated the security support table and reporting guidelines.
2025-10-20 17:08:31 -07:00
SpudGunMan 21123d2993 sysinfo from a DM
now lets you see the IP addresses of the bot
2025-10-20 16:33:58 -07:00
SpudGunMan 7dc3134d0b Update system.py 2025-10-20 16:20:27 -07:00
SpudGunMan b125178492 Update survey.py 2025-10-20 16:16:49 -07:00
SpudGunMan 2ad9e84c33 Update pong_bot.py 2025-10-20 15:55:30 -07:00
SpudGunMan 63bd288caa fixMOTD
theMOTD is how did this happen
2025-10-20 15:40:32 -07:00
SpudGunMan 5c7d199831 surveyReport
run survey report to see return data
2025-10-20 15:34:09 -07:00
SpudGunMan f56a39eeb6 refactor 2025-10-20 13:07:30 -07:00
SpudGunMan ae5991ee39 moreTime
to spare
2025-10-20 13:03:33 -07:00
21 changed files with 422 additions and 185 deletions
+11
View File
@@ -0,0 +1,11 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
+18
View File
@@ -0,0 +1,18 @@
name: Greetings
on:
issues:
types:
- opened
permissions:
issues: write
jobs:
greeting:
runs-on: ubuntu-latest
steps:
- uses: actions/first-interaction@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue_message: "Dependabot's first issue"
+7 -6
View File
@@ -2,6 +2,8 @@
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, [mesh_bot.py](mesh_bot.py) has you covered.
TLDR: [Getting Started](#getting-started)
![Example Use](etc/pong-bot.jpg "Example Use")
## Key Features
@@ -23,10 +25,9 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **Flexible Messaging**: send mail and messages, between networks.
### Advanced Messaging Capabilities
- **Mail Messaging**: Leave messages for other devices, which are sent as DMs when the device is seen.
- **Mail Messaging**: Leave messages for other devices, which are sent as DMs when the device is seen. Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
- **Scheduler**: Schedule messages like weather updates or reminders for weekly VHF nets.
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
- **Store and Forward**: Like voicemail, see messages missed with the `messages` command. Can also log messages locally to disk.
- **BBS Linking**: Combine multiple bots to expand BBS reach.
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS(Email) expanding visibility.
- **New Node Hello**: Send a hello to any new node seen in text message.
@@ -39,7 +40,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **GeoMeasuring**: HowFar from point to point using collected GPS packets on the bot to plot a course or space. Find Center of points for Fox&Hound direction finding.
### Proximity Alerts
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites, or put a geo-fence. You can also run a script or send a email. Another idea is to lower the cycle and use the bot as a 'king of the hill' or 🧭geocache game. You can also run a script to change a node config or turn on the lights🚥, have it drop an alert.txt to send a message like "Hello Start the 📊Survey"
- **High Flying Alerts**: Get notified when nodes with high altitude are seen on mesh
- **Voice/Command Triggers**: The following keywords can be used via voice (VOX) to trigger bot functions "Hey Chirpy!"
- Say "Hey Chirpy.."
@@ -197,7 +198,7 @@ Players can `q: join` to join the game, `q: leave` to leave the game, `q: score`
To Answer a question, just type the answer prefixed with `q: <answer>`
#### Survey
To use the Survey feature edit the json files in data/survey multiple surveys are possible such as `survey snow`
To use the Survey feature edit the json files in data/survey multiple surveys are possible such as `survey snow` you can pull data back with `survey report` or `survey report snow`
## Other Install Options
@@ -469,7 +470,7 @@ broadcastCh = 2 # channel to send the message to can be 2,3 multiple channels co
enable_read_news = False # news command will return the contents of a text file
news_file_path = news.txt
news_random_line = False # only return a single random line from the news file
enable_runShellCmd = False # enable the use of exernal shell commands, this enables some data in `sysinfo`
enable_runShellCmd = False # enable the use of exernal shell commands, this enables more data in `sysinfo` DM
# if runShellCmd and you think it is safe to allow the x: command to run
# direct shell command handler the x: command in DMs user must be in bbs_admin_list
allowXcmd = True
+14
View File
@@ -0,0 +1,14 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| git pull| :white_check_mark: |
## Reporting a Vulnerability
If its serious, its likley big. otherwise post issues, reachout on discord.
+18 -9
View File
@@ -127,19 +127,28 @@ alert_interface = 1
[sentry]
# detect anyone close to the bot
SentryEnabled = True
reqLocationEnabled = False
emailSentryAlerts = False
# radius in meters to detect someone close to the bot
SentryRadius = 100
# device interface and channel to send the alert message to
SentryInterface = 1
SentryChannel = 2
# holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 9
emailSentryAlerts = False
# Enable detection sensor alert, requires external GPIO sensor connected to node
detectionSensorAlert = False
# list of ignored nodes numbers ex: 2813308004,4258675309
sentryIgnoreList =
# Enable detection sensor alert, requires external sensor connected to node
detectionSensorAlert = False
# list of watched nodes numbers ex: 2813308004,4258675309
sentryWatchList =
# radius in meters to detect someone close to the bot
SentryRadius = 100
# holdoff time multiplied by seconds(20) of the watchdog
SentryHoldoff = 9
# Enable running external shell command when sentry alert is triggered
cmdShellSentryAlerts = False
# External shell command to run when sentry alert is triggered
sentryAlertNear = sentry_alert_near.sh
sentryAlertAway = sentry_alert_away.sh
# HighFlying Node alert
highFlyingAlert = True
@@ -239,7 +248,7 @@ enableDEalerts = False
myRegionalKeysDE = 110000000000,120510000000
# Satalite Pass Prediction
# Register for free API https://www.n2yo.com/login/
# Register for free API https://www.n2yo.com/login/ personal data page at bottom 'Are you developer?'
n2yoAPIKey =
# NORAD list https://www.n2yo.com/satellites/
satList = 25544,7530
+2
View File
@@ -14,6 +14,8 @@ Group=pi
WorkingDirectory=/dir/
ExecStart=python3 mesh_bot.py
ExecStop=pkill -f mesh_bot.py
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs
+5
View File
@@ -14,6 +14,8 @@ Group=pi
WorkingDirectory=/dir/
ExecStart=python3 modules/web.py
ExecStop=pkill -f mesh_bot_w3.py
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs
@@ -21,3 +23,6 @@ Environment=PYTHONUNBUFFERED=1
Restart=on-failure
Type=notify #try simple if any problems
[Install]
WantedBy=default.target
+2
View File
@@ -14,6 +14,8 @@ Group=pi
WorkingDirectory=/dir/
ExecStart=python3 pong_bot.py
ExecStop=pkill -f pong_bot.py
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
# Disable Python's buffering of STDOUT and STDERR, so that output from the
# service shows up immediately in systemd's logs
+14
View File
@@ -13,6 +13,12 @@ printf "Installer works best in raspian/debian/ubuntu or foxbuntu embedded syste
printf "If there is a problem, try running the installer again.\n"
printf "\nChecking for dependencies...\n"
# fuse
fi [[ -f config.ini ]]; then
printf "\nDetected existing installation, please backup and remove existing installation before proceeding\n"
exit 1
fi
# check if we are in /opt/meshing-around
if [ $program_path != "/opt/meshing-around" ]; then
printf "\nIt is suggested to project path to /opt/meshing-around\n"
@@ -207,6 +213,12 @@ sudo chown -R $whoami:$whoami $program_path/logs
sudo chown -R $whoami:$whoami $program_path/data
echo "Permissions set for meshbot on logs and data directories"
# check and see if some sort of NTP is running
if ! systemctl is-active --quiet ntp.service && \
! systemctl is-active --quiet systemd-timesyncd.service && \
! systemctl is-active --quiet chronyd.service; then
printf "\nNo NTP service detected, it is recommended to have NTP running for proper bot operation.\n"
# set the correct user in the service file
replace="s|User=pi|User=$whoami|g"
sed -i $replace etc/pong_bot.service
@@ -299,6 +311,7 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
printf "Reporting chron job added to run report_generator5.py\n" >> install_notes.txt
printf "chronjob: %s\n" "$chronjob" >> install_notes.txt
printf "*** Stay Up to date using 'bash update.sh' ***\n" >> install_notes.txt
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n" >> install_notes.txt
@@ -344,6 +357,7 @@ else
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
printf "*** Stay Up to date using 'bash update.sh' ***\n" >> install_notes.txt
fi
printf "\nInstallation complete!\n"
+61 -48
View File
@@ -11,6 +11,7 @@ except ImportError:
import asyncio
import time # for sleep, get some when you can :)
import random
from datetime import datetime
from modules.log import *
from modules.system import *
@@ -90,7 +91,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
"survey": lambda: surveyHandler(message, message_from_id, deviceID),
"s:": lambda: surveyHandler(message, message_from_id, deviceID),
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID),
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID, isDM),
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"tictactoe": lambda: handleTicTacToe(message, message_from_id, deviceID),
@@ -250,10 +251,13 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
else:
msg = "🔊 Can you hear me now?"
if hop == "Direct":
msg = msg + f"SNR:{snr} RSSI:{rssi}"
else:
msg = msg + hop
# append SNR/RSSI or hop info
if hop.startswith("Direct?") and (snr != 0 or rssi != 0):
msg += f"? SNR:{snr} RSSI:{rssi}"
elif hop.startswith("Direct"):
msg += f"SNR:{snr} RSSI:{rssi}"
elif hop:
msg += f"{hop}"
if "@" in message:
msg = msg + " @" + message.split("@")[1]
@@ -336,7 +340,7 @@ def handle_emergency(message_from_id, deviceID, message):
def handle_motd(message, message_from_id, isDM):
global MOTD
msg = ''
msg = MOTD
isAdmin = isNodeAdmin(message_from_id)
if "?" in message:
msg = "Message of the day, set with 'motd $ HelloWorld!'"
@@ -1036,7 +1040,6 @@ def quizHandler(message, nodeID, deviceID):
return "🧠Please provide an answer or command, or send q: ?"
def surveyHandler(message, nodeID, deviceID):
from modules.settings import surveyTracker
user_id = nodeID
location = get_node_location(nodeID, deviceID)
msg = ''
@@ -1055,10 +1058,13 @@ def surveyHandler(message, nodeID, deviceID):
return survey_module.end_survey(user_id=nodeID)
# Handle report command
if surveySays == "report":
#return survey_module.quiz_report()
# reminder to fix int and open question reporting
return "Report not implemented yet"
if 'report' in surveySays:
if str(nodeID) not in bbs_admin_list:
return "You do not have permission to view survey reports."
# remove the words 'survey' and 'report' from the message
report = msg_lower.replace("survey", "").replace("report", "").strip()
results = survey_module.get_survey_results(survey_name=report if report else None)
return survey_module.format_survey_results(results)
# Update last played or add new tracker entry
found = False
@@ -1256,7 +1262,7 @@ def handle_sun(message_from_id, deviceID, channel_number, vox=False):
location = get_node_location(message_from_id, deviceID, channel_number)
return get_sun(str(location[0]), str(location[1]))
def sysinfo(message, message_from_id, deviceID):
def sysinfo(message, message_from_id, deviceID, isDM):
if "?" in message:
return "sysinfo command returns system information."
else:
@@ -1268,6 +1274,11 @@ def sysinfo(message, message_from_id, deviceID):
if shellData == "" or shellData == None:
# no data returned from the script
shellData = "shell script data missing"
# if not an admin remove any line in the shellData that had 'IP:' in it
if (str(message_from_id) not in bbs_admin_list) or (not isDM):
shell_lines = shellData.splitlines()
filtered_lines = [line for line in shell_lines if 'IP:' not in line]
shellData = "\n".join(filtered_lines)
return get_sysinfo(message_from_id, deviceID) + "\n" + shellData.rstrip()
else:
return get_sysinfo(message_from_id, deviceID)
@@ -1447,11 +1458,13 @@ def onReceive(packet, interface):
# Values assinged to the packet
rxNode = message_from_id = snr = rssi = hop = hop_away = channel_number = hop_start = hop_count = hop_limit = 0
pkiStatus = (False, 'ABC')
rxNodeHostName = None
replyIDset = False
emojiSeen = False
simulator_flag = False
isDM = False
channel_name = "unknown"
session_passkey = None
playingGame = False
if DEBUGpacket:
@@ -1461,41 +1474,33 @@ def onReceive(packet, interface):
# Debug print the packet for debugging
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
# set the value for the incomming interface
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
if port1 in rxInterface: rxNode = 1
elif multiple_interface and port2 in rxInterface: rxNode = 2
elif multiple_interface and port3 in rxInterface: rxNode = 3
elif multiple_interface and port4 in rxInterface: rxNode = 4
elif multiple_interface and port5 in rxInterface: rxNode = 5
elif multiple_interface and port6 in rxInterface: rxNode = 6
elif multiple_interface and port7 in rxInterface: rxNode = 7
elif multiple_interface and port8 in rxInterface: rxNode = 8
elif multiple_interface and port9 in rxInterface: rxNode = 9
# determine the rxNode based on the interface type
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
if rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
if rxType == 'BLEInterface':
if interface1_type == 'ble': rxNode = 1
elif multiple_interface and interface2_type == 'ble': rxNode = 2
elif multiple_interface and interface3_type == 'ble': rxNode = 3
elif multiple_interface and interface4_type == 'ble': rxNode = 4
elif multiple_interface and interface5_type == 'ble': rxNode = 5
elif multiple_interface and interface6_type == 'ble': rxNode = 6
elif multiple_interface and interface7_type == 'ble': rxNode = 7
elif multiple_interface and interface8_type == 'ble': rxNode = 8
elif multiple_interface and interface9_type == 'ble': rxNode = 9
rxNodeHostName = interface.__dict__.get('ip', None)
rxNode = next(
(i for i in range(1, 10)
if multiple_interface and rxHost and
globals().get(f'hostname{i}', '').split(':', 1)[0] in rxHost and
globals().get(f'interface{i}_type', '') == 'tcp'),None)
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
rxNode = next(
(i for i in range(1, 10)
if globals().get(f'port{i}', '') in rxInterface),None)
if rxType == 'BLEInterface':
rxNode = next(
(i for i in range(1, 10)
if globals().get(f'interface{i}_type', '') == 'ble'),0)
if rxNode is None:
# default to interface 1 ## FIXME needs better like a default interface setting or hash lookup
if 'decoded' in packet and packet['decoded']['portnum'] in ['ADMIN_APP', 'SIMULATOR_APP']:
session_passkey = packet.get('decoded', {}).get('admin', {}).get('sessionPasskey', None)
rxNode = 1
# check if the packet has a channel flag use it ## FIXME needs to be channel hash lookup
if packet.get('channel'):
channel_number = packet.get('channel')
@@ -1596,8 +1601,8 @@ def onReceive(packet, interface):
else:
hop_count = hop_away
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
hop = "Last Hop"
if hop == "" and hop_count > 0:
hop = f"{hop_count} Hop" if hop_count == 1 else f"{hop_count} Hops"
if hop_start == hop_limit and "lora" in str(transport_mechanism).lower():
hop = "Direct"
@@ -1605,11 +1610,15 @@ def onReceive(packet, interface):
if ((hop_start == 0 and hop_limit >= 0) or via_mqtt or ("mqtt" in str(transport_mechanism).lower())):
hop = "MQTT"
## FIXME should this be here?
if hop == "" and hop_count ==0 and (snr != 0 or rssi != 0):
hop = "Direct?"
if "unknown" in str(transport_mechanism).lower() and (snr == 0 and rssi == 0):
hop = "IP-Network"
if enableHopLogs:
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism}")
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism} Hostname:{rxNodeHostName}")
# check with stringSafeChecker if the message is safe
if stringSafeCheck(message_string) is False:
@@ -1633,7 +1642,7 @@ def onReceive(packet, interface):
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
else:
# DM is useful for games or LLM
if games_enabled and (hop == "Direct" or hop_count < game_hop_limit):
if games_enabled and ("Direct" in hop or hop_count < game_hop_limit):
playingGame = checkPlayingGame(message_from_id, message_string, rxNode, channel_number)
elif hop_count >= game_hop_limit:
if games_enabled:
@@ -1828,6 +1837,10 @@ async def start_rx():
if sentry_enabled:
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel} requestLOC:{reqLocationEnabled}")
if sentryIgnoreList:
logger.debug(f"System: Sentry BlockList Enabled for nodes: {sentryIgnoreList}")
if sentryWatchList:
logger.debug(f"System: Sentry WatchList Enabled for nodes: {sentryWatchList}")
if highfly_enabled:
logger.debug(f"System: HighFly Enabled using {highfly_altitude}m limit reporting to channel:{highfly_channel}")
+1
View File
@@ -4,6 +4,7 @@
import pickle # pip install pickle
from modules.log import *
import time
from datetime import datetime
useSynchCompression = False
+11 -5
View File
@@ -6,6 +6,7 @@ import asyncio
import random
import os
import subprocess
from datetime import datetime, timedelta
trap_list_filemon = ("readnews",)
@@ -69,28 +70,33 @@ async def watch_file():
return content
await asyncio.sleep(1) # Check every
def call_external_script(message, script="script/runShell.sh"):
# Call an external script with the message as an argument this is a example only
def call_external_script(message, script="runShell.sh"):
# If no path is given, assume script/ directory
if "/" not in script and "\\" not in script:
script = os.path.join("script", script)
try:
current_working_directory = os.getcwd()
script_path = os.path.join(current_working_directory, script)
if not os.path.exists(script_path):
# try the raw script name
# Try the raw script name
script_path = script
if not os.path.exists(script_path):
logger.warning(f"FileMon: Script not found: {script_path}")
return "sorry I can't do that"
# Use subprocess.run for better resource management
result = subprocess.run(
["bash", script_path, message],
capture_output=True,
text=True,
timeout=10
)
if result.returncode != 0:
logger.error(f"FileMon: Script error: {result.stderr.strip()}")
return None
output = result.stdout.strip()
return output
return output if output else None
except Exception as e:
logger.warning(f"FileMon: Error calling external script: {e}")
return None
+21 -9
View File
@@ -46,15 +46,27 @@ lameJokes = [
"Chuck Norris can kill two stones with one bird.",
"Chuck Norris can speak braille.",
"Chuck Norris can build a snowman out of rain.",
"Chuck Norris can hear sign language.",
"Death once had a near-Chuck Norris experience.",
"Chuck Norris can unscramble an egg.",
"Chuck Norris can win a game of Connect Four in only three moves.",
"Chuck Norris can make a snowman out of rain.",
"Chuck Norris can strangle you with a cordless phone.",
"Chuck Norris can do a wheelie on a unicycle.",
"Chuck Norris can kill two stones with one bird."]
"This is a test. A test of the Joke Brodcast System. If this had been an actual joke, you would have been amused.",
"Chuck Norris doesn't join mesh networks. Mesh networks join Chuck's topology.",
"Every time Chuck Norris sends a packet, it arrives before he hits 'send'",
"Chuck Norris doesn't need LoRa. His roundhouse kick has a 15km range with zero latency.",
"When Chuck Norris uses a node, the bandwidth doubles out of fear.",
"Chuck Norris once pinged a device. It replied with an apology and a firmware update.",
"Chuck Norris doesn't use AES encryption. His packets are so secure, they punch hackers in the bits.",
"The Meshtastic protocol has a hidden mode: “Chuck Norris mode.” It only activates when he blinks.",
"Chuck Norris doesn't need a GPS fix. Satellites triangulate themselves around him.",
"Chuck Norris's mesh node doesn't sleep. It meditates while transmitting at full power.",
"Chuck Norris doesn't broadcast. He declares.",
"Chuck Norris once bridged two mesh networks using a shoelace.",
"Chuck Norris's packets don't hop. They teleport out of respect.",
"Chuck Norris doesn't need a repeater. Client_Mute is set to 'Always'.",
"Chuck Norris's mesh messages are entangled. When he sends one, it's already received.",
"Chuck Norris doesn't mesh with others. Others mesh with Chuck.",
"Chuck Norris's node doesn't need a case. The PCB is armored with his beard hair.",
"Chuck Norris once typed “Hello World” and the world replied 'Hello Chuck.'",
]
# pylint: disable=C0103, W0612
imtellingyourightnowiAmTellingYouRightNowThatMotherfErBackThereIsNotReal = ["🐦", "🦅", "🦆", "🦉", "🦜", "🐤", "🐥", "🐣", "🐔", "🐧", "🦚", "🦢", "🦩", "🦤", "🦃", "🐓"]
def tableOfContents():
@@ -178,4 +190,4 @@ def tell_joke(nodeID=0, vox=False):
return renderedLaugh
except Exception as e:
return random.choice(lameJokes)
+1
View File
@@ -8,6 +8,7 @@ from modules.log import *
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
import requests
import json
from datetime import datetime
if not rawLLMQuery:
# this may be removed in the future
+33 -28
View File
@@ -7,6 +7,7 @@ import maidenhead as mh # pip install maidenhead
import requests # pip install requests
import bs4 as bs # pip install beautifulsoup4
import xml.dom.minidom
from datetime import datetime
from modules.log import *
import math
@@ -174,8 +175,8 @@ def get_NOAAtide(lat=0, lon=0):
station_id = ""
location = lat,lon
if float(lat) == 0 and float(lon) == 0:
logger.error("Location:No GPS data, try sending location for tide")
return NO_DATA_NOGPS
lat = latitudeValue
lon = longitudeValue
station_lookup_url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/tidepredstations.json?lat=" + str(lat) + "&lon=" + str(lon) + "&radius=50"
try:
station_data = requests.get(station_lookup_url, timeout=urlTimeoutSeconds)
@@ -239,7 +240,8 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
weather = ""
location = lat,lon
if float(lat) == 0 and float(lon) == 0:
return NO_DATA_NOGPS
lat = latitudeValue
lon = longitudeValue
# get weather data from NOAA units for metric unit = 1 is metric
if use_metric:
@@ -322,6 +324,7 @@ def abbreviate_noaa(data=""):
"between four and five inches possible": "4-5in",
"between five and six inches possible": "5-6in",
"between six and eight inches possible": "6-8in",
"gusts as high as": "gusts to",
}
# Single words (no spaces)
word_replacements = {
@@ -367,6 +370,7 @@ def abbreviate_noaa(data=""):
"temperature": "temp:",
"amounts": "amts:",
"afternoon": "Aftn",
"around": "~",
"evening": "Eve",
}
@@ -388,12 +392,11 @@ def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
alerts = ""
location = lat,lon
if useDefaultLatLon:
lat = latitudeValue
lon = longitudeValue
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
return NO_DATA_NOGPS
else:
if useDefaultLatLon:
lat = latitudeValue
lon = longitudeValue
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
@@ -466,8 +469,8 @@ def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
alerts = ""
location = lat,lon
if float(lat) == 0 and float(lon) == 0:
logger.warning("Location:No GPS data, try sending location for weather alerts")
return NO_DATA_NOGPS
lat = latitudeValue
lon = longitudeValue
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
@@ -527,10 +530,10 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
try:
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
if not alert_data.ok:
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
logger.warning(f"System: iPAWS fetching IPAWS alerts from FEMA (HTTP {alert_data.status_code})")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
except Exception as e:
logger.warning(f"System: iPAWS fetching IPAWS alerts from FEMA failed: {e}")
return ERROR_FETCHING_DATA
# main feed bulletins
@@ -612,14 +615,13 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
# check if the alert is for the SAME location, if wanted keep alert
if (sameVal in mySAMEList) or (geocode_value in mySAMEList) or mySAMEList == ['']:
# ignore the FEMA test alerts
ignore_alert = False
if ignoreFEMAenable:
ignore_alert = False
for word in ignoreFEMAwords:
if word.lower() in headline.lower():
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing {word} at {areaDesc}")
ignore_alert = True
break
ignore_alert = any(
word.lower() in headline.lower()
for word in ignoreFEMAwords)
if ignore_alert:
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing one of {ignoreFEMAwords} at {areaDesc}")
if ignore_alert:
continue
@@ -744,7 +746,7 @@ def get_volcano_usgs(lat=0, lon=0):
return alerts
def get_nws_marine(zone, days=3):
# forcast from NWS coastal products
# forecast from NWS coastal products
try:
marine_pz_data = requests.get(zone, timeout=urlTimeoutSeconds)
if not marine_pz_data.ok:
@@ -753,18 +755,21 @@ def get_nws_marine(zone, days=3):
except (requests.exceptions.RequestException):
logger.warning("Location:Error fetching NWS Marine PZ data")
return ERROR_FETCHING_DATA
marine_pz_data = marine_pz_data.text
#validate data
todayDate = datetime.now().strftime("%Y%m%d")
if marine_pz_data.startswith("Expires:"):
expires = marine_pz_data.split(";;")[0].split(":")[1]
expires_date = expires[:8]
if expires_date < todayDate:
logger.debug("Location: NWS Marine PZ data expired")
if marine_pz_data and marine_pz_data.startswith("Expires:"):
try:
expires = marine_pz_data.split(";;")[0].split(":")[1]
expires_date = expires[:8]
if expires_date < todayDate:
logger.debug("Location: NWS Marine PZ data expired")
return ERROR_FETCHING_DATA
except Exception as e:
logger.debug(f"Location: NWS Marine PZ data parse error: {e}")
return ERROR_FETCHING_DATA
else:
logger.debug("Location: NWS Marine PZ data not valid")
logger.debug("Location: NWS Marine PZ data not valid or empty")
return ERROR_FETCHING_DATA
# process the marine forecast data
+8 -5
View File
@@ -1,7 +1,5 @@
import logging
from logging.handlers import TimedRotatingFileHandler
import re
from datetime import datetime, timedelta
from modules.settings import *
# if LOGGING_LEVEL is not set in settings.py, default to DEBUG
if not LOGGING_LEVEL:
@@ -38,11 +36,17 @@ class CustomFormatter(logging.Formatter):
return formatter.format(record)
class plainFormatter(logging.Formatter):
ansi_escape = re.compile(r'\x1b\[([0-9]+)(;[0-9]+)*m')
ansi_codes = [
'\x1b[38;21m', '\x1b[38;5;231m', '\x1b[38;5;39m', '\x1b[38;5;226m',
'\x1b[38;5;196m', '\x1b[38;5;46m', '\x1b[38;5;129m', '\x1b[31;1m',
'\x1b[37;1m', '\x1b[0m'
]
def format(self, record):
message = super().format(record)
return self.ansi_escape.sub('', message)
for code in self.ansi_codes:
message = message.replace(code, '')
return message
# Create logger
logger = logging.getLogger("MeshBot System Logger")
@@ -56,7 +60,6 @@ msgLogger.propagate = False
# Define format for logs
logFormat = '%(asctime)s | %(levelname)8s | %(message)s'
msgLogFormat = '%(asctime)s | %(message)s'
today = datetime.now()
# Create stdout handler for logging to the console
stdout_handler = logging.StreamHandler()
+4
View File
@@ -271,6 +271,7 @@ try:
secure_interface = config['sentry'].getint('SentryInterface', 1) # default 1
sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
sentryWatchList = config['sentry'].get('sentryWatchList', '').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
@@ -281,6 +282,9 @@ try:
highfly_check_openskynetwork = config['sentry'].getboolean('highflyOpenskynetwork', True) # default True check with OpenSkyNetwork if highfly detected
detctionSensorAlert = config['sentry'].getboolean('detectionSensorAlert', False) # default False
reqLocationEnabled = config['sentry'].getboolean('reqLocationEnabled', False) # default False
cmdShellSentryAlerts = config['sentry'].getboolean('cmdShellSentryAlerts', False) # default False
sentryAlertNear = config['sentry'].get('sentryAlertNear', 'sentry_alert_near.sh') # default sentry_alert_near.sh
sentryAlertFar = config['sentry'].get('sentryAlertFar', 'sentry_alert_far.sh') # default sentry_alert_far.sh
# location
location_enabled = config['location'].getboolean('enabled', True)
+78 -1
View File
@@ -10,6 +10,8 @@
import json
import os # For file operations
import csv
from datetime import datetime
from collections import Counter
from modules.log import *
@@ -98,7 +100,7 @@ class SurveyModule:
try:
with open(filename, 'a', encoding='utf-8') as f:
# Always write: timestamp, userID, position, answers...
timestamp = datetime.datetime.now().strftime('%d%m%Y%H%M%S')
timestamp = datetime.now().strftime('%d%m%Y%H%M%S')
user_id_str = str(user_id)
location = self.responses[user_id].get('location', "N/A")
answers = list(map(str, self.responses[user_id]['answers']))
@@ -108,6 +110,81 @@ class SurveyModule:
except Exception as e:
logger.error(f"Error saving responses to {filename}: {e}")
def format_survey_results(self, results):
if isinstance(results, dict) and "error" in results:
return results["error"]
if not results:
return "No results found."
msg = "📊Survey Results:\n"
for idx, q in enumerate(results):
msg += f"\nQ{idx+1}: {q['question']}\n"
if q['type'] == 'multiple_choice':
for opt, count in q['summary'].items():
msg += f" {opt}: {count}\n"
elif q['type'] == 'integer':
s = q['summary']
msg += f" Count: {s['count']}, Avg: {s['average']:.2f}, Min: {s['min']}, Max: {s['max']}\n"
elif q['type'] == 'text':
msg += f" Responses: {q['summary']['responses_count']}\n"
return msg
def get_survey_results(self, survey_name='example'):
if survey_name not in self.surveys:
return {"error": f"Survey '{survey_name}' not found."}
filename = os.path.join(self.response_dir, f'{survey_name}_responses.csv')
questions = self.surveys[survey_name]
results = []
try:
with open(filename, encoding='utf-8') as f:
reader = csv.reader(f)
lines = []
for row in reader:
if not row or len(row) < 4:
continue
# If location field is split due to comma, join columns 2 and 3
if row[2].startswith('[') and not row[2].endswith(']') and len(row) > 4:
location = row[2] + ',' + row[3]
answers = row[4:]
else:
location = row[2]
answers = row[3:]
lines.append(answers)
for q_idx, question in enumerate(questions):
qtype = question.get('type', 'multiple_choice')
answers = [row[q_idx] for row in lines if len(row) > q_idx]
summary = {}
if qtype == 'multiple_choice':
counts = Counter(answers)
summary = {chr(65+i): counts.get(chr(65+i), 0) for i in range(len(question.get('options', [])))}
elif qtype == 'integer':
ints = [int(a) for a in answers if a.isdigit()]
summary = {
"count": len(ints),
"average": sum(ints)/len(ints) if ints else 0,
"min": min(ints) if ints else None,
"max": max(ints) if ints else None
}
elif qtype == 'text':
summary = {"responses_count": len([a for a in answers if a.strip()])}
results.append({
"question": question['question'],
"type": qtype,
"summary": summary
})
return results
except FileNotFoundError:
return {"error": f"No responses recorded yet for '{survey_name}'."}
except Exception as e:
logger.error(f"Error summarizing survey results: {e}")
return NO_ALERTS
def answer(self, user_id, answer, location=None):
try:
"""Record an answer and return the next question or end message."""
+61 -35
View File
@@ -290,7 +290,7 @@ if voxDetectionEnabled:
from modules.radio import * # from the spudgunman/meshing-around repo
# File Monitor Configuration
if file_monitor_enabled or read_news_enabled or bee_enabled:
if file_monitor_enabled or read_news_enabled or bee_enabled or enable_runShellCmd or cmdShellSentryAlerts:
from modules.filemon import * # from the spudgunman/meshing-around repo
if read_news_enabled:
trap_list = trap_list + trap_list_filemon # items readnews
@@ -1505,6 +1505,14 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
logger.info(f"System: High Altitude {position_data['altitude']}m on Device: {rxNode} Channel: {channel} NodeID:{nodeID} Lat:{position_data.get('latitude', 0)} Lon:{position_data.get('longitude', 0)}")
altFeet = round(position_data['altitude'] * 3.28084, 2)
msg = f"🚀 High Altitude Detected! NodeID:{nodeID} Alt:{altFeet:,.0f}ft/{position_data['altitude']:,.0f}m"
# throttle sending alerts for the same node more than once every 30 minutes
last_alert_time = positionMetadata[nodeID].get('lastHighFlyAlert', 0)
current_time = time.time()
if current_time - last_alert_time < 1800:
return False # less than 30 minutes since last alert
positionMetadata[nodeID]['lastHighFlyAlert'] = current_time
if highfly_check_openskynetwork:
# check get_openskynetwork to see if the node is an aircraft
if 'latitude' in position_data and 'longitude' in position_data:
@@ -1522,6 +1530,7 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
if abs(node_alt - plane_alt) <= 900: # within 900m
msg += f"\n✈️Detected near:\n{flight_info}"
send_message(msg, highfly_channel, 0, highfly_interface)
# Keep the positionMetadata dictionary at a maximum size
if len(positionMetadata) > MAX_SEEN_NODES:
# Remove the oldest entry
@@ -2005,45 +2014,62 @@ handleSentinel_spotted = []
handleSentinel_loop = 0
async def handleSentinel(deviceID):
global handleSentinel_spotted, handleSentinel_loop
detectedNearby = ""
detectedNearby = None
resolution = "unknown"
closest_nodes = await get_closest_nodes(deviceID)
closest_node = closest_nodes[0]['id'] if closest_nodes != ERROR_FETCHING_DATA and closest_nodes else None
closest_distance = closest_nodes[0]['distance'] if closest_nodes != ERROR_FETCHING_DATA and closest_nodes else None
# check if the handleSentinel_spotted list contains the closest node already
if closest_node in [i['id'] for i in handleSentinel_spotted]:
# check if the distance is closer than the last time, if not just return
for i in range(len(handleSentinel_spotted)):
if handleSentinel_spotted[i]['id'] == closest_node and closest_distance is not None and closest_distance < handleSentinel_spotted[i]['distance']:
handleSentinel_spotted[i]['distance'] = closest_distance
break
else:
return
if closest_nodes != ERROR_FETCHING_DATA and closest_nodes:
if closest_nodes[0]['id'] is not None:
detectedNearby = get_name_from_number(closest_node, 'long', deviceID)
detectedNearby += ", " + get_name_from_number(closest_nodes[0]['id'], 'short', deviceID)
detectedNearby += ", " + str(closest_nodes[0]['id'])
detectedNearby += ", " + decimal_to_hex(closest_nodes[0]['id'])
detectedNearby += f" at {closest_distance}m"
closest_nodes = await get_closest_nodes(deviceID, returnCount=10)
#logger.debug(f"handleSentinel: closest_nodes={closest_nodes}")
if handleSentinel_loop >= sentry_holdoff and detectedNearby not in ["", None]:
if closest_nodes and positionMetadata and closest_nodes[0]['id'] in positionMetadata:
metadata = positionMetadata[closest_nodes[0]['id']]
if metadata.get('precisionBits') is not None:
resolution = metadata.get('precisionBits')
if not closest_nodes or closest_nodes == ERROR_FETCHING_DATA:
return
logger.warning(f"System: {detectedNearby} is close to your location on Interface{deviceID} Accuracy is {resolution}bits")
send_message(f"Sentry{deviceID}: {detectedNearby}", secure_channel, 0, secure_interface)
if enableSMTP and email_sentry_alerts:
for email in sysopEmails:
send_email(email, f"Sentry{deviceID}: {detectedNearby}")
handleSentinel_loop = 0
handleSentinel_spotted.append({'id': closest_node, 'distance': closest_distance})
else:
# Find any watched node inside or outside the zone
for node in closest_nodes:
node_id = node['id']
distance = node['distance']
if str(node_id) in sentryIgnoreList:
return
# Message conditions
if distance >= sentry_radius and str(node_id) and str(node_id) in sentryWatchList:
# Outside zone
detectedNearby = f"{get_name_from_number(node_id, 'long', deviceID)}, {get_name_from_number(node_id, 'short', deviceID)}, {node_id}, {decimal_to_hex(node_id)} at {distance}m (OUTSIDE ZONE)"
elif distance <= sentry_radius and str(node_id) not in sentryWatchList:
# Inside the zone
detectedNearby = f"{get_name_from_number(node_id, 'long', deviceID)}, {get_name_from_number(node_id, 'short', deviceID)}, {node_id}, {decimal_to_hex(node_id)} at {distance}m (INSIDE ZONE)"
#logger.debug(f"handleSentinel: loop={handleSentinel_loop}/{sentry_holdoff}, detectedNearby={detectedNearby} closest_nodes={closest_nodes}")
if detectedNearby:
handleSentinel_loop += 1
#logger.debug(f"handleSentinel: detectedNearby={detectedNearby}, loop={handleSentinel_loop}/{sentry_holdoff}")
if handleSentinel_loop >= sentry_holdoff:
# Get resolution if available
if positionMetadata and node_id in positionMetadata:
metadata = positionMetadata[node_id]
if metadata.get('precisionBits') is not None:
resolution = metadata.get('precisionBits')
# Send message alert
logger.warning(f"System: {detectedNearby} on Interface{deviceID} Accuracy is {resolution}bits")
send_message(f"Sentry{deviceID}: {detectedNearby}", secure_channel, 0, secure_interface)
# Send email alerts
if enableSMTP and email_sentry_alerts:
for email in sysopEmails:
send_email(email, f"Sentry{deviceID}: {detectedNearby}")
# Execute external script alerts
if cmdShellSentryAlerts and distance <= sentry_radius:
# inside zone
call_external_script('', script=sentryAlertNear)
logger.info(f"System: Sentry Script Alert {sentryAlertNear} for NodeID:{node_id} on Interface{deviceID}")
elif cmdShellSentryAlerts and distance >= sentry_radius:
# outside zone
call_external_script('', script=sentryAlertFar)
logger.info(f"System: Sentry Script Alert {sentryAlertFar} for NodeID:{node_id} on Interface{deviceID}")
handleSentinel_loop = 0 # Loop reset
else:
handleSentinel_loop = 0 # Reset if nothing detected
async def process_vox_queue():
# process the voxMsgQueue
+40 -39
View File
@@ -10,6 +10,7 @@ except ImportError:
import asyncio
import time # for sleep, get some when you can :)
from datetime import datetime
import random
from modules.log import *
from modules.system import *
@@ -92,10 +93,13 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
else:
msg = "🔊 Can you hear me now?"
if hop == "Direct":
msg = msg + f"SNR:{snr} RSSI:{rssi}"
else:
msg = msg + hop
# append SNR/RSSI or hop info
if hop.startswith("Direct?") and (snr != 0 or rssi != 0):
msg += f"? SNR:{snr} RSSI:{rssi}"
elif hop.startswith("Direct"):
msg += f"SNR:{snr} RSSI:{rssi}"
elif hop:
msg += f"{hop}"
if "@" in message:
msg = msg + " @" + message.split("@")[1]
@@ -150,7 +154,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
def handle_motd(message, message_from_id, isDM):
global MOTD
isAdmin = False
msg = ""
msg = MOTD
# check if the message_from_id is in the bbs_admin_list
if bbs_admin_list != ['']:
for admin in bbs_admin_list:
@@ -219,10 +223,12 @@ def onReceive(packet, interface):
rxNode = message_from_id = snr = rssi = hop = hop_away = channel_number = hop_start = hop_count = hop_limit = 0
pkiStatus = (False, 'ABC')
replyIDset = False
rxNodeHostName = None
emojiSeen = False
simulator_flag = False
isDM = False
channel_name = "unknown"
session_passkey = None
playingGame = False
if DEBUGpacket:
@@ -232,41 +238,32 @@ def onReceive(packet, interface):
# Debug print the packet for debugging
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
# set the value for the incomming interface
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
if port1 in rxInterface: rxNode = 1
elif multiple_interface and port2 in rxInterface: rxNode = 2
elif multiple_interface and port3 in rxInterface: rxNode = 3
elif multiple_interface and port4 in rxInterface: rxNode = 4
elif multiple_interface and port5 in rxInterface: rxNode = 5
elif multiple_interface and port6 in rxInterface: rxNode = 6
elif multiple_interface and port7 in rxInterface: rxNode = 7
elif multiple_interface and port8 in rxInterface: rxNode = 8
elif multiple_interface and port9 in rxInterface: rxNode = 9
# determine the rxNode based on the interface type
if rxType == 'TCPInterface':
rxHost = interface.__dict__.get('hostname', 'unknown')
if rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
rxNodeHostName = interface.__dict__.get('ip', None)
rxNode = next(
(i for i in range(1, 10)
if multiple_interface and rxHost and
globals().get(f'hostname{i}', '').split(':', 1)[0] in rxHost and
globals().get(f'interface{i}_type', '') == 'tcp'),None)
if rxType == 'SerialInterface':
rxInterface = interface.__dict__.get('devPath', 'unknown')
rxNode = next(
(i for i in range(1, 10)
if globals().get(f'port{i}', '') in rxInterface),None)
if rxType == 'BLEInterface':
if interface1_type == 'ble': rxNode = 1
elif multiple_interface and interface2_type == 'ble': rxNode = 2
elif multiple_interface and interface3_type == 'ble': rxNode = 3
elif multiple_interface and interface4_type == 'ble': rxNode = 4
elif multiple_interface and interface5_type == 'ble': rxNode = 5
elif multiple_interface and interface6_type == 'ble': rxNode = 6
elif multiple_interface and interface7_type == 'ble': rxNode = 7
elif multiple_interface and interface8_type == 'ble': rxNode = 8
elif multiple_interface and interface9_type == 'ble': rxNode = 9
rxNode = next(
(i for i in range(1, 10)
if globals().get(f'interface{i}_type', '') == 'ble'),0)
if rxNode is None:
# default to interface 1 ## FIXME needs better like a default interface setting or hash lookup
if 'decoded' in packet and packet['decoded']['portnum'] in ['ADMIN_APP', 'SIMULATOR_APP']:
session_passkey = packet.get('decoded', {}).get('admin', {}).get('sessionPasskey', None)
rxNode = 1
# check if the packet has a channel flag use it ## FIXME needs to be channel hash lookup
if packet.get('channel'):
@@ -351,8 +348,8 @@ def onReceive(packet, interface):
else:
hop_count = hop_away
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
hop = "Last Hop"
if hop == "" and hop_count > 0:
hop = f"{hop_count} Hop" if hop_count == 1 else f"{hop_count} Hops"
if hop_start == hop_limit and "lora" in str(transport_mechanism).lower():
hop = "Direct"
@@ -360,11 +357,15 @@ def onReceive(packet, interface):
if ((hop_start == 0 and hop_limit >= 0) or via_mqtt or ("mqtt" in str(transport_mechanism).lower())):
hop = "MQTT"
## FIXME should this be here?
if hop == "" and hop_count ==0 and (snr != 0 or rssi != 0):
hop = "Direct?"
if "unknown" in str(transport_mechanism).lower() and (snr == 0 and rssi == 0):
hop = "IP-Network"
if enableHopLogs:
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism}")
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism} Hostname:{rxNodeHostName}")
# check with stringSafeChecker if the message is safe
if stringSafeCheck(message_string) is False:
+12
View File
@@ -42,3 +42,15 @@ then
fi
fi
fi
# Get public and local IP addresses
public_ip=$(curl -s https://ifconfig.me 2>/dev/null)
public_ip=${public_ip:-""}
local_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
local_ip=${local_ip:-""}
if [ -n "$public_ip" ]; then
echo "Public IP: $public_ip"
fi
if [ -n "$local_ip" ]; then
echo "Local IP: $local_ip"
fi