Compare commits

...

82 Commits

Author SHA1 Message Date
SpudGunMan
d055c35c96 Update install.sh 2025-05-11 13:07:52 -07:00
SpudGunMan
27820daaf4 Cron4W3 2025-05-10 15:34:33 -07:00
SpudGunMan
56e8e1c0d5 🐇🪵
config register set in config.ini for hop logs
2025-05-01 10:35:43 -07:00
SpudGunMan
4545b8f4a4 Update locationdata.py 2025-04-28 23:10:52 -07:00
SpudGunMan
6ed48d49ce Update videopoker.py
fix gameplay
2025-04-24 15:54:47 -07:00
SpudGunMan
a3a54b081d Update system.py 2025-04-23 19:34:13 -07:00
SpudGunMan
ab420af63e Update system.py 2025-04-23 09:24:51 -07:00
SpudGunMan
a55c61c47d Update web.py 2025-04-23 07:17:02 -07:00
SpudGunMan
7236f47eb7 Update README.md 2025-04-17 13:30:53 -07:00
SpudGunMan
05e11ae5f8 Update README.md 2025-04-13 10:04:15 -07:00
SpudGunMan
f8ffcc19b1 Update config.template 2025-04-13 10:04:13 -07:00
SpudGunMan
ea20eec604 Update README.md 2025-04-11 14:51:50 -07:00
SpudGunMan
d1204d2c26 ignoreEAS and USGS alert word
enhance with list to ignore words not wanted for broadcast
2025-04-11 14:32:51 -07:00
SpudGunMan
654d8b3ff7 Update README.md 2025-04-10 16:14:32 -07:00
SpudGunMan
3bf12d62b5 Update mesh_bot.py 2025-04-10 16:09:15 -07:00
SpudGunMan
0ec8613d27 Update install.sh
issues raised https://github.com/SpudGunMan/meshing-around/issues/139
2025-04-10 15:57:56 -07:00
SpudGunMan
10dd413ae7 Update config.template 2025-04-10 15:50:12 -07:00
SpudGunMan
09ac7525b3 Update mesh_bot.py 2025-04-10 15:48:47 -07:00
SpudGunMan
aac497dfa0 config.ini Scheduler enhancments
request from https://github.com/SpudGunMan/meshing-around/issues/141

enhances with a basic announcement from config.ini
2025-04-10 15:46:36 -07:00
SpudGunMan
6f652230b0 fixQRZ formatting
and enhance saving names without info packet
2025-04-04 12:00:09 -07:00
SpudGunMan
6f1c44e62a Update mesh_bot.py
enhance llm error
2025-04-02 19:36:12 -07:00
SpudGunMan
837d049acb Update locationdata.py 2025-03-30 14:00:14 -07:00
SpudGunMan
2463407ade Update system.py 2025-03-30 13:49:40 -07:00
SpudGunMan
af2bc7be0c enhance sysinfo 2025-03-30 13:45:22 -07:00
SpudGunMan
38654213e8 fix script run 2025-03-30 13:44:57 -07:00
SpudGunMan
a06819dbda enhance bbsack 2025-03-30 11:49:54 -07:00
SpudGunMan
9818cccbbf fix BBSLink for open mode
fix for issue raised https://github.com/SpudGunMan/meshing-around/discussions/142
2025-03-30 11:29:00 -07:00
SpudGunMan
239dbb8be0 Update config.template
typo
2025-03-28 10:46:29 -07:00
SpudGunMan
872a9601d0 Update system.py 2025-03-27 20:31:53 -07:00
SpudGunMan
2b6dc726e1 valert for USGS Volcano Data 2025-03-27 19:44:02 -07:00
SpudGunMan
ef27ddff84 Update locationdata.py 2025-03-27 19:32:54 -07:00
SpudGunMan
8a8ad961d5 USGS Alerts 2025-03-27 19:31:56 -07:00
SpudGunMan
a8b4362d3c enhance VolcanoAlert
prevent stale records from being rebroadcast
2025-03-27 18:22:13 -07:00
SpudGunMan
dc731ae237 USGS Volcano Alerts 2025-03-27 16:11:21 -07:00
SpudGunMan
d0d024d770 Update system.py 2025-03-27 09:43:36 -07:00
SpudGunMan
9b633502e6 Update mesh_bot.py 2025-03-20 12:14:26 -07:00
Kelly
ac1a007ba4 Merge pull request #140 from todd2982/patch-2
Update .gitignore
2025-03-17 16:03:41 -07:00
todd2982
09cf6f585c Update .gitignore
Ignore rotated logs, install notes, and qrz db.
2025-03-17 02:07:01 -05:00
SpudGunMan
916719f1c5 Update mesh_bot.py 2025-03-15 17:31:51 -07:00
SpudGunMan
11a6dc3cf0 UTF-8-4-Windows
Co-Authored-By: dj505 <dj505@users.noreply.github.com>
2025-03-15 17:31:44 -07:00
SpudGunMan
c160678e79 Update locationdata.py 2025-03-07 17:55:56 -08:00
SpudGunMan
0c9fd919ab Update system.py 2025-03-07 17:53:17 -08:00
SpudGunMan
e17dc79896 🐞Bugs
issue https://github.com/SpudGunMan/meshing-around/issues/138
2025-03-04 12:46:16 -08:00
SpudGunMan
06d6855d92 cmd bang
this will solve all the worlds problems
2025-02-26 20:16:53 -08:00
SpudGunMan
66f937a645 expand BBS Block
ignore node who is cantankerous from all commands
2025-02-25 19:18:56 -08:00
SpudGunMan
f4985b744a Update README.md
Co-Authored-By: mikecarper <135079168+mikecarper@users.noreply.github.com>
2025-02-23 20:33:26 -08:00
SpudGunMan
7ae6174f96 Update hamtest.py 2025-02-23 20:06:08 -08:00
SpudGunMan
d44fdd4462 Update hamtest.py 2025-02-23 20:03:12 -08:00
SpudGunMan
3dd6da4684 Update hamtest.py 2025-02-23 20:02:15 -08:00
SpudGunMan
a229b57964 Update README.md 2025-02-23 19:25:15 -08:00
SpudGunMan
5e045b6447 Update README.md 2025-02-23 19:24:52 -08:00
SpudGunMan
1e328d4f4d Update README.md 2025-02-23 19:20:21 -08:00
SpudGunMan
879d141844 Update mesh_bot.py 2025-02-23 19:14:32 -08:00
SpudGunMan
7daf8c4c33 Update README.md 2025-02-23 18:57:50 -08:00
SpudGunMan
3e6d1f5c6f Merge branch 'main' of https://github.com/SpudGunMan/meshing-around 2025-02-23 18:40:44 -08:00
SpudGunMan
32deea9e3b hamtest
a game of the FCC/ARRL Question Pools
2025-02-23 18:40:41 -08:00
Kelly
793fabcdb8 Merge pull request #136 from NomDeTom/main
change maxBuffer to 200
2025-02-23 13:41:02 -08:00
SpudGunMan
a7a710208a Update send-environment-metrics.py 2025-02-22 17:29:42 -08:00
Tom
41efbc6189 Update config.template
change maxBuffer to 200 by default, as this is the longest that the recent firmware allows.
2025-02-23 01:28:17 +00:00
SpudGunMan
f399190d3c hangman 2025-02-21 21:48:12 -08:00
SpudGunMan
5760c10534 enhanceHangmen
is it hang man or hang men.
2025-02-21 21:31:16 -08:00
SpudGunMan
9deb4a9436 Update hangman.py 2025-02-21 19:04:56 -08:00
SpudGunMan
1f348d963d Update hangman.py 2025-02-21 18:56:07 -08:00
Kelly
b35edf13c8 Merge pull request #134 from dadecoza/main
Hangman!
2025-02-21 18:54:03 -08:00
Johannes le Roux
37185b9f8b Update hangman.py 2025-02-20 23:45:16 +02:00
Johannes le Roux
4e25535ede party face 2025-02-20 23:22:53 +02:00
Johannes le Roux
4de2a36099 added hangman 2025-02-20 22:53:28 +02:00
Kelly
6c0d6fd343 Merge pull request #133 from SpudGunMan/lab
Lab Enhancments
2025-02-19 18:32:06 -08:00
SpudGunMan
abd865c918 ignoreListFema 2025-02-19 18:29:22 -08:00
SpudGunMan
82222addbe Update log.py
enhance with more windows compatibility

Co-Authored-By: dj505 <7433694+dj505@users.noreply.github.com>
2025-02-19 18:21:43 -08:00
SpudGunMan
7750ce468b Update README.md 2025-02-19 17:31:22 -08:00
SpudGunMan
135778d511 winPython
Thanks Discord dj505 request for windows support
2025-02-19 17:30:06 -08:00
SpudGunMan
c54df673c3 refactorValue 2025-02-17 19:40:30 -08:00
SpudGunMan
2fec08060f FEMAIgnore Enhancment 2025-02-17 19:37:19 -08:00
SpudGunMan
ce9af3c0d3 Update locationdata.py 2025-02-17 14:11:06 -08:00
SpudGunMan
217cd01d0a Update locationdata.py 2025-02-17 14:00:40 -08:00
SpudGunMan
8a6057995b Update locationdata.py 2025-02-17 14:00:05 -08:00
SpudGunMan
47e21dbaab Chunker Improvement
Adjusted how packets are split, ignoring .?! which can confound things. @NomDeTom
2025-02-17 10:21:38 -08:00
SpudGunMan
267f50c591 Update locationdata.py 2025-02-16 11:04:37 -08:00
SpudGunMan
0013a7bb74 Update locationdata.py 2025-02-16 11:01:47 -08:00
SpudGunMan
73fe8be432 Update locationdata.py 2025-02-16 11:00:54 -08:00
SpudGunMan
3d45195ae9 refactor NOAA forecast to the API from bScrape
I cleaned up the config.ini noaaforecastduration you may want to set yours to `noaaforecastduration = 3` not like it was before that was a goof
2025-02-16 10:53:04 -08:00
21 changed files with 18124 additions and 140 deletions

6
.gitignore vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

5126
data/hamradio/general.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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'
```

View File

@@ -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

View File

@@ -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.

View File

@@ -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:

View File

@@ -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
View 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
View 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()

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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}")

View File

@@ -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)

View File

@@ -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")

View File

@@ -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: