mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d055c35c96 | ||
|
|
27820daaf4 | ||
|
|
56e8e1c0d5 | ||
|
|
4545b8f4a4 | ||
|
|
6ed48d49ce | ||
|
|
a3a54b081d | ||
|
|
ab420af63e | ||
|
|
a55c61c47d | ||
|
|
7236f47eb7 | ||
|
|
05e11ae5f8 | ||
|
|
f8ffcc19b1 | ||
|
|
ea20eec604 | ||
|
|
d1204d2c26 | ||
|
|
654d8b3ff7 | ||
|
|
3bf12d62b5 | ||
|
|
0ec8613d27 | ||
|
|
10dd413ae7 | ||
|
|
09ac7525b3 | ||
|
|
aac497dfa0 | ||
|
|
6f652230b0 | ||
|
|
6f1c44e62a | ||
|
|
837d049acb | ||
|
|
2463407ade | ||
|
|
af2bc7be0c | ||
|
|
38654213e8 | ||
|
|
a06819dbda | ||
|
|
9818cccbbf | ||
|
|
239dbb8be0 | ||
|
|
872a9601d0 | ||
|
|
2b6dc726e1 | ||
|
|
ef27ddff84 | ||
|
|
8a8ad961d5 | ||
|
|
a8b4362d3c | ||
|
|
dc731ae237 | ||
|
|
d0d024d770 | ||
|
|
9b633502e6 | ||
|
|
ac1a007ba4 | ||
|
|
09cf6f585c | ||
|
|
916719f1c5 | ||
|
|
11a6dc3cf0 | ||
|
|
c160678e79 | ||
|
|
0c9fd919ab | ||
|
|
e17dc79896 | ||
|
|
06d6855d92 | ||
|
|
66f937a645 | ||
|
|
f4985b744a | ||
|
|
7ae6174f96 | ||
|
|
d44fdd4462 | ||
|
|
3dd6da4684 | ||
|
|
a229b57964 | ||
|
|
5e045b6447 | ||
|
|
1e328d4f4d | ||
|
|
879d141844 | ||
|
|
7daf8c4c33 | ||
|
|
3e6d1f5c6f | ||
|
|
32deea9e3b | ||
|
|
793fabcdb8 | ||
|
|
a7a710208a | ||
|
|
41efbc6189 | ||
|
|
f399190d3c | ||
|
|
5760c10534 | ||
|
|
9deb4a9436 | ||
|
|
1f348d963d | ||
|
|
b35edf13c8 | ||
|
|
37185b9f8b | ||
|
|
4e25535ede | ||
|
|
4de2a36099 | ||
|
|
6c0d6fd343 | ||
|
|
abd865c918 | ||
|
|
82222addbe | ||
|
|
7750ce468b | ||
|
|
135778d511 | ||
|
|
c54df673c3 | ||
|
|
2fec08060f | ||
|
|
ce9af3c0d3 | ||
|
|
217cd01d0a | ||
|
|
8a6057995b | ||
|
|
47e21dbaab | ||
|
|
267f50c591 | ||
|
|
0013a7bb74 | ||
|
|
73fe8be432 | ||
|
|
3d45195ae9 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,7 +8,8 @@ config.ini
|
||||
venv/
|
||||
|
||||
# logs
|
||||
logs/*.log
|
||||
logs/
|
||||
install_notes.txt
|
||||
|
||||
# modified .service files
|
||||
etc/*.service
|
||||
@@ -18,3 +19,6 @@ __pycache__/
|
||||
|
||||
# rag data
|
||||
data/rag/*
|
||||
|
||||
# qrz db
|
||||
data/qrz.db
|
||||
|
||||
36
README.md
36
README.md
@@ -44,6 +44,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
|
||||
### Fun and Games
|
||||
- **Built-in Games**: Enjoy games like DopeWars, Lemonade Stand, BlackJack, and VideoPoker.
|
||||
- **FCC ARRL QuizBot**: The exam question pool quiz-bot.
|
||||
- **Command-Based Gameplay**: Issue `games` to display help and start playing.
|
||||
|
||||
### Radio Frequency Monitoring
|
||||
@@ -53,6 +54,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
### EAS Alerts
|
||||
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
|
||||
- **NOAA EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from NOAA.
|
||||
- **USGS Volcano Alerts via API**: Use an internet-connected node to message Emergency Alerts from USGS.
|
||||
- **EAS Alerts over the air**: Utilizing external tools to report EAS alerts offline over mesh.
|
||||
- **NINA alerts for Germany**: Emergency Alerts from xrepository.de feed
|
||||
|
||||
@@ -132,6 +134,8 @@ The following settings determine how the bot responds. By default, the bot will
|
||||
respond_by_dm_only = True
|
||||
defaultChannel = 0
|
||||
ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
ignoreChannels = # ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
|
||||
cmdBang = False # require ! to be the first character in a command
|
||||
```
|
||||
|
||||
### Location Settings
|
||||
@@ -213,7 +217,8 @@ This uses USA: SAME, FIPS, ZIP code to locate the alerts in the feed. By default
|
||||
```ini
|
||||
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
|
||||
eAlertBroadcastCh = 2,3 # Goverment Emergency IPAWS/CAP Alert Broadcast Channels
|
||||
ignoreFEMAtest = True # Ignore any headline that includes the word Test
|
||||
ignoreFEMAenable = True # Ignore any headline that includes followig word list
|
||||
ignoreFEMAwords = test,exercise
|
||||
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
|
||||
# find your SAME https://www.weather.gov/nwr/counties
|
||||
mySAME = 053029,053073
|
||||
@@ -231,6 +236,8 @@ enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
|
||||
wxAlertBroadcastEnabled = True
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2,4
|
||||
ignoreEASenable = True # Ignore any headline that includes followig word list
|
||||
ignoreEASwords = test,advisory
|
||||
```
|
||||
|
||||
### Repeater Settings
|
||||
@@ -329,9 +336,20 @@ In the config.ini enable the module
|
||||
```ini
|
||||
[scheduler]
|
||||
# enable or disable the scheduler module
|
||||
enabled = True
|
||||
enabled = False
|
||||
# interface to send the message to
|
||||
interface = 1
|
||||
# channel to send the message to
|
||||
channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
value =
|
||||
# interval to use when time is not set (e.g. every 2 days)
|
||||
interval =
|
||||
# time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
time =
|
||||
```
|
||||
The actions are via code only at this time. See mesh_bot.py around line [1097](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1097) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
|
||||
The basic brodcast message can be setup in condig.ini. For advanced, See mesh_bot.py around the bottom of file, line [1491](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1491) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
|
||||
|
||||
```python
|
||||
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
@@ -382,7 +400,8 @@ There is no direct support for MQTT in the code, however, reports from Discord a
|
||||
| `riverflow` | Return information from NOAA for river flow info. Example: `riverflow modules/settings.py`| |
|
||||
| `solar` | Gives an idea of the x-ray flux | |
|
||||
| `sun` and `moon` | Return info on rise and set local time | ✅ |
|
||||
| `tide` | Returns the local tides (NOAA data source) |
|
||||
| `tide` | Returns the local tides (NOAA data source) | |
|
||||
| `valert` | Returns USGS Volcano Data | |
|
||||
| `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting | |
|
||||
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | |
|
||||
|
||||
@@ -424,6 +443,8 @@ There is no direct support for MQTT in the code, however, reports from Discord a
|
||||
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
|
||||
| `dopewars` | Plays the classic drug trader game | ✅ |
|
||||
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
|
||||
| `hamtest` | FCC/ARRL Quiz `hamtest general` or `hamtest extra` and `score` | ✅ |
|
||||
| `hangman` | Plays the classic word guess game | ✅ |
|
||||
| `joke` | Tells a joke | ✅ |
|
||||
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
|
||||
| `mastermind` | Plays the classic code-breaking game | ✅ |
|
||||
@@ -445,6 +466,7 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
- [Video Poker Terminal Game](https://github.com/devtronvarma/Video-Poker-Terminal-Game)
|
||||
- [Python Mastermind](https://github.com/pwdkramer/pythonMastermind/)
|
||||
- [Golf](https://github.com/danfriedman30/pythongame)
|
||||
- ARRL Question Pool Data from https://github.com/russolsen/ham_radio_question_pool
|
||||
|
||||
### Special Thanks
|
||||
- **xdep**: For the reporting tools.
|
||||
@@ -453,9 +475,11 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
- **[https://github.com/A-c0rN](A-c0rN)**: Assistance with iPAWS and EAS
|
||||
- **Mike O'Connell/skrrt**: For [eas_alert_parser](etc/eas_alert_parser.py) enhanced by **sheer.cold**
|
||||
- **PiDiBi**: For looking at test functions and other suggestions like wxc, CPU use, and alerting ideas.
|
||||
- **WH6GXZ nurse dude**: For bashing on installer
|
||||
- **WH6GXZ nurse dude**: For bashing on installer, Volcano Alerts 🌋
|
||||
- **Josh**: For more bashing on installer!
|
||||
- **Cisien, bitflip, **Woof**, **propstg**, **Josh** and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **dj505**: trying it on windows!
|
||||
- **mikecarper**: ideas, and testing. hamtest
|
||||
- **Cisien, bitflip, **Woof**, **propstg**, **trs2982**, **Josh** and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
|
||||
|
||||
### Tools
|
||||
|
||||
@@ -32,6 +32,10 @@ autoPingInChannel = False
|
||||
defaultChannel = 0
|
||||
# ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
ignoreDefaultChannel = False
|
||||
# ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
|
||||
ignoreChannels =
|
||||
# require ! to be the first character in a command
|
||||
cmdBang = False
|
||||
|
||||
# motd is reset to this value on boot
|
||||
motd = Thanks for using MeshBOT! Have a good day!
|
||||
@@ -123,8 +127,8 @@ useMetric = False
|
||||
# repeaterList lookup location (rbook / artsci)
|
||||
repeaterLookup = rbook
|
||||
|
||||
# NOAA weather forecast days, the first two rows are today and tonight
|
||||
NOAAforecastDuration = 4
|
||||
# NOAA weather forecast days
|
||||
NOAAforecastDuration = 3
|
||||
# number of weather alerts to display
|
||||
NOAAalertCount = 2
|
||||
|
||||
@@ -136,6 +140,9 @@ riverListDefault =
|
||||
|
||||
# NOAA EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
# Enable Ignore any message that includes following word list
|
||||
ignoreEASenable = False
|
||||
ignoreEASwords = test,advisory
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2
|
||||
# Add extra location to the weather alert
|
||||
@@ -147,12 +154,20 @@ eAlertBroadcastEnabled = False
|
||||
eAlertBroadcastCh = 2
|
||||
|
||||
# FEMA Alert Broadcast Settings
|
||||
# Ignore any headline that includes the word Test
|
||||
ignoreFEMAtest = True
|
||||
# Enable Ignore any headline that includes following word list
|
||||
ignoreFEMAenable = True
|
||||
ignoreFEMAwords = test,exercise
|
||||
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
|
||||
# find your SAME https://www.weather.gov/nwr/counties
|
||||
mySAME = 053029,053073
|
||||
|
||||
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
|
||||
volcanoAlertBroadcastEnabled = False
|
||||
volcanoAlertBroadcastCh = 2
|
||||
# Enable Ignore any message that includes following word list
|
||||
ignoreUSGSEnable = False
|
||||
ignoreUSGSWords = test,advisory
|
||||
|
||||
# Use DE Alert Broadcast Data
|
||||
enableDEalerts = False
|
||||
# comma separated list of regional codes trigger local alert.
|
||||
@@ -191,6 +206,17 @@ repeater_channels =
|
||||
[scheduler]
|
||||
# enable or disable the scheduler module
|
||||
enabled = False
|
||||
# interface to send the message to
|
||||
interface = 1
|
||||
# channel to send the message to
|
||||
channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
value =
|
||||
# interval to use when time is not set (e.g. every 2 days)
|
||||
interval =
|
||||
# time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
time =
|
||||
|
||||
[radioMon]
|
||||
# using Hamlib rig control will monitor and alert on channel use
|
||||
@@ -253,6 +279,8 @@ blackjack = True
|
||||
videopoker = True
|
||||
mastermind = True
|
||||
golfsim = True
|
||||
hangman = True
|
||||
hamtest = True
|
||||
|
||||
[messagingSettings]
|
||||
# delay in seconds for response to avoid message collision
|
||||
@@ -264,6 +292,8 @@ MESSAGE_CHUNK_SIZE = 160
|
||||
# Request Acknowledgement of message OTA
|
||||
wantAck = False
|
||||
# Max limit buffer for radio testing. 233 is hard limit 2.5+ firmware
|
||||
maxBuffer = 220
|
||||
maxBuffer = 200
|
||||
#Enable Extra logging of Hop count data
|
||||
enableHopLogs = False
|
||||
|
||||
|
||||
|
||||
7226
data/hamradio/extra.json
Normal file
7226
data/hamradio/extra.json
Normal file
File diff suppressed because it is too large
Load Diff
5126
data/hamradio/general.json
Normal file
5126
data/hamradio/general.json
Normal file
File diff suppressed because it is too large
Load Diff
4934
data/hamradio/technician.json
Normal file
4934
data/hamradio/technician.json
Normal file
File diff suppressed because it is too large
Load Diff
30
install.sh
30
install.sh
@@ -4,6 +4,7 @@
|
||||
# install.sh
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
chronjob="0 1 * * * /usr/bin/python3 $program_path/etc/report_generator5.py"
|
||||
printf "\n########################"
|
||||
printf "\nMeshing Around Installer\n"
|
||||
printf "########################\n"
|
||||
@@ -193,17 +194,19 @@ if [[ $(echo "${meshbotservice}" | grep -i "^y") ]] || [[ $(echo "${embedded}" |
|
||||
sudo usermod -a -G meshbot meshbot
|
||||
whoami="meshbot"
|
||||
echo "Added user meshbot with no home directory"
|
||||
sudo usermod -a -G dialout $whoami
|
||||
sudo usermod -a -G tty $whoami
|
||||
sudo usermod -a -G bluetooth $whoami
|
||||
echo "Added meshbot to dialout, tty, and bluetooth groups"
|
||||
|
||||
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"
|
||||
else
|
||||
whoami=$(whoami)
|
||||
fi
|
||||
# set basic permissions for the bot user
|
||||
sudo usermod -a -G dialout $whoami
|
||||
sudo usermod -a -G tty $whoami
|
||||
sudo usermod -a -G bluetooth $whoami
|
||||
echo "Added user $whoami to dialout, tty, and bluetooth groups"
|
||||
|
||||
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"
|
||||
|
||||
# set the correct user in the service file
|
||||
replace="s|User=pi|User=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
@@ -276,6 +279,8 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
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 "Reporting chron job added to run report_generator5.py\n" >> install_notes.txt
|
||||
printf "chronjob: %s\n" "$chronjob" >> 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
|
||||
@@ -305,6 +310,14 @@ else
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable $service.service
|
||||
sudo systemctl start $service.service
|
||||
# check if the cron job already exists
|
||||
if ! crontab -l | grep -q "$chronjob"; then
|
||||
# add the cron job to run the report_generator5.py script
|
||||
(crontab -l 2>/dev/null; echo "$chronjob") | crontab -
|
||||
printf "\nAdded cron job to run report_generator5.py\n"
|
||||
else
|
||||
printf "\nCron job already exists, skipping\n"
|
||||
fi
|
||||
printf "Reference following commands:\n\n" "$service" > install_notes.txt
|
||||
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
|
||||
@@ -328,7 +341,6 @@ exit 0
|
||||
# sudo systemctl stop mesh_bot_reporting
|
||||
# sudo systemctl disable mesh_bot_reporting
|
||||
# sudo rm /etc/systemd/system/mesh_bot.service
|
||||
# sudo rm /etc/systemd/system/mesh_bot_reporting.service
|
||||
# sudo rm /etc/systemd/system/mesh_bot_w3.service
|
||||
# sudo rm /etc/systemd/system/pong_bot.service
|
||||
# sudo systemctl daemon-reload
|
||||
|
||||
@@ -24,4 +24,18 @@ log_backup_count = 32
|
||||
## Web Reporting WebServer
|
||||
There is a web-server module. You can run `python3 modules/web.py` from the project root directory and it will serve up the web content.
|
||||
|
||||
find it at. http://localhost:8420
|
||||
find it at. http://localhost:8420
|
||||
|
||||
If you have linux-native running and errors such as..
|
||||
```bash
|
||||
File "/usr/lib/python3.11/http/server.py", line 136, in server_bind
|
||||
socketserver.TCPServer.server_bind(self)
|
||||
File "/usr/lib/python3.11/socketserver.py", line 472, in server_bind
|
||||
self.socket.bind(self.server_address)
|
||||
```
|
||||
modify the modules/web.py to use a real IP address, meshtasticD-native is binding to 127.0.0.1
|
||||
|
||||
```python
|
||||
# Set the desired IP address
|
||||
server_ip = '127.0.0.1'
|
||||
```
|
||||
|
||||
188
mesh_bot.py
188
mesh_bot.py
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/python3
|
||||
# Meshtastic Autoresponder MESH Bot
|
||||
# K7MHI Kelly Keeton 2024
|
||||
# K7MHI Kelly Keeton 2025
|
||||
|
||||
try:
|
||||
from pubsub import pub
|
||||
@@ -15,12 +15,11 @@ from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
# list of commands to remove from the default list for DM only
|
||||
restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind"]
|
||||
restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind", "hangman", "hamtest"]
|
||||
restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
|
||||
|
||||
# Global Variables
|
||||
DEBUGpacket = False # Debug print the packet rx
|
||||
DEBUGhops = False # Debug print hop info and bad hop count packets
|
||||
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
global cmdHistory
|
||||
@@ -57,6 +56,8 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"games": lambda: gamesCmdList,
|
||||
"globalthermonuclearwar": lambda: handle_gTnW(),
|
||||
"golfsim": lambda: handleGolf(message, message_from_id, deviceID),
|
||||
"hamtest": lambda: handleHamtest(message, message_from_id, deviceID),
|
||||
"hangman": lambda: handleHangman(message, message_from_id, deviceID),
|
||||
"hfcond": hf_band_conditions,
|
||||
"history": lambda: handle_history(message, message_from_id, deviceID, isDM),
|
||||
"joke": lambda: tell_joke(message_from_id),
|
||||
@@ -83,6 +84,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"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),
|
||||
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
|
||||
"valert": lambda: get_volcano_usgs(),
|
||||
"videopoker": lambda: handleVideoPoker(message, message_from_id, deviceID),
|
||||
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
|
||||
"whoami": lambda: handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus),
|
||||
@@ -113,6 +115,10 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
# check the message for commands words list, processed after system.messageTrap
|
||||
for key in command_handler:
|
||||
word = message_lower.split(' ')
|
||||
if cmdBang:
|
||||
# strip the !
|
||||
if word[0].startswith("!"):
|
||||
word[0] = word[0][1:]
|
||||
if key in word:
|
||||
# append all the commands found in the message to the cmds list
|
||||
cmds.append({'cmd': key, 'index': message_lower.index(key)})
|
||||
@@ -658,6 +664,69 @@ def handleGolf(message, nodeID, deviceID):
|
||||
time.sleep(responseDelay + 1)
|
||||
return msg
|
||||
|
||||
def handleHangman(message, nodeID, deviceID):
|
||||
global hangmanTracker
|
||||
index = 0
|
||||
msg = ''
|
||||
for i in range(len(hangmanTracker)):
|
||||
if hangmanTracker[i]['nodeID'] == nodeID:
|
||||
hangmanTracker[i]["last_played"] = time.time()
|
||||
index = i+1
|
||||
break
|
||||
|
||||
if index and "end" in message.lower():
|
||||
hangman.end(nodeID)
|
||||
hangmanTracker.pop(index-1)
|
||||
return "Thanks for hanging out🤙"
|
||||
|
||||
if not index:
|
||||
hangmanTracker.append(
|
||||
{
|
||||
"nodeID": nodeID,
|
||||
"last_played": time.time()
|
||||
}
|
||||
)
|
||||
msg = "🧩Hangman🤖 'end' to cut rope🪢\n"
|
||||
msg += hangman.play(nodeID, message)
|
||||
|
||||
time.sleep(responseDelay + 1)
|
||||
return msg
|
||||
|
||||
def handleHamtest(message, nodeID, deviceID):
|
||||
global hamtestTracker
|
||||
index = 0
|
||||
msg = ''
|
||||
response = message.split(' ')
|
||||
for i in range(len(hamtestTracker)):
|
||||
if hamtestTracker[i]['nodeID'] == nodeID:
|
||||
hamtestTracker[i]["last_played"] = time.time()
|
||||
index = i+1
|
||||
break
|
||||
|
||||
if not index:
|
||||
hamtestTracker.append({"nodeID": nodeID,"last_played": time.time()})
|
||||
|
||||
if "end" in response[0].lower():
|
||||
msg = hamtest.endGame(nodeID)
|
||||
elif "score" in response[0].lower():
|
||||
msg = hamtest.getScore(nodeID)
|
||||
|
||||
if "hamtest" in response[0].lower():
|
||||
if len(response) > 1:
|
||||
if "gen" in response[1].lower():
|
||||
msg = hamtest.newGame(nodeID, 'general')
|
||||
elif "ex" in response[1].lower():
|
||||
msg = hamtest.newGame(nodeID, 'extra')
|
||||
else:
|
||||
msg = hamtest.newGame(nodeID, 'technician')
|
||||
|
||||
# if the message is an answer A B C or D upper or lower case
|
||||
if response[0].upper() in ['A', 'B', 'C', 'D']:
|
||||
msg = hamtest.answer(nodeID, response[0])
|
||||
|
||||
time.sleep(responseDelay + 1)
|
||||
return msg
|
||||
|
||||
def handle_riverFlow(message, message_from_id, deviceID):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
userRiver = message.lower()
|
||||
@@ -786,8 +855,14 @@ def sysinfo(message, message_from_id, deviceID):
|
||||
return "sysinfo command returns system information."
|
||||
else:
|
||||
if enable_runShellCmd and file_monitor_enabled:
|
||||
shellData = call_external_script(None, "script/sysEnv.sh").rstrip()
|
||||
return get_sysinfo(message_from_id, deviceID) + "\n" + shellData
|
||||
# get the system information from the shell script
|
||||
# this is an example of how to run a shell script and return the data
|
||||
shellData = call_external_script(None, "script/sysEnv.sh")
|
||||
# check if the script returned data
|
||||
if shellData == "" or shellData == None:
|
||||
# no data returned from the script
|
||||
shellData = "shell script data missing"
|
||||
return get_sysinfo(message_from_id, deviceID) + "\n" + shellData.rstrip()
|
||||
else:
|
||||
return get_sysinfo(message_from_id, deviceID)
|
||||
|
||||
@@ -828,7 +903,7 @@ def handle_history(message, nodeid, deviceID, isDM, lheard=False):
|
||||
prettyTime = getPrettyTime(cmdTime)
|
||||
|
||||
# history display output
|
||||
if nodeid in bbs_admin_list and cmdHistory[i]['nodeID'] not in lheardCmdIgnoreNode:
|
||||
if str(nodeid) in bbs_admin_list and cmdHistory[i]['nodeID'] not in lheardCmdIgnoreNode:
|
||||
buffer.append((get_name_from_number(cmdHistory[i]['nodeID'], 'short', deviceID), cmdHistory[i]['cmd'], prettyTime))
|
||||
elif cmdHistory[i]['nodeID'] == nodeid and cmdHistory[i]['nodeID'] not in lheardCmdIgnoreNode:
|
||||
buffer.append((get_name_from_number(nodeid, 'short', deviceID), cmdHistory[i]['cmd'], prettyTime))
|
||||
@@ -982,6 +1057,8 @@ def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
|
||||
(jackTracker, "BlackJack", handleBlackJack) if 'jackTracker' in globals() else None,
|
||||
(mindTracker, "MasterMind", handleMmind) if 'mindTracker' in globals() else None,
|
||||
(golfTracker, "GolfSim", handleGolf) if 'golfTracker' in globals() else None,
|
||||
(hangmanTracker, "Hangman", handleHangman) if 'hangmanTracker' in globals() else None,
|
||||
(hamtestTracker, "HamTest", handleHamtest) if 'hamtestTracker' in globals() else None,
|
||||
]
|
||||
trackers = [tracker for tracker in trackers if tracker is not None]
|
||||
|
||||
@@ -1006,6 +1083,7 @@ def onReceive(packet, interface):
|
||||
replyIDset = False
|
||||
emojiSeen = False
|
||||
isDM = False
|
||||
playingGame = False
|
||||
|
||||
if DEBUGpacket:
|
||||
# Debug print the interface object
|
||||
@@ -1115,7 +1193,7 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
hop_start = 0
|
||||
|
||||
if DEBUGhops:
|
||||
if enableHopLogs:
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start}")
|
||||
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
|
||||
logger.debug(f"System: Packet HopDebugger: No hop count found in PACKET {packet} END PACKET")
|
||||
@@ -1198,11 +1276,17 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
# message is on a channel
|
||||
if messageTrap(message_string):
|
||||
# message is for us to respond to
|
||||
# message is for us to respond to, or is it...
|
||||
if ignoreDefaultChannel and channel_number == publicChannel:
|
||||
logger.debug(f"System: ignoreDefaultChannel CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Default Channel:{channel_number}")
|
||||
elif str(message_from_id) in bbs_ban_list:
|
||||
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Cantankerous Node")
|
||||
elif str(channel_number) in ignoreChannels:
|
||||
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Ignored Channel:{channel_number}")
|
||||
elif cmdBang and not message_string.startswith("!"):
|
||||
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Didnt sound like they meant it")
|
||||
else:
|
||||
# message is for bot to respond to
|
||||
# message is for bot to respond to, seriously this time..
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "ReceivedChannel: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
if useDMForResponse:
|
||||
@@ -1256,14 +1340,18 @@ def onReceive(packet, interface):
|
||||
# if QRZ enabled check if we have said hello
|
||||
if qrz_hello_enabled:
|
||||
if never_seen_before(message_from_id):
|
||||
name = {get_name_from_number(message_from_id, 'short', rxNode)}
|
||||
# add to qrz_hello list
|
||||
hello(message_from_id, name)
|
||||
# send a hello message as a DM
|
||||
if not train_qrz:
|
||||
time.sleep(responseDelay)
|
||||
send_message(f"Hello {name} {qrz_hello_string}", channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
name = get_name_from_number(message_from_id, 'short', rxNode)
|
||||
if isinstance(name, str) and name.startswith("!") and len(name) == 9:
|
||||
# we didnt get a info packet yet so wait and ingore this go around
|
||||
logger.debug(f"System: QRZ Hello ignored, no info packet yet")
|
||||
else:
|
||||
# add to qrz_hello list
|
||||
hello(message_from_id, name)
|
||||
# send a hello message as a DM
|
||||
if not train_qrz:
|
||||
time.sleep(responseDelay)
|
||||
send_message(f"Hello {name} {qrz_hello_string}", channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode)
|
||||
@@ -1286,8 +1374,9 @@ async def start_rx():
|
||||
|
||||
if llm_enabled:
|
||||
logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait")
|
||||
llm_query(" ")
|
||||
logger.debug(f"System: LLM model {llmModel} loaded")
|
||||
llmLoad = llm_query(" ")
|
||||
if "trouble" not in llmLoad:
|
||||
logger.debug(f"System: LLM Model {llmModel} loaded")
|
||||
|
||||
if log_messages_to_file:
|
||||
logger.debug("System: Logging Messages to disk")
|
||||
@@ -1339,29 +1428,79 @@ async def start_rx():
|
||||
logger.debug(f"System: Emergency Alert Broadcast Enabled on channels {emergencyAlertBroadcastCh}")
|
||||
if emergency_responder_enabled:
|
||||
logger.debug(f"System: Emergency Responder Enabled on channels {emergency_responder_alert_channel} for interface {emergency_responder_alert_interface}")
|
||||
if volcanoAlertBroadcastEnabled:
|
||||
logger.debug(f"System: Volcano Alert Broadcast Enabled on channels {volcanoAlertBroadcastChannel}")
|
||||
if qrz_hello_enabled and train_qrz:
|
||||
logger.debug(f"System: QRZ Welcome/Hello Enabled with training mode")
|
||||
if qrz_hello_enabled and not train_qrz:
|
||||
logger.debug(f"System: QRZ Welcome/Hello Enabled")
|
||||
if checklist_enabled:
|
||||
logger.debug(f"System: CheckList Module Enabled")
|
||||
if ignoreChannels != []:
|
||||
logger.debug(f"System: Ignoring Channels: {ignoreChannels}")
|
||||
if enableSMTP:
|
||||
if enableImap:
|
||||
logger.debug(f"System: SMTP Email Alerting Enabled using IMAP")
|
||||
else:
|
||||
logger.debug(f"System: SMTP Email Alerting Enabled")
|
||||
if scheduler_enabled:
|
||||
# Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
|
||||
# Reminder Scheduler is enabled every Monday at noon send a log message
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Reminder"))
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Enabled Reminder"))
|
||||
|
||||
# basic scheduler
|
||||
if schedulerValue != '':
|
||||
logger.debug(f"System: Starting the broadcast scheduler from config.ini")
|
||||
if schedulerValue.lower() == 'day':
|
||||
if schedulerTime != '':
|
||||
# Send a message every day at the time set in schedulerTime
|
||||
schedule.every().day.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
else:
|
||||
# Send a message every day at the time set in schedulerInterval
|
||||
schedule.every(int(schedulerInterval)).days.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'mon' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Monday at the time set in schedulerTime
|
||||
schedule.every().monday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'tue' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Tuesday at the time set in schedulerTime
|
||||
schedule.every().tuesday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'wed' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Wednesday at the time set in schedulerTime
|
||||
schedule.every().wednesday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'thu' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Thursday at the time set in schedulerTime
|
||||
schedule.every().thursday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'fri' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Friday at the time set in schedulerTime
|
||||
schedule.every().friday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sat' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Saturday at the time set in schedulerTime
|
||||
schedule.every().saturday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sun' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Sunday at the time set in schedulerTime
|
||||
schedule.every().sunday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'hour' in schedulerValue.lower():
|
||||
# Send a message every hour at the time set in schedulerTime
|
||||
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'min' in schedulerValue.lower():
|
||||
# Send a message every minute at the time set in schedulerTime
|
||||
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
else:
|
||||
logger.debug(f"System: Starting the broadcast scheduler")
|
||||
|
||||
# Enhanced Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
|
||||
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
|
||||
|
||||
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
|
||||
|
||||
# Send Weather Channel Notice Wed. Noon on channel 2, device 1
|
||||
#schedule.every().wednesday.at("12:00").do(lambda: send_message("Weather alerts available on 'Alerts' channel with default 'AQ==' key.", 2, 0, 1))
|
||||
|
||||
# Send config URL for Medium Fast Network Use every other day at 10:00 to default channel 2 on device 1
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("Join us on Medium Fast https://meshtastic.org/e/#CgcSAQE6AggNEg4IARAEOAFAA0gBUB5oAQ", 2, 0, 1))
|
||||
|
||||
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
@@ -1383,7 +1522,6 @@ async def start_rx():
|
||||
|
||||
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 3 on device 1
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 3, 0, 1))
|
||||
logger.debug("System: Starting the broadcast scheduler")
|
||||
await BroadcastScheduler()
|
||||
|
||||
# here we go loopty loo
|
||||
|
||||
@@ -39,4 +39,16 @@ To help with code testing see `etc/simulator.py` to simulate a bot. I also enjoy
|
||||
```
|
||||
|
||||
5. **Test the New Command**:
|
||||
Run MeshBot and test the new command by sending a message with the command `newcommand` to ensure it responds correctly.
|
||||
Run MeshBot and test the new command by sending a message with the command `newcommand` to ensure it responds correctly.
|
||||
|
||||
|
||||
### Running a Shell command
|
||||
|
||||
Using the above example and enabling the filemon module, you can make a command which calls a bash file to do things on the system.
|
||||
|
||||
```python
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
#...
|
||||
"switchON": lambda: call_external_script(message)
|
||||
```
|
||||
This would call the default script located in script/runShell.sh and return its output.
|
||||
@@ -167,7 +167,7 @@ def bbs_sync_posts(input, peerNode, RxNode):
|
||||
messageID = 0
|
||||
|
||||
# check if the bbs link is enabled
|
||||
if bbs_link_whitelist is not None:
|
||||
if bbs_link_whitelist != ['']:
|
||||
if str(peerNode) not in bbs_link_whitelist:
|
||||
logger.warning(f"System: BBS Link is disabled for node {peerNode}.")
|
||||
return "System: BBS Link is disabled for your node."
|
||||
@@ -185,11 +185,17 @@ def bbs_sync_posts(input, peerNode, RxNode):
|
||||
return f"bbsack {messageID}"
|
||||
elif "bbsack" in input.lower():
|
||||
# increment the messageID
|
||||
ack = int(input.split(" ")[1])
|
||||
messageID = int(ack) + 1
|
||||
if len(input.split(" ")) > 1:
|
||||
try:
|
||||
messageID = int(input.split(" ")[1]) + 1
|
||||
except:
|
||||
return "link error"
|
||||
else:
|
||||
return "link error"
|
||||
|
||||
# send message with delay to keep chutil happy
|
||||
if messageID < len(bbs_messages):
|
||||
logger.debug(f"System: Sending bbslink message {messageID} to peer " + str(peerNode))
|
||||
time.sleep(5 + responseDelay)
|
||||
# every 5 messages add extra delay
|
||||
if messageID % 5 == 0:
|
||||
|
||||
@@ -17,12 +17,12 @@ def read_file(file_monitor_file_path, random_line_only=False):
|
||||
return "🐝buzz 💐buzz buzz🍯"
|
||||
if random_line_only:
|
||||
# read a random line from the file
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
with open(file_monitor_file_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
return random.choice(lines)
|
||||
else:
|
||||
# read the whole file
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
with open(file_monitor_file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
return content
|
||||
except Exception as e:
|
||||
@@ -37,7 +37,7 @@ def read_news():
|
||||
def write_news(content, append=False):
|
||||
# write the news file on demand
|
||||
try:
|
||||
with open(news_file_path, 'a' if append else 'w') as f:
|
||||
with open(news_file_path, 'a' if append else 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
logger.info(f"FileMon: Updated {news_file_path}")
|
||||
return True
|
||||
@@ -76,7 +76,7 @@ def call_external_script(message, script="script/runShell.sh"):
|
||||
logger.warning(f"FileMon: Script not found: {script_path}")
|
||||
return "sorry I can't do that"
|
||||
|
||||
output = os.popen(f"bash {script_path} {message}").read()
|
||||
output = os.popen(f"bash {script_path} {message}").read().encode('utf-8').decode('utf-8')
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error calling external script: {e}")
|
||||
|
||||
142
modules/games/hamtest.py
Normal file
142
modules/games/hamtest.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# hamradio test module for meshbot DE K7MHI 2025
|
||||
# depends on the JSON question data files from https://github.com/russolsen/ham_radio_question_pool
|
||||
|
||||
# data files which are expected to be in ../../data/hamradio/ similar to the following:
|
||||
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/technician-2022-2026/technician.json
|
||||
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/general-2023-2027/general.json
|
||||
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/extra-2024-2028/extra.json
|
||||
|
||||
import json
|
||||
import random
|
||||
import os
|
||||
from modules.log import *
|
||||
|
||||
class HamTest:
|
||||
def __init__(self):
|
||||
self.questions = {}
|
||||
self.load_questions()
|
||||
self.game = {}
|
||||
|
||||
def load_questions(self):
|
||||
for level in ['technician', 'general', 'extra']:
|
||||
try:
|
||||
with open(f'{os.path.dirname(__file__)}/../../data/hamradio/{level}.json', encoding='utf-8') as f:
|
||||
self.questions[level] = json.load(f)
|
||||
except FileNotFoundError:
|
||||
logger.error(f"File not found: ../../data/hamradio/{level}.json")
|
||||
self.questions[level] = []
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Error decoding JSON from file: ../../data/hamradio/{level}.json")
|
||||
self.questions[level] = []
|
||||
|
||||
def newGame(self, id, level='technician'):
|
||||
msg = f"📻New {level} quiz started, 'end' to exit."
|
||||
if id in self.game:
|
||||
level = self.game[id]['level']
|
||||
self.game[id] = {
|
||||
'level': level,
|
||||
'score': 0,
|
||||
'total': 0,
|
||||
'errors': [],
|
||||
'qId': None,
|
||||
'question': None,
|
||||
'answers': None,
|
||||
'correct': None
|
||||
}
|
||||
# set the pool needed for the game
|
||||
if self.game[id]['level'] == 'extra':
|
||||
self.game[id]['total'] = 50
|
||||
else:
|
||||
self.game[id]['total'] = 35
|
||||
|
||||
# randomize the questions
|
||||
random.shuffle(self.questions[level])
|
||||
|
||||
msg += f"\n{self.nextQuestion(id)}"
|
||||
return msg
|
||||
|
||||
def nextQuestion(self, id):
|
||||
level = self.game[id]['level']
|
||||
# if question has the word figure in it, skip it
|
||||
question = random.choice(self.questions[level])
|
||||
while 'figure' in question['question'].lower():
|
||||
question = random.choice(self.questions[level])
|
||||
|
||||
self.game[id]['question'] = question['question']
|
||||
self.game[id]['answers'] = question['answers']
|
||||
self.game[id]['correct'] = question['correct']
|
||||
self.game[id]['qId'] = question['id']
|
||||
self.game[id]['total'] -= 1
|
||||
|
||||
if self.game[id]['total'] == 0:
|
||||
return self.endGame(id)
|
||||
|
||||
# ask the question and return answers in A, B, C, D format
|
||||
msg = f"{self.game[id]['question']}\n"
|
||||
for i, answer in enumerate(self.game[id]['answers']):
|
||||
msg += f"{chr(65+i)}. {answer}\n"
|
||||
return msg
|
||||
|
||||
def answer(self, id, answer):
|
||||
if id not in self.game:
|
||||
return "No game in progress"
|
||||
if self.game[id]['correct'] == ord(answer.upper()) - 65:
|
||||
self.game[id]['score'] += 1
|
||||
return f"Correct👍\n" + self.nextQuestion(id)
|
||||
else:
|
||||
# record the section of the question for study aid
|
||||
section = self.game[id]['qId'][:3]
|
||||
self.game[id]['errors'].append(section)
|
||||
# provide the correct answer
|
||||
answer = [self.game[id]['correct']]
|
||||
return f"Wrong.⛔️ Correct is {chr(65+self.game[id]['correct'])}\n" + self.nextQuestion(id)
|
||||
|
||||
def getScore(self, id):
|
||||
if id not in self.game:
|
||||
return "No game in progress"
|
||||
score = self.game[id]['score']
|
||||
total = self.game[id]['total']
|
||||
level = self.game[id]['level']
|
||||
if self.game[id]['errors']:
|
||||
areaofstudy = max(set(self.game[id]['errors']), key = self.game[id]['errors'].count)
|
||||
else:
|
||||
areaofstudy = "None"
|
||||
|
||||
if level == 'extra':
|
||||
pool = 50
|
||||
else:
|
||||
pool = 35
|
||||
|
||||
return f"Score: {score}/{pool}\nQuestions left: {total}\nArea of study: {areaofstudy}"
|
||||
|
||||
def endGame(self, id):
|
||||
if id not in self.game:
|
||||
return "No game in progress"
|
||||
|
||||
score = self.game[id]['score']
|
||||
level = self.game[id]['level']
|
||||
|
||||
if level == 'extra':
|
||||
# passing score for extra is 37 out of 50
|
||||
passing = 37
|
||||
else:
|
||||
# passing score for technician and general is 26 out of 35
|
||||
passing = 26
|
||||
|
||||
if score >= passing:
|
||||
msg = f"Game over. Score: {score} 73! 🎉You passed the {level} exam."
|
||||
else:
|
||||
# find the most common section of the questions missed
|
||||
if self.game[id]['errors']:
|
||||
areaofstudy = max(set(self.game[id]['errors']), key = self.game[id]['errors'].count)
|
||||
else:
|
||||
areaofstudy = "None"
|
||||
msg = f"Game over. Score: {score} 73! 😿You did not pass the {level} exam. \nYou may want to study {areaofstudy}."
|
||||
|
||||
# remove the game[id] from the list
|
||||
del self.game[id]
|
||||
return msg
|
||||
|
||||
hamtestTracker = []
|
||||
hamtest = HamTest()
|
||||
|
||||
203
modules/games/hangman.py
Normal file
203
modules/games/hangman.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# Written for Meshtastic mesh-bot by ZR1RF Johannes le Roux 2025
|
||||
import random
|
||||
|
||||
class Hangman:
|
||||
WORDS = [
|
||||
"ability","able","about","above","accept","according","account","across",
|
||||
"act","action","activity","actually","add","address","administration","admit",
|
||||
"adult","affect","after","again","against","age","agency","agent","ago",
|
||||
"agree","agreement","ahead","air","all","allow","almost","alone","along",
|
||||
"already","also","although","always","American","among","amount","analysis",
|
||||
"and","animal","another","answer","any","anyone","anything","appear","apply",
|
||||
"approach","area","argue","arm","around","arrive","art","article","artist",
|
||||
"as","ask","assume","at","attack","attention","attorney","audience","author",
|
||||
"authority","available","avoid","away","baby","back","bad","bag","ball",
|
||||
"bank","bar","base","be","beat","beautiful","because","become","bed","before",
|
||||
"begin","behavior","behind","believe","benefit","best","better","between",
|
||||
"beyond","big","bill","billion","bit","black","blood","blue","board","body",
|
||||
"book","born","both","box","boy","break","bring","brother","budget","build",
|
||||
"building","business","but","buy","by","call","camera","campaign","can",
|
||||
"cancer","candidate","capital","car","card","care","career","carry","case",
|
||||
"catch","cause","cell","center","central","century","certain","certainly",
|
||||
"chair","challenge","chance","change","character","charge","check","child",
|
||||
"choice","choose","church","citizen","city","civil","claim","class","clear",
|
||||
"clearly","close","coach","cold","collection","college","color","come",
|
||||
"commercial","common","community","company","compare","computer","concern",
|
||||
"condition","conference","Congress","consider","consumer","contain","continue",
|
||||
"control","cost","could","country","couple","course","court","cover","create",
|
||||
"crime","cultural","culture","cup","current","customer","cut","dark","data",
|
||||
"daughter","day","dead","deal","death","debate","decade","decide","decision",
|
||||
"deep","defense","degree","democrat","democratic","describe","design",
|
||||
"despite","detail","determine","develop","development","die","difference",
|
||||
"different","difficult","dinner","direction","director","discover","discuss",
|
||||
"discussion","disease","do","doctor","dog","door","down","draw","dream","drive",
|
||||
"drop","drug","during","each","early","east","easy","eat","economic","economy",
|
||||
"edge","education","effect","effort","eight","either","election","else",
|
||||
"employee","end","energy","enjoy","enough","enter","entire","environment",
|
||||
"environmental","especially","establish","even","evening","event","ever",
|
||||
"every","everybody","everyone","everything","evidence","exactly","example",
|
||||
"executive","exist","expect","experience","expert","explain","eye","face",
|
||||
"fact","factor","fail","fall","family","far","fast","father","fear","federal",
|
||||
"feel","feeling","few","field","fight","figure","fill","film","final","finally",
|
||||
"financial","find","fine","finger","finish","fire","firm","first","fish","five",
|
||||
"floor","fly","focus","follow","food","foot","for","force","foreign","forget",
|
||||
"form","former","forward","four","free","friend","from","front","full","fund",
|
||||
"future","game","garden","gas","general","generation","get","girl","give",
|
||||
"glass","go","goal","good","government","great","green","ground","group","grow",
|
||||
"growth","guess","gun","guy","hair","half","hand","hang","happen","happy",
|
||||
"hard","have","he","head","health","hear","heart","heat","heavy","help","her",
|
||||
"here","herself","high","him","himself","his","history","hit","hold","home",
|
||||
"hope","hospital","hot","hotel","hour","house","how","however","huge","human",
|
||||
"hundred","husband","I","idea","identify","if","image","imagine","impact",
|
||||
"important","improve","in","include","including","increase","indeed","indicate",
|
||||
"individual","industry","information","inside","instead","institution","interest",
|
||||
"interesting","international","interview","into","investment","involve","issue",
|
||||
"it","item","its","itself","job","join","just","keep","key","kid","kill","kind",
|
||||
"kitchen","know","knowledge","land","language","large","last","late","later",
|
||||
"laugh","law","lawyer","lay","lead","leader","learn","least","leave","left",
|
||||
"leg","legal","less","let","letter","level","lie","life","light","like","likely",
|
||||
"line","list","listen","little","live","local","long","look","lose","loss",
|
||||
"lot","love","low","machine","magazine","main","maintain","major","majority",
|
||||
"make","man","manage","management","manager","many","market","marriage",
|
||||
"material","matter","may","maybe","me","mean","measure","media","medical","meet",
|
||||
"meeting","member","memory","mention","message","method","middle","might",
|
||||
"military","million","mind","minute","miss","mission","model","modern","moment",
|
||||
"money","month","more","morning","most","mother","mouth","move","movement",
|
||||
"movie","Mr","Mrs","much","music","must","my","myself","name","nation",
|
||||
"national","natural","nature","near","nearly","necessary","need","network",
|
||||
"never","new","news","newspaper","next","nice","night","no","none","nor",
|
||||
"north","not","note","nothing","notice","now","number","occur","of","off",
|
||||
"offer","office","officer","official","often","oh","oil","ok","old","on",
|
||||
"once","one","only","onto","open","operation","opportunity","option","or",
|
||||
"order","organization","other","others","our","out","outside","over","own",
|
||||
"owner","page","pain","painting","paper","parent","part","participant",
|
||||
"particular","particularly","partner","party","pass","past","patient","pattern",
|
||||
"pay","peace","people","per","perform","performance","perhaps","period",
|
||||
"person","personal","phone","physical","pick","picture","piece","place","plan",
|
||||
"plant","play","player","point","police","policy","political","politics",
|
||||
"poor","popular","population","position","positive","possible","power",
|
||||
"practice","prepare","present","president","pressure","pretty","prevent","price",
|
||||
"private","probably","problem","process","produce","product","production",
|
||||
"professional","professor","program","project","property","protect","prove",
|
||||
"provide","public","pull","purpose","push","put","quality","question","quickly",
|
||||
"quite","race","radio","raise","range","rate","rather","reach","read","ready",
|
||||
"real","reality","realize","really","reason","receive","recent","recently",
|
||||
"recognize","record","red","reduce","reflect","region","relate","relationship",
|
||||
"religious","remain","remember","remove","report","represent","republican",
|
||||
"require","research","resource","respond","response","responsibility","rest",
|
||||
"result","return","reveal","rich","right","rise","risk","road","rock","role",
|
||||
"room","rule","run","safe","same","save","say","scene","school","science",
|
||||
"scientist","score","sea","season","seat","second","section","security","see",
|
||||
"seek","seem","sell","send","senior","sense","series","serious","serve",
|
||||
"service","set","seven","several","shake","share","she","shoot","short","shot",
|
||||
"should","shoulder","show","side","sign","significant","similar","simple",
|
||||
"simply","since","sing","single","sister","sit","site","situation","six","size",
|
||||
"skill","skin","small","smile","so","social","society","soldier","some",
|
||||
"somebody","someone","something","sometimes","son","song","soon","sort","sound",
|
||||
"source","south","southern","space","speak","special","specific","speech",
|
||||
"spend","sport","spring","staff","stage","stand","standard","star","start",
|
||||
"state","statement","station","stay","step","still","stock","stop","store",
|
||||
"story","strategy","street","strong","structure","student","study","stuff",
|
||||
"style","subject","success","successful","such","suddenly","suffer","suggest",
|
||||
"summer","support","sure","surface","system","table","take","talk","task","tax",
|
||||
"teach","teacher","team","technology","television","tell","ten","tend","term",
|
||||
"test","than","thank","that","the","their","them","themselves","then","theory",
|
||||
"there","these","they","thing","think","third","this","those","though","thought",
|
||||
"thousand","threat","three","through","throughout","throw","thus","time","to",
|
||||
"today","together","tonight","too","top","total","tough","toward","town","trade",
|
||||
"traditional","training","travel","treat","treatment","tree","trial","trip",
|
||||
"trouble","true","truth","try","turn","TV","two","type","under","understand",
|
||||
"unit","until","up","upon","us","use","usually","value","various","very",
|
||||
"victim","view","violence","visit","voice","vote","wait","walk","wall","want",
|
||||
"war","watch","water","way","we","weapon","wear","week","weight","well","west",
|
||||
"western","what","whatever","when","where","whether","which","while","white",
|
||||
"who","whole","whom","whose","why","wide","wife","will","win","wind","window",
|
||||
"wish","with","within","without","woman","wonder","word","work","worker","world",
|
||||
"worry","would","write","writer","wrong","yard","yeah","year","yes","yet","you",
|
||||
"young","your","yourself","meshtastic","node","lora","mesh"]
|
||||
|
||||
def __init__(self):
|
||||
self.game = {}
|
||||
|
||||
def new_game(self, id):
|
||||
games = won = 0
|
||||
ret = ""
|
||||
if id in self.game:
|
||||
games = self.game[id]["games"]
|
||||
won = self.game[id]["won"]
|
||||
ret += f"Total Games: {games}, Won: {won}\n"
|
||||
|
||||
self.game[id] = {
|
||||
"word": self.random_word(),
|
||||
"guesses": [],
|
||||
"games": games+1,
|
||||
"won": won
|
||||
}
|
||||
ret += self.game_continue(id)
|
||||
return ret
|
||||
|
||||
def guess(self, id, input):
|
||||
g = self.game[id]
|
||||
if not input:
|
||||
return
|
||||
letter = input[0].lower()
|
||||
if letter.isalpha() and letter not in g["guesses"]:
|
||||
g["guesses"].append(letter)
|
||||
|
||||
def wrong_guesses(self, id):
|
||||
g = self.game[id]
|
||||
wrong = 0
|
||||
for letter in g["guesses"]:
|
||||
if letter not in g["word"]:
|
||||
wrong += 1
|
||||
return wrong
|
||||
|
||||
def won(self, id):
|
||||
g = self.game[id]
|
||||
for letter in g["word"]:
|
||||
if letter not in g["guesses"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def mask(self, id):
|
||||
g = self.game[id]
|
||||
return " ".join([a if a in g["guesses"] else "_" for a in g["word"]])
|
||||
|
||||
def game_board(self, id):
|
||||
g = self.game[id]
|
||||
emotions = "😀🙂😐😑😕😔💀"
|
||||
wrong = self.wrong_guesses(id)
|
||||
ret = ""
|
||||
if self.won(id):
|
||||
ret += "🥳" + "\n"
|
||||
g["won"] += 1
|
||||
else:
|
||||
ret += emotions[wrong] + "\n"
|
||||
ret += hangman.mask(id) + "\n"
|
||||
if g["guesses"]:
|
||||
ret += ",".join(g["guesses"]) + "\n"
|
||||
return ret
|
||||
|
||||
def game_continue(self, id):
|
||||
return self.game_board(id) + "Guess a letter"
|
||||
|
||||
def game_over(self, id):
|
||||
return self.game_board(id) + "Game over, the word was " + self.game[id]["word"]
|
||||
|
||||
def play(self, id, input):
|
||||
if id not in self.game:
|
||||
return self.new_game(id)
|
||||
self.guess(id, input)
|
||||
wrong = self.wrong_guesses(id)
|
||||
if wrong >= 6 or self.won(id):
|
||||
return self.game_over(id) + "\n" + self.new_game(id)
|
||||
return self.game_continue(id)
|
||||
|
||||
def end(self, id):
|
||||
del self.game[id]
|
||||
|
||||
def random_word(self):
|
||||
return random.choice(self.WORDS)
|
||||
|
||||
hangmanTracker = []
|
||||
hangman = Hangman()
|
||||
@@ -164,7 +164,7 @@ class PlayerVP:
|
||||
return self.show_hand()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
return "ex:1,3,4 deals them new, and keeps 2,5 or (N)o to keep current (H)and"
|
||||
|
||||
# Method for scoring hand, calculating winnings, and outputting message
|
||||
@@ -390,7 +390,7 @@ def playVideoPoker(nodeID, message):
|
||||
else:
|
||||
if drawCount <= 1:
|
||||
msg = player.redraw(deck, message)
|
||||
if msg.startswith("Send Card"):
|
||||
if msg.startswith("ex:"):
|
||||
# if returned error message, return it
|
||||
return msg
|
||||
drawCount += 1
|
||||
@@ -403,7 +403,7 @@ def playVideoPoker(nodeID, message):
|
||||
if drawCount == 2:
|
||||
# this is the last draw will carry on to endGame for scoring
|
||||
msg = player.redraw(deck, message) + f"\n"
|
||||
if msg.startswith("Send Card"):
|
||||
if msg.startswith("ex:"):
|
||||
# if returned error message, return it
|
||||
return msg
|
||||
# redraw done
|
||||
|
||||
@@ -9,7 +9,7 @@ import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from modules.log import *
|
||||
|
||||
trap_list_location = ("whereami", "tide", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow")
|
||||
trap_list_location = ("whereami", "tide", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow","valert")
|
||||
|
||||
def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
whereIam = ""
|
||||
@@ -234,38 +234,39 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
# get weather data from NOAA units for metric unit = 1 is metric
|
||||
if use_metric:
|
||||
unit = 1
|
||||
logger.debug("Location: new API metric units not implemented yet")
|
||||
|
||||
weather_url = "https://forecast.weather.gov/MapClick.php?FcstType=text&lat=" + str(lat) + "&lon=" + str(lon)
|
||||
if unit == 1:
|
||||
weather_url += "&unit=1"
|
||||
|
||||
weather_api = "https://api.weather.gov/points/" + str(lat) + "," + str(lon)
|
||||
# extract the "forecast": property from the JSON response
|
||||
try:
|
||||
weather_data = requests.get(weather_url, timeout=urlTimeoutSeconds)
|
||||
weather_data = requests.get(weather_api, timeout=urlTimeoutSeconds)
|
||||
if not weather_data.ok:
|
||||
logger.error("Location:Error fetching weather data from NOAA")
|
||||
logger.warning("Location:Error fetching weather data from NOAA for location")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.error("Location:Error fetching weather data from NOAA")
|
||||
logger.warning("Location:Error fetching weather data from NOAA for location")
|
||||
return ERROR_FETCHING_DATA
|
||||
# get the forecast URL from the JSON response
|
||||
weather_json = weather_data.json()
|
||||
forecast_url = weather_json['properties']['forecast']
|
||||
try:
|
||||
forecast_data = requests.get(forecast_url, timeout=urlTimeoutSeconds)
|
||||
if not forecast_data.ok:
|
||||
logger.warning("Location:Error fetching weather forecast from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching weather forecast from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
soup = bs.BeautifulSoup(weather_data.text, 'html.parser')
|
||||
table = soup.find('div', id="detailed-forecast-body")
|
||||
# from periods, get the detailedForecast from number of days in NOAAforecastDuration
|
||||
forecast_json = forecast_data.json()
|
||||
forecast = forecast_json['properties']['periods']
|
||||
for day in forecast[:forecastDuration]:
|
||||
# abreviate the forecast
|
||||
|
||||
if table is None:
|
||||
logger.error("Location:Bad weather data from NOAA")
|
||||
return ERROR_FETCHING_DATA
|
||||
else:
|
||||
# get rows
|
||||
rows = table.find_all('div', class_="row")
|
||||
|
||||
# extract data from rows
|
||||
for row in rows:
|
||||
# shrink the text
|
||||
line = abbreviate_noaa(row.text)
|
||||
# only grab a few days of weather
|
||||
if len(weather.split("\n")) < forecastDuration:
|
||||
weather += line + "\n"
|
||||
# trim off last newline
|
||||
weather += abbreviate_noaa(day['name']) + ": " + abbreviate_noaa(day['detailedForecast']) + "\n"
|
||||
# remove last newline
|
||||
weather = weather[:-1]
|
||||
|
||||
# get any alerts and return the count
|
||||
@@ -287,20 +288,13 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
def abbreviate_noaa(row):
|
||||
# replace long strings with shorter ones for display
|
||||
replacements = {
|
||||
"monday": "Mon ",
|
||||
"tuesday": "Tue ",
|
||||
"wednesday": "Wed ",
|
||||
"thursday": "Thu ",
|
||||
"friday": "Fri ",
|
||||
"saturday": "Sat ",
|
||||
"sunday": "Sun ",
|
||||
"today": "Today ",
|
||||
"night": "Night ",
|
||||
"tonight": "Tonight ",
|
||||
"tomorrow": "Tomorrow ",
|
||||
"day": "Day ",
|
||||
"this afternoon": "Afternoon ",
|
||||
"overnight": "Overnight ",
|
||||
"monday": "Mon",
|
||||
"tuesday": "Tue",
|
||||
"wednesday": "Wed",
|
||||
"thursday": "Thu",
|
||||
"friday": "Fri",
|
||||
"saturday": "Sat",
|
||||
"sunday": "Sun",
|
||||
"northwest": "NW",
|
||||
"northeast": "NE",
|
||||
"southwest": "SW",
|
||||
@@ -330,6 +324,9 @@ def abbreviate_noaa(row):
|
||||
"degrees": "°",
|
||||
"percent": "%",
|
||||
"department": "Dept.",
|
||||
"amounts less than a tenth of an inch possible.": "< 0.1in",
|
||||
"temperatures": "temps.",
|
||||
"temperature": "temp.",
|
||||
}
|
||||
|
||||
line = row
|
||||
@@ -400,6 +397,12 @@ def alertBrodcastNOAA():
|
||||
elif currentAlert == NO_ALERTS:
|
||||
wxAlertCacheNOAA = ""
|
||||
return False
|
||||
if ignoreEASenable:
|
||||
# check if the alert is in the ignoreEAS list
|
||||
for word in ignoreEASwords:
|
||||
if word.lower() in currentAlert[0].lower():
|
||||
logger.debug(f"Location:Ignoring NOAA Alert: {currentAlert[0]} containing {word}")
|
||||
return False
|
||||
# broadcast the alerts send to wxBrodcastCh
|
||||
elif currentAlert[0] not in wxAlertCacheNOAA:
|
||||
# Check if the current alert is not in the weather alert cache
|
||||
@@ -517,7 +520,6 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
if info.getElementsByTagName("description") and info.getElementsByTagName("description")[0].childNodes:
|
||||
description = info.getElementsByTagName("description")[0].childNodes[0].nodeValue
|
||||
else:
|
||||
logger.debug(f"System: report this to discord - iPAWS No description for alert: {headline}")
|
||||
description = headline
|
||||
|
||||
area_table = info.getElementsByTagName("area")[0]
|
||||
@@ -536,10 +538,11 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
# check if the alert is for the current location, if wanted keep alert
|
||||
if (sameVal in mySAME) or (geocode_value in mySAME):
|
||||
# ignore the FEMA test alerts
|
||||
if ignoreFEMAtest:
|
||||
if "Test" in headline:
|
||||
logger.debug(f"System: Ignoring FEMA Test Alert: {headline} for {areaDesc}")
|
||||
continue
|
||||
if ignoreFEMAenable:
|
||||
for word in ignoreFEMAwords:
|
||||
if word.lower() in headline.lower():
|
||||
logger.debug(f"System: Ignoring FEMA Alert: {headline} containing {word} at {areaDesc}")
|
||||
continue
|
||||
|
||||
# add to alerts list
|
||||
alerts.append({
|
||||
@@ -619,3 +622,53 @@ def get_flood_noaa(lat=0, lon=0, uid=0):
|
||||
|
||||
return flood_data
|
||||
|
||||
def get_volcano_usgs(lat=0, lon=0):
|
||||
alerts = ''
|
||||
if lat == 0 and lon == 0:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
# get the latest volcano alert from USGS from CAP feed
|
||||
usgs_volcano_url = "https://volcanoes.usgs.gov/hans-public/api/volcano/getCapElevated"
|
||||
try:
|
||||
volcano_data = requests.get(usgs_volcano_url, timeout=urlTimeoutSeconds)
|
||||
if not volcano_data.ok:
|
||||
logger.warning("System: USGS fetching volcano alerts from USGS")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("System: USGS fetching volcano alerts from USGS")
|
||||
return ERROR_FETCHING_DATA
|
||||
volcano_json = volcano_data.json()
|
||||
# extract alerts from main feed
|
||||
if volcano_json and isinstance(volcano_json, list):
|
||||
for alert in volcano_json:
|
||||
# check ignore list
|
||||
if ignoreUSGSEnable:
|
||||
for word in ignoreUSGSwords:
|
||||
if word.lower() in alert['volcano_name_appended'].lower():
|
||||
logger.debug(f"System: Ignoring USGS Alert: {alert['volcano_name_appended']} containing {word}")
|
||||
continue
|
||||
# check if the alert lat long is within the range of bot latitudeValue and longitudeValue
|
||||
if (alert['latitude'] >= latitudeValue - 10 and alert['latitude'] <= latitudeValue + 10) and (alert['longitude'] >= longitudeValue - 10 and alert['longitude'] <= longitudeValue + 10):
|
||||
volcano_name = alert['volcano_name_appended']
|
||||
alert_level = alert['alert_level']
|
||||
color_code = alert['color_code']
|
||||
cap_severity = alert['cap_severity']
|
||||
synopsis = alert['synopsis']
|
||||
# format Alert
|
||||
alerts += f"🌋🚨: {volcano_name}, {alert_level} {color_code}, {cap_severity}.\n{synopsis}\n"
|
||||
else:
|
||||
#logger.debug(f"System: USGS volcano alert not in range: {alert['volcano_name_appended']}")
|
||||
continue
|
||||
else:
|
||||
logger.debug("Location:Error fetching volcano data from USGS")
|
||||
return NO_ALERTS
|
||||
if alerts == "":
|
||||
return NO_ALERTS
|
||||
# trim off last newline
|
||||
if alerts[-1] == "\n":
|
||||
alerts = alerts[:-1]
|
||||
# return the alerts
|
||||
alerts = abbreviate_noaa(alerts)
|
||||
return alerts
|
||||
|
||||
|
||||
|
||||
@@ -69,14 +69,14 @@ logger.addHandler(stdout_handler)
|
||||
|
||||
if syslog_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count)
|
||||
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count, encoding='utf-8')
|
||||
file_handler_sys.setLevel(LOGGING_LEVEL) # DEBUG used by default for system logs to disk
|
||||
file_handler_sys.setFormatter(plainFormatter(logFormat))
|
||||
logger.addHandler(file_handler_sys)
|
||||
|
||||
if log_messages_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count)
|
||||
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count, encoding='utf-8')
|
||||
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
|
||||
file_handler.setFormatter(logging.Formatter(msgLogFormat))
|
||||
msgLogger.addHandler(file_handler)
|
||||
|
||||
@@ -26,7 +26,6 @@ max_retry_count2 = 4 # max retry count for interface 2
|
||||
retry_int1 = False
|
||||
retry_int2 = False
|
||||
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
|
||||
playingGame = False
|
||||
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
|
||||
cmdHistory = [] # list to hold the last commands
|
||||
seenNodes = [] # list to hold the last seen nodes
|
||||
@@ -36,7 +35,7 @@ config = configparser.ConfigParser()
|
||||
config_file = "config.ini"
|
||||
|
||||
try:
|
||||
config.read(config_file)
|
||||
config.read(config_file, encoding='utf-8')
|
||||
except Exception as e:
|
||||
print(f"System: Error reading config file: {e}")
|
||||
|
||||
@@ -196,7 +195,9 @@ try:
|
||||
# general
|
||||
useDMForResponse = config['general'].getboolean('respond_by_dm_only', True)
|
||||
publicChannel = config['general'].getint('defaultChannel', 0) # the meshtastic public channel
|
||||
ignoreChannels = config['general'].get('ignoreChannels', '').split(',') # ignore these channels
|
||||
ignoreDefaultChannel = config['general'].getboolean('ignoreDefaultChannel', False)
|
||||
cmdBang = config['general'].getboolean('cmdBang', False) # default off
|
||||
zuluTime = config['general'].getboolean('zuluTime', False) # aka 24 hour time
|
||||
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', False) # default off
|
||||
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
|
||||
@@ -251,15 +252,22 @@ try:
|
||||
enableGBalerts = config['location'].getboolean('enableGBalerts', False) # default False
|
||||
enableDEalerts = config['location'].getboolean('enableDEalerts', False) # default False
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
|
||||
ignoreEASenable = config['location'].getboolean('ignoreEASenable', False) # default False
|
||||
ignoreEASwords = config['location'].get('ignoreEASwords', 'test,advisory').split(',') # default test,advisory
|
||||
mySAME = config['location'].get('mySAME', '').split(',') # default empty
|
||||
myRegionalKeysDE = config['location'].get('myRegionalKeysDE', '110000000000').split(',') # default city Berlin
|
||||
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
|
||||
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
|
||||
enableExtraLocationWx = config['location'].getboolean('enableExtraLocationWx', False) # default False
|
||||
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
|
||||
ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True
|
||||
ignoreFEMAenable = config['location'].getboolean('ignoreFEMAenable', True) # default True
|
||||
ignoreFEMAwords = config['location'].get('ignoreFEMAwords', 'test,exercise').split(',') # default test,exercise
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
emergencyAlertBroadcastCh = config['location'].get('eAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
volcanoAlertBroadcastEnabled = config['location'].getboolean('volcanoAlertBroadcastEnabled', False) # default False
|
||||
volcanoAlertBroadcastChannel = config['location'].get('volcanoAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
ignoreUSGSEnable = config['location'].getboolean('ignoreVolcanoEnable', False) # default False
|
||||
ignoreUSGSWords = config['location'].get('ignoreVolcanoWords', 'test,advisory').split(',') # default test,advisory
|
||||
|
||||
# bbs
|
||||
bbs_enabled = config['bbs'].getboolean('enabled', False)
|
||||
@@ -303,6 +311,12 @@ try:
|
||||
|
||||
# scheduler
|
||||
scheduler_enabled = config['scheduler'].getboolean('enabled', False)
|
||||
schedulerInterface = config['scheduler'].getint('interface', 1) # default interface 1
|
||||
schedulerChannel = config['scheduler'].getint('channel', 2) # default channel 2
|
||||
schedulerMessage = config['scheduler'].get('message', 'Scheduled message') # default message
|
||||
schedulerInterval = config['scheduler'].get('interval', '') # default empty
|
||||
schedulerTime = config['scheduler'].get('time', '') # default empty
|
||||
schedulerValue = config['scheduler'].get('value', '') # default empty
|
||||
|
||||
# radio monitoring
|
||||
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
|
||||
@@ -330,6 +344,8 @@ try:
|
||||
videoPoker_enabled = config['games'].getboolean('videoPoker', True)
|
||||
mastermind_enabled = config['games'].getboolean('mastermind', True)
|
||||
golfSim_enabled = config['games'].getboolean('golfSim', True)
|
||||
hangman_enabled = config['games'].getboolean('hangman', True)
|
||||
hamtest_enabled = config['games'].getboolean('hamtest', True)
|
||||
|
||||
# messaging settings
|
||||
responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7
|
||||
@@ -337,6 +353,7 @@ try:
|
||||
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160
|
||||
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
|
||||
maxBuffer = config['messagingSettings'].getint('maxBuffer', 220) # default 220
|
||||
enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False
|
||||
|
||||
except KeyError as e:
|
||||
print(f"System: Error reading config file: {e}")
|
||||
|
||||
@@ -89,7 +89,14 @@ if location_enabled:
|
||||
from modules.wx_meteo import * # from the spudgunman/meshing-around repo
|
||||
else:
|
||||
# NOAA only features
|
||||
help_message = help_message + ", wxa, tide, ealert"
|
||||
help_message = help_message + ", wxa, tide"
|
||||
|
||||
# NOAA alerts needs location module
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled or volcanoAlertBroadcastEnabled:
|
||||
from modules.locationdata import * # from the spudgunman/meshing-around repo
|
||||
# limited subset, this should be done better but eh..
|
||||
trap_list = trap_list + ("wx", "wxc", "wxa", "wxalert", "ea", "ealert", "valert")
|
||||
help_message = help_message + ", wxalert, ealert, valert"
|
||||
|
||||
# BBS Configuration
|
||||
if bbs_enabled:
|
||||
@@ -151,7 +158,17 @@ if golfSim_enabled:
|
||||
from modules.games.golfsim import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + ("golfsim",)
|
||||
games_enabled = True
|
||||
|
||||
|
||||
if hangman_enabled:
|
||||
from modules.games.hangman import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + ("hangman",)
|
||||
games_enabled = True
|
||||
|
||||
if hamtest_enabled:
|
||||
from modules.games.hamtest import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + ("hamtest",)
|
||||
games_enabled = True
|
||||
|
||||
# Games Configuration
|
||||
if games_enabled is True:
|
||||
help_message = help_message + ", games"
|
||||
@@ -172,6 +189,10 @@ if games_enabled is True:
|
||||
gamesCmdList += "masterMind, "
|
||||
if golfSim_enabled:
|
||||
gamesCmdList += "golfSim, "
|
||||
if hangman_enabled:
|
||||
gamesCmdList += "hangman, "
|
||||
if hamtest_enabled:
|
||||
gamesCmdList += "hamTest, "
|
||||
gamesCmdList = gamesCmdList[:-2] # remove the last comma
|
||||
else:
|
||||
gamesCmdList = ""
|
||||
@@ -363,27 +384,27 @@ def get_node_list(nodeInt=1):
|
||||
|
||||
return node_list
|
||||
|
||||
def get_node_location(number, nodeInt=1, channel=0):
|
||||
def get_node_location(nodeID, nodeInt=1, channel=0):
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
# Get the location of a node by its number from nodeDB on device
|
||||
# if no location data, return default location
|
||||
latitude = latitudeValue
|
||||
longitude = longitudeValue
|
||||
position = [latitudeValue,longitudeValue]
|
||||
lastheard = 0
|
||||
if interface.nodes:
|
||||
for node in interface.nodes.values():
|
||||
if number == node['num']:
|
||||
if 'position' in node:
|
||||
if nodeID == node['num']:
|
||||
if 'position' in node and node['position'] is not {}:
|
||||
try:
|
||||
latitude = node['position']['latitude']
|
||||
longitude = node['position']['longitude']
|
||||
logger.debug(f"System: location data for {nodeID} is {latitude},{longitude}")
|
||||
position = [latitude,longitude]
|
||||
except Exception as e:
|
||||
logger.warning(f"System: Error getting location data for {number}")
|
||||
logger.debug(f"System: location data for {number} is {latitude},{longitude}")
|
||||
position = [latitude,longitude]
|
||||
logger.debug(f"System: No location data for {nodeID} use default location")
|
||||
return position
|
||||
else:
|
||||
logger.warning(f"System: No location data for {number} using default location")
|
||||
logger.debug(f"System: No location data for {nodeID} using default location")
|
||||
# request location data
|
||||
# try:
|
||||
# logger.debug(f"System: Requesting location data for {number}")
|
||||
@@ -392,7 +413,7 @@ def get_node_location(number, nodeInt=1, channel=0):
|
||||
# logger.error(f"System: Error requesting location data for {number}. Error: {e}")
|
||||
return position
|
||||
else:
|
||||
logger.warning(f"System: No nodes found")
|
||||
logger.warning(f"System: Location for NodeID {nodeID} not found in nodeDb")
|
||||
return position
|
||||
|
||||
|
||||
@@ -467,7 +488,7 @@ def handleSentinelIgnore(nodeInt=1, nodeID=0, aor=False):
|
||||
else:
|
||||
sentryIgnoreList.remove(str(nodeID))
|
||||
logger.info(f"System: Removed {nodeID} from sentry ignore list")
|
||||
|
||||
|
||||
def messageChunker(message):
|
||||
message_list = []
|
||||
if len(message) > MESSAGE_CHUNK_SIZE:
|
||||
@@ -487,9 +508,9 @@ def messageChunker(message):
|
||||
sentence = ''
|
||||
for char in part:
|
||||
sentence += char
|
||||
if char in '.!?':
|
||||
sentences.append(sentence.strip())
|
||||
sentence = ''
|
||||
# if char in '.!?':
|
||||
# sentences.append(sentence.strip())
|
||||
# sentence = ''
|
||||
if sentence:
|
||||
sentences.append(sentence.strip())
|
||||
|
||||
@@ -523,8 +544,12 @@ def messageChunker(message):
|
||||
final_message_list = []
|
||||
for chunk in message_list:
|
||||
while len(chunk) > MESSAGE_CHUNK_SIZE:
|
||||
final_message_list.append(chunk[:MESSAGE_CHUNK_SIZE])
|
||||
chunk = chunk[MESSAGE_CHUNK_SIZE:]
|
||||
# Find the last space within the chunk size limit
|
||||
split_index = chunk.rfind(' ', 0, MESSAGE_CHUNK_SIZE)
|
||||
if split_index == -1:
|
||||
split_index = MESSAGE_CHUNK_SIZE
|
||||
final_message_list.append(chunk[:split_index])
|
||||
chunk = chunk[split_index:].strip()
|
||||
if chunk:
|
||||
final_message_list.append(chunk)
|
||||
|
||||
@@ -641,6 +666,8 @@ def messageTrap(msg):
|
||||
# if word in message is in the trap list, return True
|
||||
if t.lower() == m.lower():
|
||||
return True
|
||||
if cmdBang and m.startswith("!"):
|
||||
return True
|
||||
# if no trap words found, run a search for near misses like ping? or cmd?
|
||||
for m in message_list:
|
||||
for t in range(len(trap_list)):
|
||||
@@ -696,12 +723,15 @@ def handleMultiPing(nodeID=0, deviceID=1):
|
||||
multiPingList.pop(j)
|
||||
break
|
||||
|
||||
|
||||
priorVolcanoAlert = ""
|
||||
def handleAlertBroadcast(deviceID=1):
|
||||
global priorVolcanoAlert
|
||||
alertUk = NO_ALERTS
|
||||
alertDe = NO_ALERTS
|
||||
alertFema = NO_ALERTS
|
||||
wxAlert = NO_ALERTS
|
||||
volcanoAlert = NO_ALERTS
|
||||
alertWx = False
|
||||
# only allow API call every 20 minutes
|
||||
# the watchdog will call this function 3 times, seeing possible throttling on the API
|
||||
clock = datetime.now()
|
||||
@@ -725,7 +755,7 @@ def handleAlertBroadcast(deviceID=1):
|
||||
|
||||
# format alert
|
||||
if alertWx:
|
||||
wxAlert = f"🚨 {alertWx[1]} EAS WX ALERT: {alertWx[0]}"
|
||||
wxAlert = f"🚨 {alertWx[1]} EAS-WX ALERT: {alertWx[0]}"
|
||||
else:
|
||||
wxAlert = False
|
||||
|
||||
@@ -757,8 +787,8 @@ def handleAlertBroadcast(deviceID=1):
|
||||
send_message(ukAlert, emergencyAlertBroadcastCh, 0, deviceID)
|
||||
return True
|
||||
|
||||
# pause for 10 seconds
|
||||
time.sleep(10)
|
||||
# pause for traffic
|
||||
time.sleep(5)
|
||||
|
||||
if wxAlertBroadcastEnabled:
|
||||
if wxAlert:
|
||||
@@ -768,6 +798,22 @@ def handleAlertBroadcast(deviceID=1):
|
||||
else:
|
||||
send_message(wxAlert, wxAlertBroadcastChannel, 0, deviceID)
|
||||
return True
|
||||
|
||||
# pause for traffic
|
||||
time.sleep(5)
|
||||
|
||||
if volcanoAlertBroadcastEnabled:
|
||||
volcanoAlert = get_volcano_usgs(latitudeValue, longitudeValue)
|
||||
if volcanoAlert and NO_ALERTS not in volcanoAlert and ERROR_FETCHING_DATA not in volcanoAlert:
|
||||
# check if the alert is different from the last one
|
||||
if volcanoAlert != priorVolcanoAlert:
|
||||
priorVolcanoAlert = volcanoAlert
|
||||
if isinstance(volcanoAlertBroadcastChannel, list):
|
||||
for channel in volcanoAlertBroadcastChannel:
|
||||
send_message(volcanoAlert, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(volcanoAlert, volcanoAlertBroadcastChannel, 0, deviceID)
|
||||
return True
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
|
||||
@@ -933,7 +979,7 @@ def consumeMetadata(packet, rxNode=0):
|
||||
for key in keys:
|
||||
positionMetadata[nodeID][key] = position_data.get(key, 0)
|
||||
|
||||
# Keep the positionMetadata dictionary at 5 records
|
||||
# Keep the positionMetadata dictionary at a maximum size of 20
|
||||
if len(positionMetadata) > 20:
|
||||
# Remove the oldest entry
|
||||
oldest_nodeID = next(iter(positionMetadata))
|
||||
@@ -1182,7 +1228,7 @@ async def watchdog():
|
||||
|
||||
handleMultiPing(0, i)
|
||||
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled:
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled or volcanoAlertBroadcastEnabled:
|
||||
handleAlertBroadcast(i)
|
||||
|
||||
intData = displayNodeTelemetry(0, i)
|
||||
|
||||
@@ -40,8 +40,8 @@ class QuietHandler(http.server.SimpleHTTPRequestHandler):
|
||||
# Change the current working directory to webRoot
|
||||
os.chdir(webRoot)
|
||||
|
||||
# boot up simple HTTP server
|
||||
httpd = http.server.HTTPServer(('127.0.0.1', PORT), QuietHandler)
|
||||
# Create the HTTP server instance with the desired IP address
|
||||
httpd = http.server.HTTPServer((server_ip, PORT), QuietHandler)
|
||||
|
||||
if SSL:
|
||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
@@ -51,12 +51,9 @@ if SSL:
|
||||
print("SSL certificate file not found. Please generate it using the command provided in the comments.")
|
||||
exit(1)
|
||||
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
|
||||
|
||||
# Create the HTTP server instance with the desired IP address
|
||||
httpd = http.server.HTTPServer((server_ip, PORT), QuietHandler)
|
||||
|
||||
# Print out the URL using the IP address stored in server_ip
|
||||
print(f"Serving reports at http://{server_ip}:{PORT} Press ^C to quit.\n\n")
|
||||
print(f"Serving reports at https://{server_ip}:{PORT} Press ^C to quit.\n\n")
|
||||
else:
|
||||
print(f"Serving reports at http://{server_ip}:{PORT} Press ^C to quit.\n\n")
|
||||
|
||||
if not webServerLogs:
|
||||
print("Server Logs are disabled")
|
||||
|
||||
@@ -16,7 +16,7 @@ interface = meshtastic.tcp_interface.TCPInterface(hostname='127.0.0.1', noProto=
|
||||
# Create a telemetry data object
|
||||
telemetry_data = telemetry_pb2.Telemetry()
|
||||
telemetry_data.time = int(time.time())
|
||||
telemetry_data.local_stats.upTime = 0
|
||||
#telemetry_data.local_stats.upTime = 0
|
||||
telemetry_data.environment_metrics.temperature = 0
|
||||
# telemetry_data.environment_metrics.voltage = 0
|
||||
# telemetry_data.environment_metrics.current = 0
|
||||
@@ -36,8 +36,8 @@ telemetry_data.environment_metrics.temperature = 0
|
||||
# telemetry_data.environment_metrics.weight = 0
|
||||
|
||||
# Read the uptime
|
||||
with open('/proc/uptime', 'r') as uptime:
|
||||
telemetry_data.local_stats.upTime = int(float(uptime.readline().split()[0]))
|
||||
# with open('/proc/uptime', 'r') as uptime:
|
||||
# telemetry_data.local_stats.upTime = int(float(uptime.readline().split()[0]))
|
||||
|
||||
# Read the CPU temperature
|
||||
with open('/sys/class/thermal/thermal_zone0/temp', 'r') as cpu_temp:
|
||||
|
||||
Reference in New Issue
Block a user