forked from iarv/meshing-around
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8b2aefa28 | |||
| 849565cacb | |||
| fdec3a6754 | |||
| f3a97bc567 | |||
| 02625ad0f2 | |||
| b4ba4b0daf | |||
| fd7f8a94f5 | |||
| d252250edd | |||
| d6410e0461 | |||
| 050b4ab3ce | |||
| 8ac1a1eed7 | |||
| 370a417ce6 | |||
| 378b05df35 | |||
| d002c5ede8 | |||
| cd03cc56b4 | |||
| d4fd484706 | |||
| 82d519279e | |||
| 09302e8c91 | |||
| 91fc4605ec | |||
| abc6c07ee3 | |||
| bbf8b04bd3 | |||
| 5fd293c990 | |||
| a9a65a6c6d | |||
| 34e95c86d6 | |||
| 80891090c3 | |||
| 1b098fbf7b | |||
| 165d76cf8d | |||
| 045c9d433b | |||
| fbe5e008de | |||
| 004adc7d9a | |||
| 9a2033452f | |||
| 5638204f82 | |||
| e5c3b0cceb | |||
| 18ac53b230 | |||
| 4aa65dad6a | |||
| f65a7b7934 | |||
| 886293087a | |||
| e05e6f3451 | |||
| 4b5dd934e9 | |||
| 008ddfb5a2 | |||
| e1330b9b9e | |||
| 7b43213094 | |||
| b17c2b17ee | |||
| b6505ee577 | |||
| f7379b7ca5 | |||
| ec9a1d88db | |||
| a339570afe | |||
| f5af9f419a | |||
| ad5c1c90da | |||
| eb1e0c82ea | |||
| 8d2277bc59 | |||
| dc9908a72c | |||
| 21123d2993 | |||
| 7dc3134d0b | |||
| b125178492 | |||
| 2ad9e84c33 | |||
| 63bd288caa | |||
| 5c7d199831 | |||
| f56a39eeb6 | |||
| ae5991ee39 |
@@ -0,0 +1,11 @@
|
||||
---
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
|
||||

|
||||
|
||||
## 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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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}")
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import pickle # pip install pickle
|
||||
from modules.log import *
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
useSynchCompression = False
|
||||
|
||||
|
||||
+11
-5
@@ -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
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user