Compare commits

...

39 Commits

Author SHA1 Message Date
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
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
13 changed files with 17643 additions and 38 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
@@ -383,7 +387,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 | |
@@ -425,6 +430,7 @@ 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 | ✅ |
@@ -447,6 +453,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.
@@ -455,10 +462,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!
- **dj505**: trying it on windows!
- **Cisien, bitflip, **Woof**, **propstg**, **Josh** and Hailo1999**: For testing and feature ideas on Discord and GitHub.
- **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!
@@ -154,6 +158,10 @@ ignoreFEMAwords = test,exercise
# find your SAME https://www.weather.gov/nwr/counties
mySAME = 053029,053073
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
volcanoAlertBroadcastEnabled = False
volcanoAlertBroadcastCh = 2
# Use DE Alert Broadcast Data
enableDEalerts = False
# comma separated list of regional codes trigger local alert.
@@ -255,6 +263,7 @@ videopoker = True
mastermind = True
golfsim = True
hangman = True
hamtest = True
[messagingSettings]
# delay in seconds for response to avoid message collision
@@ -266,6 +275,6 @@ 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

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

@@ -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,7 +15,7 @@ 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", "hangman"]
restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind", "hangman", "hamtest"]
restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
# Global Variables
@@ -57,6 +57,7 @@ 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),
@@ -84,6 +85,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),
@@ -114,6 +116,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)})
@@ -687,6 +693,41 @@ def handleHangman(message, nodeID, deviceID):
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()
@@ -815,8 +856,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)
@@ -1012,6 +1059,7 @@ def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
(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]
@@ -1036,6 +1084,7 @@ def onReceive(packet, interface):
replyIDset = False
emojiSeen = False
isDM = False
playingGame = False
if DEBUGpacket:
# Debug print the interface object
@@ -1228,11 +1277,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:
@@ -1286,14 +1341,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)
@@ -1316,8 +1375,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")
@@ -1369,12 +1429,16 @@ 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")
@@ -1392,6 +1456,12 @@ async def start_rx():
# 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))
@@ -1413,6 +1483,7 @@ 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()

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

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,6 +234,7 @@ 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)
weather_api = "https://api.weather.gov/points/" + str(lat) + "," + str(lon)
@@ -513,7 +514,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]
@@ -616,3 +616,43 @@ 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
for alert in volcano_json:
# 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
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

@@ -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
@@ -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
@@ -261,6 +262,8 @@ try:
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
# bbs
bbs_enabled = config['bbs'].getboolean('enabled', False)
@@ -332,6 +335,7 @@ try:
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

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:
@@ -157,6 +164,11 @@ if hangman_enabled:
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"
@@ -179,6 +191,8 @@ if games_enabled is True:
gamesCmdList += "golfSim, "
if hangman_enabled:
gamesCmdList += "hangman, "
if hamtest_enabled:
gamesCmdList += "hamTest, "
gamesCmdList = gamesCmdList[:-2] # remove the last comma
else:
gamesCmdList = ""
@@ -652,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)):
@@ -707,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()
@@ -736,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
@@ -768,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:
@@ -779,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 volcanoAlert != NO_ALERTS and volcanoAlert != ERROR_FETCHING_DATA:
# 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
@@ -1193,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)