Compare commits

...

50 Commits

Author SHA1 Message Date
Kelly
eeab9f3fb1 Merge pull request #86 from SpudGunMan/lab
Enhancements 🦃
2024-11-27 23:01:45 -08:00
SpudGunMan
c21a67d1cf Update README.md 2024-11-27 22:58:47 -08:00
SpudGunMan
afe48a44da fixEAS Multi Channel 2024-11-27 22:50:30 -08:00
SpudGunMan
7e4822e4ec Update locationdata.py 2024-11-27 22:22:19 -08:00
SpudGunMan
705ab6a980 fixClosesNodex2 2024-11-27 21:31:52 -08:00
SpudGunMan
963b29eea4 fixEnhanceAutoPing 2024-11-27 21:20:08 -08:00
SpudGunMan
b3f889c4c7 fixBugClosestNodes 2024-11-27 20:40:08 -08:00
SpudGunMan
545b4891b4 error2Warning 2024-11-27 20:38:38 -08:00
SpudGunMan
c89f14b3c2 fix that needed for later 2024-11-25 11:51:25 -08:00
SpudGunMan
c416b00383 newLogRotation 2024-11-25 11:49:39 -08:00
SpudGunMan
669a891eeb Update log.py 2024-11-25 11:46:22 -08:00
SpudGunMan
520d58b262 Update log.py 2024-11-25 11:35:36 -08:00
SpudGunMan
24dff868ff RotateLogger
default is 32 days of logs configure if needed otherwise.
2024-11-24 19:52:25 -08:00
SpudGunMan
cf45bb5060 LOG Rotation Update
update log handler
2024-11-24 19:43:47 -08:00
SpudGunMan
0f9064f2c3 EAS API Alerts
Enable EAS API Messages to Mesh
Fix multiping Device 2
2024-11-23 16:27:55 -08:00
SpudGunMan
f94f329b1f Create eas_alert_parser.py 2024-11-23 15:50:04 -08:00
SpudGunMan
dc4560081d dropLangChain 2024-11-23 15:49:27 -08:00
SpudGunMan
b42cd0e6dc Update README.md 2024-11-20 16:06:28 -08:00
SpudGunMan
bbe1e45541 Update README.md 2024-11-20 16:03:22 -08:00
SpudGunMan
2c61db1215 fix that enhance 2024-11-19 19:52:04 -08:00
SpudGunMan
fde2bb94d9 enhance 2024-11-19 19:51:40 -08:00
SpudGunMan
436a43d3ad Update README.md 2024-11-19 19:49:46 -08:00
SpudGunMan
6b2a6f3a83 enhanceFileMon 2024-11-19 19:44:49 -08:00
SpudGunMan
8e5773115c FileMon
Enhancement with FileMon to watch a file and deliver its goods to the mesh
2024-11-19 19:41:14 -08:00
SpudGunMan
626a5dfe16 moveAPItide 2024-11-15 16:21:56 -08:00
SpudGunMan
e63f4816c4 Update locationdata.py 2024-11-15 14:16:35 -08:00
SpudGunMan
13852b194b enhance 2024-11-11 15:11:25 -08:00
SpudGunMan
a68c20098b ScrubUno
gone but not forgotten
2024-11-11 14:41:14 -08:00
Kelly
432b5a767e Merge pull request #85 from SpudGunMan/lab
BBS LiNK
2024-11-07 15:01:54 -08:00
SpudGunMan
952659198c fixes 2024-11-05 07:45:19 -08:00
SpudGunMan
4e518758e5 Update bbstools.py 2024-10-31 16:56:43 -07:00
SpudGunMan
e1b3dd311f Update bbstools.py 2024-10-31 16:42:09 -07:00
SpudGunMan
bb0f923155 bbslink
first attempt at giving BBS link over the air
2024-10-31 16:38:16 -07:00
SpudGunMan
ab86f02bd7 Update llm.py 2024-10-27 17:12:28 -07:00
SpudGunMan
43067cfb07 Update llm.py 2024-10-27 16:58:05 -07:00
SpudGunMan
3300694059 Update mesh_bot.py 2024-10-26 18:54:48 -07:00
SpudGunMan
f59b8715dd Update simulator.py 2024-10-26 17:31:05 -07:00
SpudGunMan
60abadd1fc Update llm.py 2024-10-24 19:49:01 -07:00
SpudGunMan
4297c91c5e Update llm.py 2024-10-24 19:47:19 -07:00
SpudGunMan
c8eddc3787 Update llm.py 2024-10-24 19:44:51 -07:00
SpudGunMan
d01d81a6d7 openWebUI 2024-10-24 19:21:23 -07:00
SpudGunMan
40b31fd8af Update dopewar.py 2024-10-23 22:13:48 -07:00
SpudGunMan
7b995b35cd 💊
cleanup display on some things
2024-10-23 22:07:46 -07:00
SpudGunMan
00885d57c9 Update dopewar.py 2024-10-23 21:21:44 -07:00
SpudGunMan
d03d7dbc47 Update llm.py 2024-10-21 19:36:02 -07:00
SpudGunMan
7fd4074bd3 Update videopoker.py 2024-10-20 22:56:58 -07:00
SpudGunMan
8367bca4d5 Update joke.py 2024-10-20 17:42:00 -07:00
SpudGunMan
5059990adb Update mesh_bot.py 2024-10-20 17:24:22 -07:00
SpudGunMan
9dd9d39df4 Update llm.py 2024-10-19 08:14:23 -07:00
SpudGunMan
87f89fea6d Update llm.py 2024-10-18 10:50:29 -07:00
20 changed files with 454 additions and 345 deletions

View File

@@ -19,6 +19,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **Scheduler**: Schedule messages like weather updates or reminders for weekly VHF nets.
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
- **BBS Linking**: Combine multiple bots to expand BBS reach
### Interactive AI and Data Lookup
- **NOAA location Data**: Get localized weather(alerts) and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
@@ -36,6 +37,13 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
- **SNR RF Activity Alerts**: Monitor a radio frequency and get alerts when high SNR RF activity is detected.
- **Hamlib Integration**: Use Hamlib (rigctld) to watch the S meter on a connected radio.
### NOAA EAS Alerts
- **EAS Alerts via NOAA API**: Use an internet connected node to message Emergency Alerts from NOAA
- **EAS Alerts over the air**: Utalizing external tools to report EAS alerts offline over mesh
### File Monitor Alerts
- **File Mon**: Monitor a flat/text file for changes, brodcast the contents of the message to mesh channel.
### Data Reporting
- **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md).
@@ -118,7 +126,7 @@ defaultChannel = 0
```
### Location Settings
The weather forecasting defaults to NOAA, but for locations outside the USA, you can set `UseMeteoWxAPI` "Go to definition") to `True` to use a global weather API. The `lat` and `lon` are default values when a node has no location data. It is also the default used for Sentry.
The weather forecasting defaults to NOAA, for locations outside the USA, you can set `UseMeteoWxAPI` to `True`, to use a global weather API. The `lat` and `lon` are default values when a node has no location data. It is also the default used for Sentry.
```ini
[location]
@@ -202,6 +210,41 @@ llmContext_fromGoogle = True # enable context from google search results helps w
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
```
### File Monitoring
Some dev notes for ideas of use
```ini
[fileMon]
enabled = True
file_path = alert.txt
broadcastCh = 2,4
```
#### NOAA EAS
To Alert on Mesh with the NOAA EAS API you can set the channels and enable, checks every 30min
```ini
# EAS Alert Broadcast
wxAlertBroadcastEnabled = False
# EAS Alert Broadcast Channels
wxAlertBroadcastCh = 2,4
```
To Monitor EAS with no internet connection see the following notes
- [EAS2Text](https://github.com/A-c0rN/EAS2Text)
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng) or [direwolf](https://github.com/wb2osz/direwolf)
- [dsame3](https://github.com/jamieden/dsame3) // recomend not using anything but the sample file for basic work
- this can be used with a rtl-sdr to capture alerts
- has a sample .ogg file for testing alerts
The following example shell command can pipe the data using [etc/eas_alert_parser.py](etc/eas_alert_parser.py) to alert.txt
```bash
sox -t ogg WXR-RWT.ogg -esigned-integer -b16 -r 22050 -t raw - | multimon-ng -a EAS -v 1 -t raw - | python eas_alert_parser.py
```
The following example shell command will pipe rtl_sdr to alert.txt
```bash
rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas_alert_parser.py
```
### Scheduler
The Scheduler is enabled in the `settings.py` by setting `scheduler_enabled = True`. The actions and settings are via code only at this time. See mesh_bot.py around line [425](https://github.com/SpudGunMan/meshing-around/blob/22983133ee4db3df34f66699f565e506de296197/mesh_bot.py#L425-L435) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more.
@@ -213,6 +256,13 @@ schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'),
schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
```
#### BBS Link
The scheduler also handles the BBL Link Brodcast message
```python
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 8 on device 1
schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 8, 0, 1))
```
### MQTT Notes
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. There also seems to be a quicker way to enable MQTT by having your bot node with the enabled [serial](https://meshtastic.org/docs/configuration/module/serial/) module with echo enabled and MQTT uplink and downlink. These two methods have been mentioned as allowing MQTT routing for the project.
@@ -250,8 +300,6 @@ For the Ollama LLM:
```sh
pip install ollama
pip install langchain
pip install langchain-ollama
pip install googlesearch-python
```
@@ -294,6 +342,7 @@ sudo apt-get install fonts-noto-color-emoji
| `bbspost` | Posts a message to the public board or sends a DM(Mail) Examples: `bbspost $subject #message`, `bbspost @nodeNumber #message`, `bbspost @nodeShortName #message` | ✅ |
| `bbsdelete` | Deletes a message. Example: `bbsdelete #4` | ✅ |
| `bbsinfo` | Provides stats on BBS delivery and messages (sysop) | ✅ |
| `bbllink` | Links Bulletin Messages between BBS Systems | ✅ |
### Data Lookup
| Command | Description | |
@@ -314,7 +363,6 @@ sudo apt-get install fonts-noto-color-emoji
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
| `mastermind` | Plays the classic code-breaking game | ✅ |
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
| `uno` | Plays Uno card game against the bot or with others on the mesh near you! | ✅ |
# Recognition
@@ -343,5 +391,3 @@ I used ideas and snippets from other responder bots and want to call them out!
### Tools
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)

View File

@@ -61,6 +61,8 @@ urlTimeout = 10
LogMessagesToFile = False
# Logging of system messages to file
SyslogToFile = True
# Number of log files to keep in days, 0 to keep all
log_backup_count = 32
[games]
# if hop limit for the user exceeds this value, the message will be dropped
@@ -72,7 +74,6 @@ blackjack = True
videopoker = True
mastermind = True
golfsim = True
uno = True
[sentry]
# detect anyone close to the bot
@@ -108,6 +109,10 @@ UseMeteoWxAPI = False
useMetric = False
# repeaterList lookup location (rbook / artsci)
repeaterLookup = rbook
# EAS Alert Broadcast
wxAlertBroadcastEnabled = False
# EAS Alert Broadcast Channels
wxAlertBroadcastCh = 2
# repeater module
[repeater]
@@ -132,6 +137,11 @@ signalHoldTime = 10
signalCooldown = 5
signalCycleLimit = 5
[fileMon]
enabled = False
file_path = alert.txt
broadcastCh = 2
[messagingSettings]
# delay in seconds for response to avoid message collision
responseDelay = 0.7

40
etc/eas_alert_parser.py Normal file
View File

@@ -0,0 +1,40 @@
# Super sloppy multimon-ng output cleaner for processing by EAS2Text
# I maed dis, sorta, mostly just mashed code I found or that chatGPT hallucinated
# by Mike O'Connell/skrrt, no licence or whatever just be chill yo
# enhanced by sheer.cold
import re
from EAS2Text import EAS2Text
buff=[] # store messages for writing
seen=set()
pattern = re.compile(r'ZCZC.*?NWS-')
while True:
try:
# Handle piped input
line=input().strip()
except EOFError:
break
# only want EAS lines
if line.startswith("EAS:") or line.startswith("EAS (part):"):
content=line.split(maxsplit=1)[1]
if content=="NNNN": # end of EAS message
# write if we have something
if buff:
print("writing")
with open("alert.txt","w") as fh:
fh.write('\n'.join(buff))
# prepare for new data
buff.clear()
seen.clear()
elif content in seen:
# don't need repeats'
continue
else:
# check for national weather service
match=pattern.search(content)
if match:
seen.add(content)
msg=EAS2Text(content).EASText
print("got message", msg)
buff.append(msg)

View File

@@ -372,7 +372,7 @@ def get_database_info():
elif 'bbsdm' in file:
bbsdm = pickle.load(f)
except Exception as e:
print(f"Error reading database file: {str(e)}")
print(f"Warning issue reading database file: {str(e)}")
if 'lemonstand' in file:
lemon_score = "no data"
elif 'dopewar' in file:
@@ -922,8 +922,8 @@ def generate_database_html(database_info):
def main():
log_dir = LOG_PATH
today = datetime.now().strftime('%Y_%m_%d')
log_file = f'meshbot{today}.log'
today = datetime.now().strftime('%Y-%m-%d')
log_file = f'meshbot.log'
log_path = os.path.join(log_dir, log_file)
if not os.path.exists(log_path):

View File

@@ -381,7 +381,7 @@ def get_database_info():
elif 'bbsdm' in file:
bbsdm = pickle.load(f)
except Exception as e:
print(f"Error reading database file: {str(e)}")
print(f"Warning issue reading database file: {str(e)}")
if 'lemonstand' in file:
lemon_score = "no data"
elif 'dopewar' in file:
@@ -1217,8 +1217,8 @@ def generate_database_html(database_info):
def main():
# Log file
log_dir = LOG_PATH
today = datetime.now().strftime('%Y_%m_%d')
log_file = f'meshbot{today}.log'
today = datetime.now().strftime('%Y-%m-%d')
log_file = f'meshbot.log'
log_path = os.path.join(log_dir, log_file)
if not os.path.exists(log_path):

View File

@@ -25,7 +25,7 @@ def get_name_from_number(nodeID, length='short', interface=1):
# # Function to handle, or the project in test
def example_handler(nodeID, message):
def example_handler(message, nodeID, deviceID):
readableTime = time.ctime(time.time())
msg = "Hello World! "
msg += f" You are Node ID: {nodeID} "

View File

@@ -14,6 +14,8 @@ Logging messages to disk or 'Syslog' to disk uses the python native logging func
LogMessagesToFile = False
# Logging of system messages to file, needed for reporting engine
SyslogToFile = True
# Number of log files to keep in days, 0 to keep all
log_backup_count = 32
```
To change the stdout (what you see on the console) logging level (default is DEBUG) see the following example, line is in [../modules/log.py](../modules/log.py)

View File

@@ -10,7 +10,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", "uno"]
restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind"]
restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
# Global Variables
@@ -25,19 +25,21 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
# Command List
default_commands = {
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
"askai": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
"bbslink": lambda: bbs_sync_posts(message, message_from_id, deviceID),
"bbsdelete": lambda: handle_bbsdelete(message, message_from_id),
"bbshelp": bbs_help,
"bbsinfo": lambda: get_bbs_stats(),
"bbslink": lambda: bbs_sync_posts(message, message_from_id, deviceID),
"bbslist": bbs_list_messages,
"bbspost": lambda: handle_bbspost(message, message_from_id, deviceID),
"bbsread": lambda: handle_bbsread(message),
"blackjack": lambda: handleBlackJack(message, message_from_id, deviceID),
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"cmd": lambda: help_message,
"dopewars": lambda: handleDopeWars(message, message_from_id, deviceID),
"games": lambda: gamesCmdList,
@@ -52,16 +54,15 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
"messages": lambda: handle_messages(message, deviceID, channel_number, msg_history, publicChannel, isDM),
"moon": lambda: handle_moon(message_from_id, deviceID, channel_number),
"motd": lambda: handle_motd(message, message_from_id, isDM),
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"playuno": lambda: handleUno(message, message_from_id, deviceID),
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"pong": lambda: "🏓PING!!🛜",
"rlist": lambda: handle_repeaterQuery(message_from_id, deviceID, channel_number),
"sitrep": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
"solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
"videopoker": lambda: handleVideoPoker(message, message_from_id, deviceID),
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
@@ -109,7 +110,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
time.sleep(responseDelay)
return bot_response
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM):
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
global multiPing
if "?" in message and isDM:
return message.split("?")[0].title() + " command returns SNR and RSSI, or hopcount from your message. Try adding e.g. @place or #tag"
@@ -160,13 +161,15 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM):
msg = "🛑 auto-ping"
try:
pingCount = int(message.split(" ")[1])
if pingCount == 123 or pingCount == 1234:
pingCount = 1
if pingCount > 51:
pingCount = 50
except:
pingCount = -1
if pingCount > 1:
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID})
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number})
msg = f"🚦Initalizing {pingCount} auto-ping"
return msg
@@ -213,8 +216,9 @@ def handle_wxalert(message_from_id, deviceID, message):
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]), str(location[1]))
else:
weatherAlert = getWeatherAlerts(str(location[0]), str(location[1]))
weatherAlert = weatherAlert[0]
if NO_ALERTS not in weatherAlert:
weatherAlert = weatherAlert[0]
return weatherAlert
def handle_wiki(message, isDM):
@@ -545,29 +549,6 @@ def handleGolf(message, nodeID, deviceID):
time.sleep(responseDelay + 1)
return msg
def handleUno(message, nodeID, deviceID):
global unoTracker
msg = ''
# get player's last command from tracker if not new player
last_cmd = ""
for i in range(len(unoTracker)):
if unoTracker[i]['nodeID'] == nodeID:
last_cmd = unoTracker[i]['cmd']
logger.debug(f"System: {nodeID} PlayingGame uno last_cmd: {last_cmd}")
if last_cmd == "" and nodeID != 0:
# create new player
logger.debug("System: Uno: New Player: " + str(nodeID) + " " + get_name_from_number(nodeID))
unoTracker.append({'nodeID': nodeID, 'last_played': time.time(), 'cmd': '', 'playerName': get_name_from_number(nodeID)})
msg = "Welcome to 🃏 Uno!, waiting for others to join, (S)tart when ready"
msg += playUno(nodeID, message=message)
# wait a second to keep from message collision
time.sleep(responseDelay + 1)
return msg
def handle_wxc(message_from_id, deviceID, cmd):
location = get_node_location(message_from_id, deviceID)
if use_meteo_wxApi and not "wxc" in cmd and not use_metric:
@@ -798,7 +779,6 @@ def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
(jackTracker, "BlackJack", handleBlackJack),
(mindTracker, "MasterMind", handleMmind),
(golfTracker, "GolfSim", handleGolf),
(unoTracker, "Uno", handleUno)
]
for tracker, game_name, handle_game_func in trackers:
@@ -919,7 +899,7 @@ def onReceive(packet, interface):
# message is DM to us
isDM = True
# check if the message contains a trap word, DMs are always responded to
if messageTrap(message_string):
if (messageTrap(message_string) and not llm_enabled) or messageTrap(message_string.split()[0]):
# log the message to the message log
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
@@ -1057,6 +1037,10 @@ async def start_rx():
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
if radio_detection_enabled:
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
if file_monitor_enabled:
logger.debug(f"System: File Monitor Enabled for {file_monitor_file_path}, broadcasting to channels: {file_monitor_broadcastCh}")
if wxAlertBroadcastEnabled:
logger.debug(f"System: Weather Alert Broadcast Enabled on channels {wxAlertBroadcastChannel}")
if scheduler_enabled:
# Examples of using the scheduler, Times here are in 24hr format
# https://schedule.readthedocs.io/en/stable/
@@ -1081,6 +1065,9 @@ async def start_rx():
# Send the MOTD every day at 13:00 using send_message function to channel 2 on device 1
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, 2, 0, 1))
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 0 on device 1
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 0, 0, 1))
#
logger.debug("System: Starting the broadcast scheduler")
@@ -1095,11 +1082,15 @@ async def start_rx():
async def main():
meshRxTask = asyncio.create_task(start_rx())
watchdogTask = asyncio.create_task(watchdog())
if file_monitor_enabled:
fileMonTask: asyncio.Task = asyncio.create_task(handleFileWatcher())
if radio_detection_enabled:
hamlibTask = asyncio.create_task(handleSignalWatcher())
await asyncio.wait([meshRxTask, watchdogTask, hamlibTask])
else:
await asyncio.wait([meshRxTask, watchdogTask])
await asyncio.gather(meshRxTask, watchdogTask)
await asyncio.gather(hamlibTask)
await asyncio.gather(fileMonTask)
await asyncio.sleep(0.01)
try:

View File

@@ -4,7 +4,7 @@
import pickle # pip install pickle
from modules.log import *
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo")
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
# global message list, later we will use a pickle on disk
bbs_messages = []
@@ -77,6 +77,12 @@ def bbs_post_message(subject, message, fromNode):
if str(fromNode) in bbs_ban_list:
logger.warning(f"System: Naughty node {fromNode}, tried to post a message: {subject}, {message} and was dropped.")
return "Message posted. ID is: " + str(messageID)
# validate not a duplicate message
for msg in bbs_messages:
if msg[1].strip().lower() == subject.strip().lower() and msg[2].strip().lower() == message.strip().lower():
messageID = msg[0]
return "Message posted. ID is: " + str(messageID)
# append the message to the list
bbs_messages.append([messageID, subject, message, fromNode])
@@ -156,6 +162,29 @@ def bbs_delete_dm(toNode, message):
return "System: cleared mail for" + str(toNode)
return "System: No DM found for node " + str(toNode)
def bbs_sync_posts(input, peerNode, RxNode):
messageID = 0
# respond when another bot asks for the bbs posts to sync
if "bbslink" in input.lower():
if "$" in input and "#" in input:
#store the message
subject = input.split("$")[1].split("#")[0]
body = input.split("#")[1]
bbs_post_message(subject, body, peerNode)
messageID = input.split(" ")[1]
return f"bbsack {messageID}"
elif "bbsack" in input.lower():
# increment the messageID
ack = int(input.split(" ")[1])
messageID = int(ack) + 1
# send message
if messageID < len(bbs_messages):
return f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]}"
else:
logger.debug("System: bbslink sync complete with peer " + str(peerNode))
#initialize the bbsdb's
load_bbsdb()
load_bbsdm()

32
modules/filemon.py Normal file
View File

@@ -0,0 +1,32 @@
# File monitor module for the meshing-around bot
# 2024 Kelly Keeton K7MHI
from modules.log import *
import asyncio
import os
async def watch_file():
def read_file(file_monitor_file_path):
try:
with open(file_monitor_file_path, 'r') as f:
content = f.read()
return content
except Exception as e:
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
return None
if not os.path.exists(file_monitor_file_path):
return None
else:
last_modified_time = os.path.getmtime(file_monitor_file_path)
while True:
current_modified_time = os.path.getmtime(file_monitor_file_path)
if current_modified_time != last_modified_time:
# File has been modified
content = read_file(file_monitor_file_path)
last_modified_time = current_modified_time
# Cleanup the content
content = content.replace('\n', ' ').replace('\r', '').strip()
if content:
return content
await asyncio.sleep(1) # Check every

View File

@@ -159,7 +159,7 @@ def get_found_items(nodeID):
if dwInventoryDb[i].get('userID') == nodeID:
dwInventoryDb[i]['inventory'] = inventory
dwInventoryDb[i]['amount'] = amount
msg = "💊You found " + str(qty) + " of " + my_drugs[found]
msg = "💊You found " + str(qty) + " of " + str(my_drugs[found])
else:
# rolls to see how much cash the user finds
cash_found = random.randint(1, 977)
@@ -232,8 +232,9 @@ def buy_func(nodeID, price_list, choice=0, value='0'):
else:
if drug_choice in range(1, len(my_drugs) + 1):
drug_choice = drug_choice - 1
cost = price_list[drug_choice]
msg = my_drugs[drug_choice].name + ": you have🎒 " + str(amount[drug_choice]) + " "
msg += " The going price is: $" + "{:,}".format(price_list[drug_choice]) + " "
msg += " The going price is: $" + "{:,}".format(cost) + " "
buy_amount = value
if buy_amount == 'm':
@@ -315,15 +316,17 @@ def sell_func(nodeID, price_list, choice=0, value='0'):
else:
if drug_choice in range(1, len(my_drugs) + 1) and amount[drug_choice - 1] > 0:
drug_choice = drug_choice - 1
cost = price_list[drug_choice]
msg = my_drugs[drug_choice].name + ": you have " + str(amount[drug_choice]) +\
" The going price is: $" + str(price_list[drug_choice])
" The going price is: $" + str("{:,}".format(cost))
# check if the user has enough of the drug to sell
if sell_amount <= amount[drug_choice]:
amount[drug_choice] -= sell_amount
cash += sell_amount * price_list[drug_choice]
inventory -= sell_amount
msg += " You sold " + str(sell_amount) + " " + my_drugs[drug_choice].name + ' for $' +\
str(sell_amount * price_list[drug_choice]) + '. Total cash: $' + "{:,}".format(cash)
profit = sell_amount * price_list[drug_choice]
msg += " You sold " + str(sell_amount) + " " + my_drugs[drug_choice].name +\
' for $' + "{:,}".format(profit) + '. Total cash: $' + "{:,}".format(cash)
else:
msg = "You don't have that much"
return msg
@@ -392,7 +395,7 @@ def endGameDw(nodeID):
dwHighScore = ({'userID': nodeID, 'cash': round(cash, 2)})
with open('data/dopewar_hs.pkl', 'wb') as file:
pickle.dump(dwHighScore, file)
msg = "You finished with $" + str(cash) + " and beat the high score!🎉💰"
msg = "You finished with $" + "{:,}".format(cash) + " and beat the high score!🎉💰"
return msg
if cash > starting_cash:
msg = 'You made money! 💵 Up ' + str((cash/starting_cash).__round__()) + 'x! Well done.'
@@ -601,9 +604,9 @@ def playDopeWars(nodeID, cmd):
sell = sell_func(nodeID, price_list, i, 'm')
# ignore starts with "You don't have any"
if not sell.startswith("You don't have any"):
msg += sell
if i != len(my_drugs):
msg += '\n'
msg += sell + '\n'
# trim the last newline
msg = msg[:-1]
return msg
elif 'f' in menu_choice:
# set last command to location
@@ -614,7 +617,7 @@ def playDopeWars(nodeID, cmd):
elif 'p' in menu_choice:
# render_game_screen
msg = render_game_screen(nodeID, game_day, total_days, loc_choice, -1, price_list, 0)
msg = render_game_screen(nodeID, game_day, total_days, loc_choice, -1, price_list, 0, 'nothing')
return msg
elif 'e' in menu_choice:
msg = endGameDw(nodeID)

View File

@@ -78,14 +78,14 @@ def tableOfContents():
'whale': '🐋', 'dolphin': '🐬', 'fish': '🐟', 'blowfish': '🐡', 'shark': '🦈', 'octopus': '🐙', 'shell': '🐚', 'crab': '🦀', 'lobster': '🦞', 'shrimp': '🦐', 'squid': '🦑', 'snail': '🐌', 'butterfly': '🦋',
'bee': '🐝', 'beetle': '🐞', 'ant': '🐜', 'cricket': '🦗', 'spider': '🕷️', 'scorpion': '🦂', 'mosquito': '🦟', 'microbe': '🦠', 'locomotive': '🚂', 'arm': '💪', 'leg': '🦵', 'sponge': '🧽',
'toothbrush': '🪥', 'broom': '🧹', 'basket': '🧺', 'roll of paper': '🧻', 'bucket': '🪣', 'soap': '🧼', 'toilet paper': '🧻', 'shower': '🚿', 'bathtub': '🛁', 'razor': '🪒', 'lotion': '🧴',
'letter': '✉️', 'envelope': '✉️', 'mail': '📬', 'post': '📮', 'golf': '⛳️', 'golfing': '⛳️', 'office': '🏢', 'work': '💼', 'meeting': '📅', 'presentation': '📊', 'report': '📄', 'document': '📄',
'letter': '✉️', 'envelope': '✉️', 'mail': '📬', 'post': '📮', 'golf': '⛳️', 'golfing': '⛳️', 'office': '🏢', 'meeting': '📅', 'presentation': '📊', 'report': '📄', 'document': '📄',
'file': '📁', 'folder': '📂', 'sports': '🏅', 'athlete': '🏃', 'competition': '🏆', 'race': '🏁', 'tournament': '🏆', 'champion': '🏆', 'medal': '🏅', 'victory': '🏆', 'win': '🏆', 'lose': '😞',
'draw': '🤝', 'team': '👥', 'player': '👤', 'coach': '👨‍🏫', 'referee': '🧑‍⚖️', 'stadium': '🏟️', 'arena': '🏟️', 'field': '🏟️', 'court': '🏟️', 'track': '🏟️', 'gym': '🏋️', 'fitness': '🏋️', 'exercise': '🏋️',
'workout': '🏋️', 'training': '🏋️', 'practice': '🏋️', 'game': '🎮', 'match': '🎮', 'score': '🏅', 'goal': '🥅', 'point': '🏅', 'basket': '🏀', 'home run': '⚾️', 'strike': '🎳', 'spare': '🎳', 'frame': '🎳',
'inning': '⚾️', 'quarter': '🏈', 'half': '🏈', 'overtime': '🏈', 'penalty': '⚽️', 'foul': '⚽️', 'timeout': '⏱️', 'substitute': '🔄', 'bench': '🪑', 'sideline': '🏟️', 'dugout': '⚾️', 'locker room': '🚪', 'shower': '🚿',
'uniform': '👕', 'jersey': '👕', 'cleats': '👟', 'helmet': '⛑️', 'pads': '🛡️', 'gloves': '🧤', 'bat': '⚾️', 'ball': '⚽️', 'puck': '🏒', 'stick': '🏒', 'net': '🥅', 'hoop': '🏀', 'goalpost': '🥅', 'whistle': '🔔',
'scoreboard': '📊', 'fans': '👥', 'crowd': '👥', 'cheer': '📣', 'boo': '😠', 'applause': '👏', 'celebration': '🎉', 'parade': '🎉', 'trophy': '🏆', 'medal': '🏅', 'ribbon': '🎀', 'cup': '🏆', 'championship': '🏆',
'league': '🏆', 'season': '🏆', 'playoffs': '🏆', 'finals': '🏆', 'champion': '🏆', 'runner-up': '🥈', 'third place': '🥉'
'league': '🏆', 'season': '🏆', 'playoffs': '🏆', 'finals': '🏆', 'champion': '🏆', 'runner-up': '🥈', 'third place': '🥉', 'snowman': '☃️', 'snowmen': '⛄️'
}
return wordToEmojiMap

View File

@@ -1,200 +0,0 @@
# https://github.com/melvin-02/UNO-game
# Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
import random
import time
from modules.log import *
color = ('RED', 'GREEN', 'BLUE', 'YELLOW')
rank = ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Skip', 'Reverse', 'Draw2', 'Draw4', 'Wild')
ctype = {'0': 'number', '1': 'number', '2': 'number', '3': 'number', '4': 'number', '5': 'number', '6': 'number',
'7': 'number', '8': 'number', '9': 'number', 'Skip': 'action', 'Reverse': 'action', 'Draw2': 'action',
'Draw4': 'action_nocolor', 'Wild': 'action_nocolor'}
# Player List
unoLobby = []
unoTracker = [{'nodeID': 0, 'last_played': time.time(), 'cmd': '', 'playerName': ''}]
unoGameTable = {'turn': -1, 'direction': 1, 'deck': None, 'hands': None, 'top_card': None}
class Card:
def __init__(self, color, rank):
self.rank = rank
if ctype[rank] == 'number':
self.color = color
self.cardtype = 'number'
elif ctype[rank] == 'action':
self.color = color
self.cardtype = 'action'
else:
self.color = None
self.cardtype = 'action_nocolor'
def __str__(self):
if self.color is None:
return self.rank
else:
return self.color + " " + self.rank
class Deck:
def __init__(self):
self.deck = []
self.discard_pile = []
for clr in color:
for ran in rank:
if ctype[ran] != 'action_nocolor':
self.deck.append(Card(clr, ran))
self.deck.append(Card(clr, ran))
else:
self.deck.append(Card(clr, ran))
def __str__(self):
deck_comp = ''
for card in self.deck:
deck_comp += '\n' + card.__str__()
return 'The deck has ' + deck_comp
def shuffle(self):
random.shuffle(self.deck)
def deal(self):
if not self.deck:
self.reshuffle_discard_pile()
return self.deck.pop()
def reshuffle_discard_pile(self):
if len(self.discard_pile) > 1:
top_card = self.discard_pile.pop()
self.deck = self.discard_pile[:]
self.discard_pile = [top_card]
self.shuffle()
else:
raise IndexError("No cards left to reshuffle")
class Hand:
def __init__(self):
self.cards = []
self.cardsstr = []
self.number_cards = 0
self.action_cards = 0
def add_card(self, card):
self.cards.append(card)
self.cardsstr.append(str(card))
if card.cardtype == 'number':
self.number_cards += 1
else:
self.action_cards += 1
self.sort_cards()
def remove_card(self, place):
self.cardsstr.pop(place - 1)
return self.cards.pop(place - 1)
def cards_in_hand(self):
msg = ''
for i in range(len(self.cardsstr)):
msg += f' {i + 1}.{self.cardsstr[i]}'
return msg
def single_card(self, place):
return self.cards[place - 1]
def no_of_cards(self):
return len(self.cards)
def sort_cards(self):
self.cards.sort(key=lambda card: (
card.color if card.color is not None else '',
int(card.rank) if card.cardtype == 'number' and card.rank is not None else 0))
self.cardsstr = [str(card) for card in self.cards]
def choose_first():
global unoLobby
if unoLobby != []:
random_player = random.choice(unoLobby)
return random_player
else:
return None
def single_card_check(top_card, card):
if card.color == top_card.color or top_card.rank == card.rank or card.cardtype == 'action_nocolor':
return True
else:
return False
def full_hand_check(hand, top_card):
for c in hand.cards:
if c.color == top_card.color or c.rank == top_card.rank or c.cardtype == 'action_nocolor':
#return hand.remove_card(hand.cardsstr.index(str(c)) + 1)
return hand.remove_card(hand.cards.index(c) + 1)
else:
return 'no card'
def win_check(hand):
if len(hand.cards) == 0:
return True
else:
return False
def last_card_check(hand):
for c in hand.cards:
if c.cardtype != 'number':
return True
else:
return False
def getNextPlayer(playerIndex, direction=1, skip=False):
current_index = unoLobby.index(playerIndex)
next_index = (current_index + direction) % len(unoLobby)
if skip:
next_index = (next_index + direction) % len(unoLobby)
return unoLobby[next_index]
def getNextPlayerID(playerIndex, direction=1, skip=False):
current_index = unoLobby.index(playerIndex)
next_index = (current_index + direction) % len(unoLobby)
if skip:
next_index = (next_index + direction) % len(unoLobby)
return unoTracker[next_index]['nodeID']
def unoPlayerDetail(nodeID):
for i in range(len(unoTracker)):
if unoTracker[i] == nodeID:
return f'{unoTracker[i]}'
def getUnoPname(nodeID):
global unoTracker
for i in range(len(unoTracker)):
if unoTracker[i]['nodeID'] == nodeID:
return unoTracker[i]['playerName']
def setLastCmd(nodeID, cmd):
global unoTracker
for i in range(len(unoTracker)):
if unoTracker[i]['nodeID'] == nodeID:
unoTracker[i]['cmd'] = cmd
def getLastCmd(nodeID):
global unoTracker
for i in range(len(unoTracker)):
if unoTracker[i]['nodeID'] == nodeID:
return unoTracker[i]['cmd']
def getUnoIDs():
global unoTracker, unoLobby
userIDlist = []
for i in range(len(unoLobby)):
for j in range(len(unoTracker)):
if unoTracker[j]['playerName'] == unoLobby[i]:
unoTracker[j]['last_played'] = time.time()
userIDlist.append(unoTracker[j]['nodeID'])
return (userIDlist)
def playUno(nodeID, message):
global unoTracker, unoGameTable, unoLobby
playing = False
nextPlayerNodeID = 0
msg = 'Not implemented yet'
return msg

View File

@@ -165,7 +165,7 @@ class PlayerVP:
except Exception as e:
pass
return "Re-Draw/Deal ex:1,3,4 to hold cards 1,3 and 4, or (N)o to keep current (H)and"
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
def score_hand(self, resetHand = True):

View File

@@ -7,20 +7,27 @@ from modules.log import *
# Ollama Client
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
from ollama import Client as OllamaClient
from langchain_ollama import OllamaEmbeddings # pip install ollama langchain-ollama
from googlesearch import search # pip install googlesearch-python
# This is my attempt at a simple RAG implementation it will require some setup
# you will need to have the RAG data in a folder named rag in the data directory (../data/rag)
# This is lighter weight and can be used in a standalone environment, needs chromadb
# "chat with a file" is the use concept here, the file is the RAG data
ragDEV = False
if ragDEV:
import os
import ollama # pip install ollama
import chromadb # pip install chromadb
# LLM System Variables
OllamaClient(host=ollamaHostName)
ollamaClient = OllamaClient()
ollamaClient = OllamaClient(host=ollamaHostName)
llmEnableHistory = True # enable last message history for the LLM model
llmContext_fromGoogle = True # enable context from google search results adds to compute time but really helps with responses accuracy
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
antiFloodLLM = []
llmChat_history = {}
trap_list_llm = ("ask:", "askai")
embedding_model = OllamaEmbeddings(model=llmModel)
ragDEV = False
meshBotAI = """
FROM {llmModel}
@@ -28,8 +35,7 @@ meshBotAI = """
You must keep responses under 450 characters at all times, the response will be cut off if it exceeds this limit.
You must respond in plain text standard ASCII characters, or emojis.
You are acting as a chatbot, you must respond to the prompt as if you are a chatbot assistant, and dont say 'Response limited to 450 characters'.
Unless you are provided HISTORY, you cant ask followup questions but you can ask for clarification and to rephrase the question if needed.
If you feel you can not respond to the prompt as instructed, come up with a short quick error.
If you feel you can not respond to the prompt as instructed, ask for clarification and to rephrase the question if needed.
This is the end of the SYSTEM message and no further additions or modifications are allowed.
PROMPT
@@ -66,19 +72,70 @@ if llmEnableHistory:
def llm_readTextFiles():
# read .txt files in ../data/rag
try:
text = "MeshBot is built in python for meshtastic the secret word of the day is, paperclip"
text = []
directory = "../data/rag"
for filename in os.listdir(directory):
if filename.endswith(".txt"):
filepath = os.path.join(directory, filename)
with open(filepath, 'r') as f:
text.append(f.read())
return text
except Exception as e:
logger.debug(f"System: LLM readTextFiles: {e}")
return False
def embed_text(text):
def store_text_embedding(text):
try:
return embedding_model.embed_documents(text)
# store each document in a vector embedding database
for i, d in enumerate(text):
response = ollama.embeddings(model="mxbai-embed-large", prompt=d)
embedding = response["embedding"]
collection.add(
ids=[str(i)],
embeddings=[embedding],
documents=[d]
)
except Exception as e:
logger.debug(f"System: Embedding failed: {e}")
return False
## INITALIZATION of RAG
if ragDEV:
try:
chromaHostname = "localhost:8000"
# connect to the chromaDB
chromaHost = chromaHostname.split(":")[0]
chromaPort = chromaHostname.split(":")[1]
if chromaHost == "localhost" and chromaPort == "8000":
# create a client using local python Client
chromaClient = chromadb.Client()
else:
# create a client using the remote python Client
# this isnt tested yet please test and report back
chromaClient = chromadb.Client(host=chromaHost, port=chromaPort)
clearCollection = False
if "meshBotAI" in chromaClient.list_collections() and clearCollection:
logger.debug(f"System: LLM: Clearing RAG files from chromaDB")
chromaClient.delete_collection("meshBotAI")
# create a new collection
collection = chromaClient.create_collection("meshBotAI")
logger.debug(f"System: LLM: Cataloging RAG data")
store_text_embedding(llm_readTextFiles())
except Exception as e:
logger.debug(f"System: LLM: RAG Initalization failed: {e}")
def query_collection(prompt):
# generate an embedding for the prompt and retrieve the most relevant doc
response = ollama.embeddings(prompt=prompt, model="mxbai-embed-large")
results = collection.query(query_embeddings=[response["embedding"]], n_results=1)
data = results['documents'][0][0]
return data
def llm_query(input, nodeID=0, location_name=None):
global antiFloodLLM, llmChat_history
googleResults = []
@@ -125,24 +182,26 @@ def llm_query(input, nodeID=0, location_name=None):
location_name += f" at the current time of {datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')}"
try:
# Build the query from the template
modelPrompt = meshBotAI.format(input=input, context='\n'.join(googleResults), location_name=location_name, llmModel=llmModel, history=history)
# RAG context inclusion testing
ragData = llm_readTextFiles()
if ragData and ragDEV:
ragContext = embed_text(ragData)
ragContext = False
if ragDEV:
ragContext = query_collection(input)
if ragContext:
ragContextGooogle = ragContext + '\n'.join(googleResults)
# Build the query from the template
modelPrompt = meshBotAI.format(input=input, context=ragContext, location_name=location_name, llmModel=llmModel, history=history)
# Query the model with RAG context
if ragContext:
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt, context=ragContext)
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
else:
# Build the query from the template
modelPrompt = meshBotAI.format(input=input, context='\n'.join(googleResults), location_name=location_name, llmModel=llmModel, history=history)
# Query the model without RAG context
result = ollamaClient.generate(model=llmModel, prompt=modelPrompt)
# Condense the result to just needed
result = result.get("response")
if isinstance(result, dict):
result = result.get("response")
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
except Exception as e:
@@ -169,4 +228,4 @@ def llm_query(input, nodeID=0, location_name=None):
# logger.debug(f"System: Ollama process with CPU, query time will be slower")
# except Exception as e:
# logger.debug(f"System: Ollama process not found, {e}")
# return False
# return False

View File

@@ -180,40 +180,44 @@ def get_tide(lat=0, lon=0):
logger.error("Location:Error fetching tide station table from NOAA")
return ERROR_FETCHING_DATA
station_url = "https://tidesandcurrents.noaa.gov/noaatidepredictions.html?id=" + station_id
if zuluTime:
station_url += "&clock=24hour"
station_url = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?date=today&time_zone=lst_ldt&datum=MLLW&product=predictions&interval=hilo&format=json&station=" + station_id
if use_metric:
station_url += "&units=metric"
else:
station_url += "&units=english"
try:
station_data = requests.get(station_url, timeout=urlTimeoutSeconds)
if not station_data.ok:
logger.error("Location:Error fetching station data from NOAA")
tide_data = requests.get(station_url, timeout=urlTimeoutSeconds)
if tide_data.ok:
tide_json = tide_data.json()
else:
logger.error("Location:Error fetching tide data from NOAA")
return ERROR_FETCHING_DATA
except (requests.exceptions.RequestException):
logger.error("Location:Error fetching station data from NOAA")
return ERROR_FETCHING_DATA
# extract table class="table table-condensed"
soup = bs.BeautifulSoup(station_data.text, 'html.parser')
table = soup.find('table', class_='table table-condensed')
# extract rows
rows = table.find_all('tr')
# extract data from rows
tide_data = []
for row in rows:
row_text = ""
cols = row.find_all('td')
for col in cols:
row_text += col.text + " "
tide_data.append(row_text)
# format tide data into a string
tide_string = ""
for data in tide_data:
tide_string += data + "\n"
# trim off last newline
tide_string = tide_string[:-1]
return tide_string
except (requests.exceptions.RequestException, json.JSONDecodeError):
logger.error("Location:Error fetching tide data from NOAA")
return ERROR_FETCHING_DATA
tide_data = tide_json['predictions']
# format tide data into a table string for mesh
# get the date out of the first t value
tide_date = tide_data[0]['t'].split(" ")[0]
tide_table = "Tide Data for " + tide_date + "\n"
for tide in tide_data:
tide_time = tide['t'].split(" ")[1]
if not zuluTime:
# convert to 12 hour clock
if int(tide_time.split(":")[0]) > 12:
tide_time = str(int(tide_time.split(":")[0]) - 12) + ":" + tide_time.split(":")[1] + " PM"
else:
tide_time = tide_time + " AM"
tide_table += tide['type'] + " " + tide_time + ", " + tide['v'] + "\n"
# remove last newline
tide_table = tide_table[:-1]
return tide_table
def get_weather(lat=0, lon=0, unit=0):
# get weather report from NOAA for forecast detailed
@@ -321,11 +325,15 @@ def abbreviate_weather(row):
return line
def getWeatherAlerts(lat=0, lon=0):
def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
alerts = ""
if float(lat) == 0 and float(lon) == 0:
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
return NO_DATA_NOGPS
else:
if useDefaultLatLon:
lat = latitudeValue
lon = longitudeValue
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
@@ -365,6 +373,23 @@ def getWeatherAlerts(lat=0, lon=0):
data = "\n".join(alerts.split("\n")[:numWxAlerts]), alert_num
return data
wxAlertCache = ""
def alertBrodcast():
# get the latest weather alerts and broadcast them if there are any
global wxAlertCache
currentAlert = getWeatherAlerts(latitudeValue, longitudeValue)
if currentAlert[0] == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS or currentAlert == NO_ALERTS:
wxAlertCache = ""
return False
# broadcast the alerts send to wxBrodcastCh
elif currentAlert[0] != wxAlertCache:
logger.debug("Location:Broadcasting weather alerts")
wxAlertCache = currentAlert[0]
return currentAlert
return False
def getActiveWeatherAlertsDetail(lat=0, lon=0):
# get the latest details of weather alerts from NOAA
alerts = ""

View File

@@ -1,4 +1,5 @@
import logging
from logging.handlers import TimedRotatingFileHandler
import re
from datetime import datetime
from modules.settings import *
@@ -63,14 +64,14 @@ logger.addHandler(stdout_handler)
if syslog_to_file:
# Create file handler for logging to a file
file_handler_sys = logging.FileHandler('logs/meshbot{}.log'.format(today.strftime('%Y_%m_%d')))
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count)
file_handler_sys.setLevel(logging.DEBUG) # 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 = logging.FileHandler('logs/messages{}.log'.format(today.strftime('%Y_%m_%d')))
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count)
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
file_handler.setFormatter(logging.Formatter(msgLogFormat))
msgLogger.addHandler(file_handler)

View File

@@ -19,6 +19,7 @@ antiSpam = True # anti-spam feature to prevent flooding public channel
ping_enabled = True # ping feature to respond to pings, ack's etc.
sitrep_enabled = True # sitrep feature to respond to sitreps
lastHamLibAlert = 0 # last alert from hamlib
lastFileAlert = 0 # last alert from file monitor
max_retry_count1 = 4 # max retry count for interface 1
max_retry_count2 = 4 # max retry count for interface 2
retry_int1 = False
@@ -50,7 +51,7 @@ if 'sentry' not in config:
config.write(open(config_file, 'w'))
if 'location' not in config:
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True'}
config['location'] = {'enabled': 'True', 'lat': '48.50', 'lon': '-123.0', 'UseMeteoWxAPI': 'False', 'useMetric': 'False', 'NOAAforecastDuration': '4', 'NOAAalertCount': '2', 'NOAAalertsEnabled': 'True', 'wxAlertBroadcastEnabled': 'False', 'wxAlertBroadcastChannel': '2', 'repeaterLookup': 'rbook'}
config.write(open(config_file, 'w'))
if 'bbs' not in config:
@@ -73,6 +74,10 @@ if 'messagingSettings' not in config:
config['messagingSettings'] = {'responseDelay': '0.7', 'splitDelay': '0', 'MESSAGE_CHUNK_SIZE': '160'}
config.write(open(config_file, 'w'))
if 'fileMon' not in config:
config['fileMon'] = {'enabled': 'False', 'file_path': 'alert.txt', 'broadcastCh': '2'}
config.write(open(config_file, 'w'))
# interface1 settings
interface1_type = config['interface'].get('type', 'serial')
port1 = config['interface'].get('port', '')
@@ -97,6 +102,7 @@ try:
ignoreDefaultChannel = config['general'].getboolean('ignoreDefaultChannel', False)
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
syslog_to_file = config['general'].getboolean('SyslogToFile', True) # default on
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
store_forward_enabled = config['general'].getboolean('StoreForward', True)
@@ -132,6 +138,13 @@ try:
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True not enabled yet
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
# brodcast channel for weather alerts
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
if ',' in wxAlertBroadcastChannel:
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',')
else:
wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2
# bbs
bbs_enabled = config['bbs'].getboolean('enabled', False)
@@ -151,7 +164,12 @@ try:
signalHoldTime = config['radioMon'].getint('signalHoldTime', 10) # default 10 seconds
signalCooldown = config['radioMon'].getint('signalCooldown', 5) # default 1 second
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
# file monitor
file_monitor_enabled = config['fileMon'].getboolean('enabled', False)
file_monitor_file_path = config['fileMon'].get('file_path', 'alert.txt') # default alert.txt
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
# games
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops
dopewars_enabled = config['games'].getboolean('dopeWars', True)
@@ -160,7 +178,6 @@ try:
videoPoker_enabled = config['games'].getboolean('videoPoker', True)
mastermind_enabled = config['games'].getboolean('mastermind', True)
golfSim_enabled = config['games'].getboolean('golfSim', True)
uno_enabled = config['games'].getboolean('uno', True)
# messaging settings
responseDelay = config['messagingSettings'].getfloat('responseDelay', 0.7) # default 0.7

View File

@@ -15,7 +15,7 @@ trap_list = ("cmd","cmd?") # default trap list
help_message = "Bot CMD?:\n"
asyncLoop = asyncio.new_event_loop()
games_enabled = False
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0}]
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0, 'channel_number': 0}]
# Ping Configuration
@@ -129,11 +129,6 @@ if golfSim_enabled:
from modules.games.golfsim import * # from the spudgunman/meshing-around repo
trap_list = trap_list + ("golfsim",)
games_enabled = True
if uno_enabled:
from modules.games.uno import * # from the spudgunman/meshing-around repo
trap_list = trap_list + ("playuno",)
games_enabled = True
# Games Configuration
if games_enabled is True:
@@ -155,8 +150,6 @@ if games_enabled is True:
gamesCmdList += "masterMind, "
if golfSim_enabled:
gamesCmdList += "golfSim, "
if uno_enabled:
gamesCmdList += "playuno, "
gamesCmdList = gamesCmdList[:-2] # remove the last comma
else:
gamesCmdList = ""
@@ -181,6 +174,10 @@ if store_forward_enabled:
if radio_detection_enabled:
from modules.radio import * # from the spudgunman/meshing-around repo
# File Monitor Configuration
if file_monitor_enabled:
from modules.filemon import * # from the spudgunman/meshing-around repo
# BLE dual interface prevention
if interface1_type == 'ble' and interface2_type == 'ble':
logger.critical(f"System: BLE Interface1 and Interface2 cannot both be BLE. Exiting")
@@ -576,15 +573,18 @@ def handleMultiPing(nodeID=0, deviceID=1):
for i in range(len(mPlCpy)):
message_id_from = mPlCpy[i]['message_from_id']
count = mPlCpy[i]['count']
type = mPlCpy[i]['type']
type = mPlCpy[i]['type'].strip()
deviceID = mPlCpy[i]['deviceID']
channel_number = mPlCpy[i]['channel_number']
if count > 1 and deviceID == 1:
if count > 1:
count -= 1
# update count in the list
multiPingList[i]['count'] = count
for i in range(len(multiPingList)):
if multiPingList[i]['message_from_id'] == message_id_from:
multiPingList[i]['count'] = count
send_message(f"🔂{count} {type}", publicChannel, message_id_from, 1)
send_message(f"🔂{count} {type}", channel_number, message_id_from, deviceID)
if count < 2:
# remove the item from the list
for j in range(len(multiPingList)):
@@ -592,6 +592,24 @@ def handleMultiPing(nodeID=0, deviceID=1):
multiPingList.pop(j)
break
def handleWxBroadcast(deviceID=1):
# only allow API call every 30 minutes
clock = datetime.now()
if clock.minute % 30 != 0:
return False
# check for alerts
alert = alertBrodcast()
if alert:
msg = f"🚨 {alert[1]} EAS ALERTs: {alert[0]}"
if isinstance(wxAlertBroadcastChannel, list):
for channel in wxAlertBroadcastChannel:
send_message(msg, int(channel), 0, deviceID)
else:
send_message(msg, wxAlertBroadcastChannel, 0, deviceID)
return True
def onDisconnect(interface):
global retry_int1, retry_int2
rxType = type(interface).__name__
@@ -820,7 +838,7 @@ async def BroadcastScheduler():
await asyncio.sleep(1)
async def handleSignalWatcher():
global lastHamLibAlert, antiSpam, sigWatchBroadcastCh
global lastHamLibAlert
# monitor rigctld for signal strength and frequency
while True:
msg = await signalWatcher()
@@ -850,6 +868,36 @@ async def handleSignalWatcher():
await asyncio.sleep(1)
pass
async def handleFileWatcher():
global lastFileAlert
# monitor the file system for changes
while True:
msg = await watch_file()
if msg != ERROR_FETCHING_DATA and msg is not None:
logger.debug(f"System: Detected Alert from FileWatcher on file {file_monitor_file_path}")
# check we are not spammig the channel limit messages to once per minute
if time.time() - lastFileAlert > 60:
lastFileAlert = time.time()
# if fileWatchBroadcastCh list contains multiple channels, broadcast to all
if type(file_monitor_broadcastCh) is list:
for ch in file_monitor_broadcastCh:
if antiSpam and ch != publicChannel:
send_message(msg, int(ch), 0, 1)
if interface2_enabled:
send_message(msg, int(ch), 0, 2)
else:
logger.warning(f"System: antiSpam prevented Alert from FileWatcher")
else:
if antiSpam and file_monitor_broadcastCh != publicChannel:
send_message(msg, int(file_monitor_broadcastCh), 0, 1)
if interface2_enabled:
send_message(msg, int(file_monitor_broadcastCh), 0, 2)
else:
logger.warning(f"System: antiSpam prevented Alert from FileWatcher")
await asyncio.sleep(1)
pass
async def retry_interface(nodeID=1):
global interface1, interface2, retry_int1, retry_int2, max_retry_count1, max_retry_count2
@@ -897,7 +945,6 @@ async def retry_interface(nodeID=1):
logger.error(f"System: Error Opening interface{nodeID} on: {e}")
handleSentinel_spotted = ""
handleSentinel_loop = 0
async def handleSentinel(deviceID=1):
@@ -905,6 +952,7 @@ async def handleSentinel(deviceID=1):
# Locate Closest Nodes and report them to a secure channel
# async function for possibly demanding back location data
enemySpotted = ""
resolution = "unknown"
closest_nodes = get_closest_nodes(deviceID)
if closest_nodes != ERROR_FETCHING_DATA and closest_nodes:
if closest_nodes[0]['id'] is not None:
@@ -918,7 +966,9 @@ async def handleSentinel(deviceID=1):
# check the positionMetadata for nodeID and get metadata
if positionMetadata and closest_nodes[0]['id'] in positionMetadata:
metadata = positionMetadata[closest_nodes[0]['id']]
resolution = metadata.get('precisionBits', 'na')
if metadata.get('precisionBits') is not None:
resolution = metadata.get('precisionBits')
logger.warning(f"System: {enemySpotted} is close to your location on Interface1 Accuracy is {resolution}bits")
send_message(f"Sentry{deviceID}: {enemySpotted}", secure_channel, 0, deviceID)
@@ -950,6 +1000,9 @@ async def watchdog():
# multiPing handler
handleMultiPing(0,1)
if wxAlertBroadcastEnabled:
handleWxBroadcast(1)
# Telemetry data
int1Data = displayNodeTelemetry(0, 1)
if int1Data != -1 and telemetryData[0]['lastAlert1'] != int1Data:
@@ -979,6 +1032,9 @@ async def watchdog():
# multiPing handler
handleMultiPing(0,2)
if wxAlertBroadcastEnabled:
handleWxBroadcast(2)
# Telemetry data
int2Data = displayNodeTelemetry(0, 2)
if int2Data != -1 and telemetryData[0]['lastAlert2'] != int2Data:

View File

@@ -14,6 +14,4 @@ geopy
schedule
wikipedia
ollama
langchain
langchain-ollama
googlesearch-python