mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f652230b0 | ||
|
|
6f1c44e62a | ||
|
|
837d049acb | ||
|
|
2463407ade | ||
|
|
af2bc7be0c | ||
|
|
38654213e8 | ||
|
|
a06819dbda | ||
|
|
9818cccbbf | ||
|
|
239dbb8be0 | ||
|
|
872a9601d0 | ||
|
|
2b6dc726e1 | ||
|
|
ef27ddff84 | ||
|
|
8a8ad961d5 | ||
|
|
a8b4362d3c | ||
|
|
dc731ae237 | ||
|
|
d0d024d770 | ||
|
|
9b633502e6 | ||
|
|
ac1a007ba4 | ||
|
|
09cf6f585c | ||
|
|
916719f1c5 | ||
|
|
11a6dc3cf0 | ||
|
|
c160678e79 | ||
|
|
0c9fd919ab | ||
|
|
e17dc79896 | ||
|
|
06d6855d92 | ||
|
|
66f937a645 | ||
|
|
f4985b744a | ||
|
|
7ae6174f96 | ||
|
|
d44fdd4462 | ||
|
|
3dd6da4684 | ||
|
|
a229b57964 | ||
|
|
5e045b6447 | ||
|
|
1e328d4f4d | ||
|
|
879d141844 | ||
|
|
7daf8c4c33 | ||
|
|
3e6d1f5c6f | ||
|
|
32deea9e3b | ||
|
|
793fabcdb8 | ||
|
|
41efbc6189 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -8,7 +8,8 @@ config.ini
|
||||
venv/
|
||||
|
||||
# logs
|
||||
logs/*.log
|
||||
logs/
|
||||
install_notes.txt
|
||||
|
||||
# modified .service files
|
||||
etc/*.service
|
||||
@@ -18,3 +19,6 @@ __pycache__/
|
||||
|
||||
# rag data
|
||||
data/rag/*
|
||||
|
||||
# qrz db
|
||||
data/qrz.db
|
||||
|
||||
14
README.md
14
README.md
@@ -44,6 +44,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
|
||||
### Fun and Games
|
||||
- **Built-in Games**: Enjoy games like DopeWars, Lemonade Stand, BlackJack, and VideoPoker.
|
||||
- **FCC ARRL QuizBot**: The exam question pool quiz-bot.
|
||||
- **Command-Based Gameplay**: Issue `games` to display help and start playing.
|
||||
|
||||
### Radio Frequency Monitoring
|
||||
@@ -53,6 +54,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
### EAS Alerts
|
||||
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
|
||||
- **NOAA EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from NOAA.
|
||||
- **USGS Volcano Alerts via API**: Use an internet-connected node to message Emergency Alerts from USGS.
|
||||
- **EAS Alerts over the air**: Utilizing external tools to report EAS alerts offline over mesh.
|
||||
- **NINA alerts for Germany**: Emergency Alerts from xrepository.de feed
|
||||
|
||||
@@ -132,6 +134,8 @@ The following settings determine how the bot responds. By default, the bot will
|
||||
respond_by_dm_only = True
|
||||
defaultChannel = 0
|
||||
ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
ignoreChannels = # ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
|
||||
cmdBang = False # require ! to be the first character in a command
|
||||
```
|
||||
|
||||
### Location Settings
|
||||
@@ -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
|
||||
|
||||
@@ -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
7226
data/hamradio/extra.json
Normal file
File diff suppressed because it is too large
Load Diff
5126
data/hamradio/general.json
Normal file
5126
data/hamradio/general.json
Normal file
File diff suppressed because it is too large
Load Diff
4934
data/hamradio/technician.json
Normal file
4934
data/hamradio/technician.json
Normal file
File diff suppressed because it is too large
Load Diff
105
mesh_bot.py
105
mesh_bot.py
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/python3
|
||||
# Meshtastic Autoresponder MESH Bot
|
||||
# K7MHI Kelly Keeton 2024
|
||||
# K7MHI Kelly Keeton 2025
|
||||
|
||||
try:
|
||||
from pubsub import pub
|
||||
@@ -15,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()
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ def bbs_sync_posts(input, peerNode, RxNode):
|
||||
messageID = 0
|
||||
|
||||
# check if the bbs link is enabled
|
||||
if bbs_link_whitelist is not None:
|
||||
if bbs_link_whitelist != ['']:
|
||||
if str(peerNode) not in bbs_link_whitelist:
|
||||
logger.warning(f"System: BBS Link is disabled for node {peerNode}.")
|
||||
return "System: BBS Link is disabled for your node."
|
||||
@@ -185,11 +185,17 @@ def bbs_sync_posts(input, peerNode, RxNode):
|
||||
return f"bbsack {messageID}"
|
||||
elif "bbsack" in input.lower():
|
||||
# increment the messageID
|
||||
ack = int(input.split(" ")[1])
|
||||
messageID = int(ack) + 1
|
||||
if len(input.split(" ")) > 1:
|
||||
try:
|
||||
messageID = int(input.split(" ")[1]) + 1
|
||||
except:
|
||||
return "link error"
|
||||
else:
|
||||
return "link error"
|
||||
|
||||
# send message with delay to keep chutil happy
|
||||
if messageID < len(bbs_messages):
|
||||
logger.debug(f"System: Sending bbslink message {messageID} to peer " + str(peerNode))
|
||||
time.sleep(5 + responseDelay)
|
||||
# every 5 messages add extra delay
|
||||
if messageID % 5 == 0:
|
||||
|
||||
@@ -17,12 +17,12 @@ def read_file(file_monitor_file_path, random_line_only=False):
|
||||
return "🐝buzz 💐buzz buzz🍯"
|
||||
if random_line_only:
|
||||
# read a random line from the file
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
with open(file_monitor_file_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
return random.choice(lines)
|
||||
else:
|
||||
# read the whole file
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
with open(file_monitor_file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
return content
|
||||
except Exception as e:
|
||||
@@ -37,7 +37,7 @@ def read_news():
|
||||
def write_news(content, append=False):
|
||||
# write the news file on demand
|
||||
try:
|
||||
with open(news_file_path, 'a' if append else 'w') as f:
|
||||
with open(news_file_path, 'a' if append else 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
logger.info(f"FileMon: Updated {news_file_path}")
|
||||
return True
|
||||
@@ -76,7 +76,7 @@ def call_external_script(message, script="script/runShell.sh"):
|
||||
logger.warning(f"FileMon: Script not found: {script_path}")
|
||||
return "sorry I can't do that"
|
||||
|
||||
output = os.popen(f"bash {script_path} {message}").read()
|
||||
output = os.popen(f"bash {script_path} {message}").read().encode('utf-8').decode('utf-8')
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error calling external script: {e}")
|
||||
|
||||
142
modules/games/hamtest.py
Normal file
142
modules/games/hamtest.py
Normal file
@@ -0,0 +1,142 @@
|
||||
# hamradio test module for meshbot DE K7MHI 2025
|
||||
# depends on the JSON question data files from https://github.com/russolsen/ham_radio_question_pool
|
||||
|
||||
# data files which are expected to be in ../../data/hamradio/ similar to the following:
|
||||
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/technician-2022-2026/technician.json
|
||||
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/general-2023-2027/general.json
|
||||
# https://raw.githubusercontent.com/russolsen/ham_radio_question_pool/refs/heads/master/extra-2024-2028/extra.json
|
||||
|
||||
import json
|
||||
import random
|
||||
import os
|
||||
from modules.log import *
|
||||
|
||||
class HamTest:
|
||||
def __init__(self):
|
||||
self.questions = {}
|
||||
self.load_questions()
|
||||
self.game = {}
|
||||
|
||||
def load_questions(self):
|
||||
for level in ['technician', 'general', 'extra']:
|
||||
try:
|
||||
with open(f'{os.path.dirname(__file__)}/../../data/hamradio/{level}.json', encoding='utf-8') as f:
|
||||
self.questions[level] = json.load(f)
|
||||
except FileNotFoundError:
|
||||
logger.error(f"File not found: ../../data/hamradio/{level}.json")
|
||||
self.questions[level] = []
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Error decoding JSON from file: ../../data/hamradio/{level}.json")
|
||||
self.questions[level] = []
|
||||
|
||||
def newGame(self, id, level='technician'):
|
||||
msg = f"📻New {level} quiz started, 'end' to exit."
|
||||
if id in self.game:
|
||||
level = self.game[id]['level']
|
||||
self.game[id] = {
|
||||
'level': level,
|
||||
'score': 0,
|
||||
'total': 0,
|
||||
'errors': [],
|
||||
'qId': None,
|
||||
'question': None,
|
||||
'answers': None,
|
||||
'correct': None
|
||||
}
|
||||
# set the pool needed for the game
|
||||
if self.game[id]['level'] == 'extra':
|
||||
self.game[id]['total'] = 50
|
||||
else:
|
||||
self.game[id]['total'] = 35
|
||||
|
||||
# randomize the questions
|
||||
random.shuffle(self.questions[level])
|
||||
|
||||
msg += f"\n{self.nextQuestion(id)}"
|
||||
return msg
|
||||
|
||||
def nextQuestion(self, id):
|
||||
level = self.game[id]['level']
|
||||
# if question has the word figure in it, skip it
|
||||
question = random.choice(self.questions[level])
|
||||
while 'figure' in question['question'].lower():
|
||||
question = random.choice(self.questions[level])
|
||||
|
||||
self.game[id]['question'] = question['question']
|
||||
self.game[id]['answers'] = question['answers']
|
||||
self.game[id]['correct'] = question['correct']
|
||||
self.game[id]['qId'] = question['id']
|
||||
self.game[id]['total'] -= 1
|
||||
|
||||
if self.game[id]['total'] == 0:
|
||||
return self.endGame(id)
|
||||
|
||||
# ask the question and return answers in A, B, C, D format
|
||||
msg = f"{self.game[id]['question']}\n"
|
||||
for i, answer in enumerate(self.game[id]['answers']):
|
||||
msg += f"{chr(65+i)}. {answer}\n"
|
||||
return msg
|
||||
|
||||
def answer(self, id, answer):
|
||||
if id not in self.game:
|
||||
return "No game in progress"
|
||||
if self.game[id]['correct'] == ord(answer.upper()) - 65:
|
||||
self.game[id]['score'] += 1
|
||||
return f"Correct👍\n" + self.nextQuestion(id)
|
||||
else:
|
||||
# record the section of the question for study aid
|
||||
section = self.game[id]['qId'][:3]
|
||||
self.game[id]['errors'].append(section)
|
||||
# provide the correct answer
|
||||
answer = [self.game[id]['correct']]
|
||||
return f"Wrong.⛔️ Correct is {chr(65+self.game[id]['correct'])}\n" + self.nextQuestion(id)
|
||||
|
||||
def getScore(self, id):
|
||||
if id not in self.game:
|
||||
return "No game in progress"
|
||||
score = self.game[id]['score']
|
||||
total = self.game[id]['total']
|
||||
level = self.game[id]['level']
|
||||
if self.game[id]['errors']:
|
||||
areaofstudy = max(set(self.game[id]['errors']), key = self.game[id]['errors'].count)
|
||||
else:
|
||||
areaofstudy = "None"
|
||||
|
||||
if level == 'extra':
|
||||
pool = 50
|
||||
else:
|
||||
pool = 35
|
||||
|
||||
return f"Score: {score}/{pool}\nQuestions left: {total}\nArea of study: {areaofstudy}"
|
||||
|
||||
def endGame(self, id):
|
||||
if id not in self.game:
|
||||
return "No game in progress"
|
||||
|
||||
score = self.game[id]['score']
|
||||
level = self.game[id]['level']
|
||||
|
||||
if level == 'extra':
|
||||
# passing score for extra is 37 out of 50
|
||||
passing = 37
|
||||
else:
|
||||
# passing score for technician and general is 26 out of 35
|
||||
passing = 26
|
||||
|
||||
if score >= passing:
|
||||
msg = f"Game over. Score: {score} 73! 🎉You passed the {level} exam."
|
||||
else:
|
||||
# find the most common section of the questions missed
|
||||
if self.game[id]['errors']:
|
||||
areaofstudy = max(set(self.game[id]['errors']), key = self.game[id]['errors'].count)
|
||||
else:
|
||||
areaofstudy = "None"
|
||||
msg = f"Game over. Score: {score} 73! 😿You did not pass the {level} exam. \nYou may want to study {areaofstudy}."
|
||||
|
||||
# remove the game[id] from the list
|
||||
del self.game[id]
|
||||
return msg
|
||||
|
||||
hamtestTracker = []
|
||||
hamtest = HamTest()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user