mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-05-02 19:42:18 +02:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb051f4225 | ||
|
|
61c5be1a08 | ||
|
|
bc7d47b2a7 | ||
|
|
24bcd5cbf9 | ||
|
|
8407512b0f | ||
|
|
6f4e8615a3 | ||
|
|
314d36e0dc | ||
|
|
27accb0d4a | ||
|
|
fd84505ad1 | ||
|
|
8f75b13c4d | ||
|
|
31d05f8aa7 | ||
|
|
cdfe4bb844 | ||
|
|
f30e9cd8b8 | ||
|
|
931bc7b9f7 | ||
|
|
049c0d5ad7 | ||
|
|
a5f1e452e4 | ||
|
|
d89cd8598d | ||
|
|
d4e3ea60e3 | ||
|
|
b98bc8429a | ||
|
|
4bb7c9296a | ||
|
|
bb7b5b1c90 | ||
|
|
c400f6f998 | ||
|
|
fce6c0b2e4 | ||
|
|
0d0288ba18 | ||
|
|
c25d7bc8de | ||
|
|
d42fa72d54 | ||
|
|
bc7176c1cf | ||
|
|
15d454f93a | ||
|
|
249ee3bb5a | ||
|
|
a3b3d4ea0e | ||
|
|
27f9d04538 | ||
|
|
03f1869b23 | ||
|
|
479e177a64 | ||
|
|
5cf166af87 | ||
|
|
e24bcd7d38 | ||
|
|
768898df64 | ||
|
|
cf282e04bb | ||
|
|
db4edac083 | ||
|
|
877d0cf7f8 | ||
|
|
e78c441a6e | ||
|
|
e945819365 | ||
|
|
23e8db50fd | ||
|
|
193ffe6394 | ||
|
|
87016186d8 | ||
|
|
d7d96a89cf | ||
|
|
aa5ef23363 | ||
|
|
c18e0401e4 | ||
|
|
8568990295 | ||
|
|
44e6460224 | ||
|
|
d53480290c | ||
|
|
1499d883bc | ||
|
|
883a6902fa | ||
|
|
6d3b754c6c | ||
|
|
62f73ce2e6 | ||
|
|
eeab9f3fb1 | ||
|
|
c21a67d1cf | ||
|
|
afe48a44da | ||
|
|
7e4822e4ec | ||
|
|
705ab6a980 | ||
|
|
963b29eea4 | ||
|
|
b3f889c4c7 | ||
|
|
545b4891b4 | ||
|
|
c89f14b3c2 | ||
|
|
c416b00383 | ||
|
|
669a891eeb | ||
|
|
520d58b262 | ||
|
|
24dff868ff | ||
|
|
cf45bb5060 | ||
|
|
0f9064f2c3 | ||
|
|
f94f329b1f | ||
|
|
dc4560081d | ||
|
|
b42cd0e6dc | ||
|
|
bbe1e45541 | ||
|
|
2c61db1215 | ||
|
|
fde2bb94d9 | ||
|
|
436a43d3ad | ||
|
|
6b2a6f3a83 | ||
|
|
8e5773115c | ||
|
|
626a5dfe16 | ||
|
|
e63f4816c4 | ||
|
|
13852b194b | ||
|
|
a68c20098b | ||
|
|
432b5a767e | ||
|
|
952659198c | ||
|
|
4e518758e5 | ||
|
|
e1b3dd311f | ||
|
|
bb0f923155 | ||
|
|
ab86f02bd7 | ||
|
|
43067cfb07 | ||
|
|
3300694059 | ||
|
|
f59b8715dd | ||
|
|
60abadd1fc | ||
|
|
4297c91c5e | ||
|
|
c8eddc3787 | ||
|
|
d01d81a6d7 | ||
|
|
40b31fd8af | ||
|
|
7b995b35cd | ||
|
|
00885d57c9 | ||
|
|
d03d7dbc47 | ||
|
|
7fd4074bd3 | ||
|
|
8367bca4d5 | ||
|
|
5059990adb | ||
|
|
9dd9d39df4 | ||
|
|
87f89fea6d |
122
README.md
122
README.md
@@ -10,6 +10,10 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Automated Responses**: The bot traps keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
|
||||
- **Customizable Triggers**: Monitor group channels for specific keywords and set custom responses.
|
||||
|
||||
### Network Tools
|
||||
- **Build, Test Local Mesh**: Ping allow for message delivery testing with more realistic packets vs. telemetry
|
||||
- **Test Node Hardware**: `test` will send incremental sized data into the radio buffer for overall length of message testing
|
||||
|
||||
### Dual Radio/Node Support
|
||||
- **Simultaneous Monitoring**: Monitor two networks at the same time.
|
||||
- **Flexible Messaging**: send mail and messages, between networks.
|
||||
@@ -19,6 +23,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 +41,14 @@ 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.
|
||||
- **News File**: on request of news the contents of the file is returned.
|
||||
|
||||
### Data Reporting
|
||||
- **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md).
|
||||
|
||||
@@ -118,7 +131,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]
|
||||
@@ -150,7 +163,7 @@ lheardCmdIgnoreNodes = # command history ignore list ex: 2813308004,4258675309
|
||||
|
||||
### Sentry Settings
|
||||
|
||||
Sentry Bot detects anyone coming close to the bot-node.
|
||||
Sentry Bot detects anyone coming close to the bot-node. uses the Location Lat/Lon value.
|
||||
|
||||
```ini
|
||||
SentryEnabled = True # detect anyone close to the bot
|
||||
@@ -169,20 +182,6 @@ enabled = True
|
||||
repeater_channels = [2, 3]
|
||||
```
|
||||
|
||||
### Radio Monitoring
|
||||
A module allowing a Hamlib compatible radio to connect to the bot. When functioning, it will message the configured channel with a message of in use. **Requires hamlib/rigctld to be running as a service.**
|
||||
|
||||
```ini
|
||||
[radioMon]
|
||||
enabled = False
|
||||
rigControlServerAddress = localhost:4532
|
||||
sigWatchBroadcastCh = 2 # channel to broadcast to can be 2,3
|
||||
signalDetectionThreshold = -10 # minimum SNR as reported by radio via hamlib
|
||||
signalHoldTime = 10 # hold time for high SNR
|
||||
signalCooldown = 5 # the following are combined to reset the monitor
|
||||
signalCycleLimit = 5
|
||||
```
|
||||
|
||||
### Ollama (LLM/AI) Settings
|
||||
For Ollama to work, the command line `ollama run 'model'` needs to work properly. Ensure you have enough RAM and your GPU is working as expected. The default model for this project is set to `gemma2:2b`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server)
|
||||
|
||||
@@ -202,8 +201,72 @@ 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
|
||||
```
|
||||
|
||||
### Radio Monitoring
|
||||
A module allowing a Hamlib compatible radio to connect to the bot. When functioning, it will message the configured channel with a message of in use. **Requires hamlib/rigctld to be running as a service.**
|
||||
|
||||
```ini
|
||||
[radioMon]
|
||||
enabled = False
|
||||
rigControlServerAddress = localhost:4532
|
||||
sigWatchBroadcastCh = 2 # channel to broadcast to can be 2,3
|
||||
signalDetectionThreshold = -10 # minimum SNR as reported by radio via hamlib
|
||||
signalHoldTime = 10 # hold time for high SNR
|
||||
signalCooldown = 5 # the following are combined to reset the monitor
|
||||
signalCycleLimit = 5
|
||||
```
|
||||
|
||||
### File Monitoring
|
||||
Some dev notes for ideas of use
|
||||
|
||||
```ini
|
||||
[fileMon]
|
||||
filemon_enabled = True
|
||||
file_path = alert.txt
|
||||
broadcastCh = 2,4
|
||||
enable_read_news = False
|
||||
news_file_path = news.txt
|
||||
```
|
||||
#### 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
|
||||
- [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
|
||||
- [sameold](https://crates.io/crates/sameold) rust SAME message translator much like EAS2Text and dsame3
|
||||
|
||||
no examples yet for these tools
|
||||
|
||||
- [EAS2Text](https://github.com/A-c0rN/EAS2Text)
|
||||
- depends on [multimon-ng](https://github.com/EliasOenal/multimon-ng), [direwolf](https://github.com/wb2osz/direwolf), [samedec](https://crates.io/crates/samedec) rust decoder much like multimon-ng
|
||||
- [dsame3](https://github.com/jamieden/dsame3)
|
||||
- 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
|
||||
```
|
||||
|
||||
#### Newspaper on mesh
|
||||
a newspaper could be built by external scripts. could use Ollama to compile text via news web pages and write news.txt
|
||||
|
||||
### 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.
|
||||
In the config.ini enable the module
|
||||
```ini
|
||||
[scheduler]
|
||||
# enable or disable the scheduler module
|
||||
enabled = True
|
||||
```
|
||||
The actions are via code only at this time. See mesh_bot.py around line [1050](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1050) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
|
||||
|
||||
```python
|
||||
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
@@ -213,6 +276,18 @@ 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, this would be an esxample of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initator, one direction pull.
|
||||
```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))
|
||||
```
|
||||
```ini
|
||||
bbslink_enabled = True
|
||||
# list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
bbslink_whitelist =
|
||||
```
|
||||
|
||||
### 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 +325,6 @@ For the Ollama LLM:
|
||||
|
||||
```sh
|
||||
pip install ollama
|
||||
pip install langchain
|
||||
pip install langchain-ollama
|
||||
pip install googlesearch-python
|
||||
```
|
||||
|
||||
@@ -266,7 +339,8 @@ sudo apt-get install fonts-noto-color-emoji
|
||||
### Networking
|
||||
| Command | Description | ✅ Works Off-Grid |
|
||||
|---------|-------------|-
|
||||
| `ping`, `ack`, `test` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15) | ✅ |
|
||||
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15) | ✅ |
|
||||
| `test` | Returns like ping but also can be used to test the limits of data buffers `test 4` sends data to the maxBuffer limit (default 220) | ✅ |
|
||||
| `whereami` | Returns the address of the sender's location if known |
|
||||
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
|
||||
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
|
||||
@@ -294,6 +368,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 | |
|
||||
@@ -301,6 +376,7 @@ sudo apt-get install fonts-noto-color-emoji
|
||||
| `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
|
||||
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
|
||||
| `messages` | Replays the last messages heard, like Store and Forward | ✅ |
|
||||
| `readnews` | returns the contents of a file (news.txt, by default) via the chunker on air | ✅ |
|
||||
|
||||
|
||||
|
||||
@@ -314,7 +390,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
|
||||
|
||||
@@ -337,11 +412,10 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
- **xdep**: For the reporting tools.
|
||||
- **Nestpebble**: For new ideas and enhancements.
|
||||
- **mrpatrick1991**: For Docker configurations.
|
||||
- **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.
|
||||
- **Cisien, bitflip, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **Cisien, bitflip, **Woof**, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
|
||||
|
||||
### Tools
|
||||
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ welcome_message = MeshBot, here for you like a friend who is not. Try sending: p
|
||||
whoami = True
|
||||
# enable or disable the Joke module
|
||||
DadJokes = True
|
||||
DadJokesEmoji = False
|
||||
# enable or disable the Solar module
|
||||
spaceWeather = True
|
||||
# enable or disable the wikipedia search module
|
||||
@@ -61,6 +62,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 +75,6 @@ blackjack = True
|
||||
videopoker = True
|
||||
mastermind = True
|
||||
golfsim = True
|
||||
uno = True
|
||||
|
||||
[sentry]
|
||||
# detect anyone close to the bot
|
||||
@@ -92,6 +94,10 @@ enabled = True
|
||||
bbs_ban_list =
|
||||
# list of admin nodes numbers ex: 2813308004,4258675309
|
||||
bbs_admin_list =
|
||||
# enable bbs synchronization with other nodes
|
||||
bbslink_enabled = False
|
||||
# list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
bbslink_whitelist =
|
||||
|
||||
# location module
|
||||
[location]
|
||||
@@ -108,6 +114,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]
|
||||
@@ -116,7 +126,11 @@ enabled = False
|
||||
# and rebroadcasted on the same channel on the other device/node/interface
|
||||
# with great power comes great responsibility, danger could be lurking in use of this feature
|
||||
# if you have the two nodes on the same radio configurations, you could create a feedback loop
|
||||
repeater_channels =
|
||||
repeater_channels =
|
||||
|
||||
[scheduler]
|
||||
# enable or disable the scheduler module
|
||||
enabled = False
|
||||
|
||||
[radioMon]
|
||||
# using Hamlib rig control will monitor and alert on channel use
|
||||
@@ -132,6 +146,13 @@ signalHoldTime = 10
|
||||
signalCooldown = 5
|
||||
signalCycleLimit = 5
|
||||
|
||||
[fileMon]
|
||||
filemon_enabled = False
|
||||
file_path = alert.txt
|
||||
broadcastCh = 2
|
||||
enable_read_news = False
|
||||
news_file_path = news.txt
|
||||
|
||||
[messagingSettings]
|
||||
# delay in seconds for response to avoid message collision
|
||||
responseDelay = 0.7
|
||||
@@ -139,4 +160,9 @@ responseDelay = 0.7
|
||||
splitDelay = 0.0
|
||||
# message chunk size for sending at high success rate
|
||||
MESSAGE_CHUNK_SIZE = 160
|
||||
# Request Acknowledgement of message OTA
|
||||
wantAck = False
|
||||
# Max lilmit Buffer for radio testing
|
||||
maxBuffer = 220
|
||||
|
||||
|
||||
|
||||
48
etc/eas_alert_parser.py
Normal file
48
etc/eas_alert_parser.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# 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-')
|
||||
|
||||
# alternate regex for parsing multimon-ng output
|
||||
#reg = r"^.*?(NNNN|ZCZC)(?:-([A-Za-z0-9]{3})-([A-Za-z0-9]{3})-((?:-?[0-9]{6})+)\+([0-9]{4})-([0-9]{7})-(.{8})-)?.*?$"
|
||||
#prog = re.compile(reg, re.MULTILINE)
|
||||
#groups = prog.match(sameData).groups()
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Handle piped input
|
||||
inp=input().strip()
|
||||
except EOFError:
|
||||
break
|
||||
# potentially take multiple lines in one buffered input
|
||||
for line in inp.splitlines():
|
||||
# only want EAS lines
|
||||
if line.startswith("EAS:") or line.startswith("EAS (part):"):
|
||||
content=line.split(":", maxsplit=1)[1].strip()
|
||||
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)
|
||||
@@ -57,7 +57,7 @@ def parse_log_file(file_path):
|
||||
if multiLogReader:
|
||||
# set file_path to the cwd of the default project ../log
|
||||
log_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'logs')
|
||||
log_files = glob.glob(os.path.join(log_dir, 'meshbot*.log'))
|
||||
log_files = glob.glob(os.path.join(log_dir, 'meshbot.log.*'))
|
||||
print(f"Checking log files: {log_files}")
|
||||
|
||||
if log_files:
|
||||
@@ -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:
|
||||
@@ -745,7 +745,7 @@ def generate_main_html(log_data, system_info):
|
||||
"""
|
||||
template = Template(html_template)
|
||||
return template.safe_substitute(
|
||||
date=datetime.now().strftime('%Y_%m_%d'),
|
||||
date=datetime.now().strftime('%Y-%m-%d'),
|
||||
command_data=json.dumps(log_data['command_counts']),
|
||||
message_data=json.dumps(log_data['message_types']),
|
||||
activity_data=json.dumps(log_data['hourly_activity']),
|
||||
@@ -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):
|
||||
|
||||
@@ -58,7 +58,7 @@ def parse_log_file(file_path):
|
||||
if multiLogReader:
|
||||
# set file_path to the cwd of the default project ../log
|
||||
log_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', 'logs')
|
||||
log_files = glob.glob(os.path.join(log_dir, 'meshbot*.log'))
|
||||
log_files = glob.glob(os.path.join(log_dir, 'meshbot.log.*'))
|
||||
print(f"Checking log files: {log_files}")
|
||||
|
||||
if log_files:
|
||||
@@ -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:
|
||||
@@ -1036,7 +1036,7 @@ options: {
|
||||
|
||||
template = Template(html_template)
|
||||
return template.safe_substitute(
|
||||
date=datetime.now().strftime('%Y_%m_%d'),
|
||||
date=datetime.now().strftime('%Y-%m-%d'),
|
||||
command_data=json.dumps(log_data['command_counts']),
|
||||
message_data=json.dumps(log_data['message_types']),
|
||||
activity_data=json.dumps(log_data['hourly_activity']),
|
||||
@@ -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):
|
||||
|
||||
@@ -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} "
|
||||
|
||||
@@ -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)
|
||||
|
||||
128
mesh_bot.py
128
mesh_bot.py
@@ -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),
|
||||
"bbsack": 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,16 @@ 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!!🛜",
|
||||
"readnews": lambda: read_news(),
|
||||
"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 +111,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"
|
||||
@@ -119,7 +121,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM):
|
||||
|
||||
if "ping" in message.lower():
|
||||
msg = "🏓PONG\n"
|
||||
type = "🏓PING\n"
|
||||
type = "🏓PING"
|
||||
elif "test" in message.lower() or "testing" in message.lower():
|
||||
msg = random.choice(["🎙Testing 1,2,3\n", "🎙Testing\n",\
|
||||
"🎙Testing, testing\n",\
|
||||
@@ -158,16 +160,28 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM):
|
||||
if multiPingList[i].get('message_from_id') == message_from_id:
|
||||
multiPingList.pop(i)
|
||||
msg = "🛑 auto-ping"
|
||||
try:
|
||||
pingCount = int(message.split(" ")[1])
|
||||
if pingCount > 51:
|
||||
pingCount = 50
|
||||
except:
|
||||
|
||||
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
|
||||
if len(multiPingList) > 2:
|
||||
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
|
||||
pingCount = -1
|
||||
else:
|
||||
# set inital pingCount
|
||||
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})
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
|
||||
if type == "🎙TEST":
|
||||
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
|
||||
else:
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
|
||||
return msg
|
||||
|
||||
@@ -213,8 +227,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 +560,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 +790,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:
|
||||
@@ -899,6 +890,7 @@ def onReceive(packet, interface):
|
||||
|
||||
if hop_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
else:
|
||||
# set hop to Direct if the message was sent directly otherwise set the hop count
|
||||
if hop_away > 0:
|
||||
@@ -909,7 +901,7 @@ def onReceive(packet, interface):
|
||||
|
||||
hop = f"{hop_count} hops"
|
||||
|
||||
if message_string == help_message or message_string == welcome_message or "CMD?:" in message_string:
|
||||
if help_message in message_string or welcome_message in message_string or "CMD?:" in message_string:
|
||||
# ignore help and welcome messages
|
||||
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
return
|
||||
@@ -919,7 +911,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)}")
|
||||
@@ -930,15 +922,19 @@ def onReceive(packet, interface):
|
||||
if games_enabled and (hop == "Direct" or hop_count < game_hop_limit):
|
||||
playingGame = checkPlayingGame(message_from_id, message_string, rxNode, channel_number)
|
||||
else:
|
||||
playingGame = False
|
||||
logger.warning(f"Device:{rxNode} Ignoring Request to Play Game: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)} with hop count: {hop}")
|
||||
send_message(f"Your hop count exceeds safe playable distance at {hop_count} hops", channel_number, message_from_id, rxNode)
|
||||
if games_enabled:
|
||||
logger.warning(f"Device:{rxNode} Ignoring Request to Play Game: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)} with hop count: {hop}")
|
||||
send_message(f"Your hop count exceeds safe playable distance at {hop_count} hops", channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
playingGame = False
|
||||
|
||||
if not playingGame:
|
||||
if llm_enabled:
|
||||
# respond with LLM
|
||||
llm = handle_llm(message_from_id, channel_number, rxNode, message_string, publicChannel)
|
||||
send_message(llm, channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# respond with welcome message on DM
|
||||
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
@@ -1032,6 +1028,11 @@ async def start_rx():
|
||||
logger.debug("System: Logging System Logs to disk")
|
||||
if bbs_enabled:
|
||||
logger.debug(f"System: BBS Enabled, {bbsdb} has {len(bbs_messages)} messages. Direct Mail Messages waiting: {(len(bbs_dm) - 1)}")
|
||||
if bbs_link_enabled:
|
||||
if len(bbs_link_whitelist) > 0:
|
||||
logger.debug(f"System: BBS Link Enabled with {len(bbs_link_whitelist)} peers")
|
||||
else:
|
||||
logger.debug(f"System: BBS Link Enabled allowing all")
|
||||
if solar_conditions_enabled:
|
||||
logger.debug("System: Celestial Telemetry Enabled")
|
||||
if location_enabled:
|
||||
@@ -1057,9 +1058,18 @@ 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 read_news_enabled:
|
||||
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
|
||||
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/
|
||||
|
||||
# Reminder Scheduler is enabled every Monday at noon send a log message
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Reminder"))
|
||||
|
||||
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
|
||||
@@ -1076,13 +1086,17 @@ async def start_rx():
|
||||
# Send a joke every 6 hours using tell_joke function to channel 2 on device 1
|
||||
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), 2, 0, 1))
|
||||
|
||||
# Send a joke every 2 minutes using tell_joke function to channel 2 on device 1
|
||||
#schedule.every(2).minutes.do(lambda: send_message(tell_joke(), 2, 0, 1))
|
||||
|
||||
# Send the Welcome Message every other day at 08:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, 2, 0, 1))
|
||||
|
||||
# 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 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()
|
||||
|
||||
@@ -1095,11 +1109,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:
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
import pickle # pip install pickle
|
||||
from modules.log import *
|
||||
import time
|
||||
|
||||
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 +78,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 +163,42 @@ 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
|
||||
|
||||
# check if the bbs link is enabled
|
||||
if bbs_link_whitelist is not None:
|
||||
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."
|
||||
if bbs_link_enabled == False:
|
||||
return "System: BBS Link is disabled."
|
||||
|
||||
# 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 with delay to keep chutil happy
|
||||
if messageID < len(bbs_messages):
|
||||
time.sleep(5 + responseDelay)
|
||||
# every 5 messages add extra delay
|
||||
if messageID % 5 == 0:
|
||||
time.sleep(10 + responseDelay)
|
||||
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()
|
||||
|
||||
39
modules/filemon.py
Normal file
39
modules/filemon.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# File monitor module for the meshing-around bot
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
|
||||
from modules.log import *
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
trap_list_filemon = ("readnews",)
|
||||
|
||||
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
|
||||
|
||||
def read_news():
|
||||
# read the news file on demand
|
||||
return read_file(news_file_path)
|
||||
|
||||
async def watch_file():
|
||||
|
||||
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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -117,6 +117,10 @@ def sendWithEmoji(message):
|
||||
|
||||
def tell_joke(nodeID=0):
|
||||
dadjoke = Dadjoke()
|
||||
renderedLaugh = sendWithEmoji(dadjoke.joke)
|
||||
|
||||
if dad_jokes_emojiJokes:
|
||||
renderedLaugh = sendWithEmoji(dadjoke.joke)
|
||||
else:
|
||||
renderedLaugh = dadjoke.joke
|
||||
return renderedLaugh
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
101
modules/llm.py
101
modules/llm.py
@@ -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
|
||||
|
||||
@@ -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,25 @@ 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)
|
||||
# check if any reason to discard the alerts
|
||||
if currentAlert == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS:
|
||||
return False
|
||||
elif 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 = ""
|
||||
|
||||
@@ -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)
|
||||
@@ -5,7 +5,7 @@ import configparser
|
||||
# messages
|
||||
NO_DATA_NOGPS = "No location data: does your device have GPS?"
|
||||
ERROR_FETCHING_DATA = "error fetching data"
|
||||
WELCOME_MSG = 'MeshBot, here for you like a friend who is not. Try sending: ping @foo or, cmd? for more'
|
||||
WELCOME_MSG = 'MeshBot, here for you like a friend who is not. Try sending: ping @foo or, CMD? for more'
|
||||
MOTD = 'Thanks for using MeshBOT! Have a good day!'
|
||||
NO_ALERTS = "No weather alerts found."
|
||||
|
||||
@@ -19,11 +19,11 @@ 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
|
||||
retry_int2 = False
|
||||
scheduler_enabled = False # enable the scheduler currently config via code only
|
||||
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
|
||||
@@ -50,7 +50,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 +73,14 @@ 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'))
|
||||
|
||||
if 'scheduler' not in config:
|
||||
config['scheduler'] = {'enabled': 'False'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
# interface1 settings
|
||||
interface1_type = config['interface'].get('type', 'serial')
|
||||
port1 = config['interface'].get('port', '')
|
||||
@@ -97,6 +105,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)
|
||||
@@ -109,6 +118,7 @@ try:
|
||||
lheardCmdIgnoreNode = config['general'].get('lheardCmdIgnoreNode', '').split(',')
|
||||
whoami_enabled = config['general'].getboolean('whoami', True)
|
||||
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
|
||||
dad_jokes_emojiJokes = config['general'].getboolean('DadJokesEmoji', False)
|
||||
solar_conditions_enabled = config['general'].getboolean('spaceWeather', True)
|
||||
wikipedia_enabled = config['general'].getboolean('wikipedia', False)
|
||||
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
|
||||
@@ -132,17 +142,30 @@ 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 wxAlertBroadcastChannel:
|
||||
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)
|
||||
bbsdb = config['bbs'].get('bbsdb', 'data/bbsdb.pkl')
|
||||
bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',')
|
||||
bbs_admin_list = config['bbs'].get('bbs_admin_list', '').split(',')
|
||||
bbs_link_enabled = config['bbs'].getboolean('bbslink_enabled', False)
|
||||
bbs_link_whitelist = config['bbs'].get('bbslink_whitelist', '').split(',')
|
||||
|
||||
# repeater
|
||||
repeater_enabled = config['repeater'].getboolean('enabled', False)
|
||||
repeater_channels = config['repeater'].get('repeater_channels', '').split(',')
|
||||
|
||||
# scheduler
|
||||
scheduler_enabled = config['scheduler'].getboolean('enabled', False)
|
||||
|
||||
# radio monitoring
|
||||
radio_detection_enabled = config['radioMon'].getboolean('enabled', False)
|
||||
rigControlServerAddress = config['radioMon'].get('rigControlServerAddress', 'localhost:4532') # default localhost:4532
|
||||
@@ -151,7 +174,14 @@ 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('filemon_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
|
||||
read_news_enabled = config['fileMon'].getboolean('enable_read_news', False) # default disabled
|
||||
news_file_path = config['fileMon'].get('news_file_path', 'news.txt') # default news.txt
|
||||
|
||||
# games
|
||||
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops
|
||||
dopewars_enabled = config['games'].getboolean('dopeWars', True)
|
||||
@@ -160,12 +190,13 @@ 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
|
||||
splitDelay = config['messagingSettings'].getfloat('splitDelay', 0) # default 0
|
||||
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160
|
||||
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
|
||||
maxBuffer = config['messagingSettings'].getint('maxBuffer', 220) # default 220
|
||||
|
||||
except KeyError as e:
|
||||
print(f"System: Error reading config file: {e}")
|
||||
|
||||
@@ -6,6 +6,7 @@ import meshtastic.tcp_interface
|
||||
import meshtastic.ble_interface
|
||||
import time
|
||||
import asyncio
|
||||
import random
|
||||
import contextlib # for suppressing output on watchdog
|
||||
import io # for suppressing output on watchdog
|
||||
from modules.log import *
|
||||
@@ -15,7 +16,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, 'startCount': 0}]
|
||||
|
||||
|
||||
# Ping Configuration
|
||||
@@ -129,11 +130,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 +151,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 = ""
|
||||
@@ -164,8 +158,6 @@ else:
|
||||
# Scheduled Broadcast Configuration
|
||||
if scheduler_enabled:
|
||||
import schedule # pip install schedule
|
||||
# Reminder Scheduler is enabled every Monday at noon send a log message
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Reminder"))
|
||||
|
||||
# Sentry Configuration
|
||||
if sentry_enabled:
|
||||
@@ -181,6 +173,13 @@ 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 or read_news_enabled:
|
||||
from modules.filemon import * # from the spudgunman/meshing-around repo
|
||||
if read_news_enabled:
|
||||
trap_list = trap_list + trap_list_filemon # items readnews
|
||||
help_message = help_message + ", readmail"
|
||||
|
||||
# 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")
|
||||
@@ -464,12 +463,18 @@ def messageChunker(message):
|
||||
|
||||
return message
|
||||
|
||||
def send_message(message, ch, nodeid=0, nodeInt=1):
|
||||
def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
|
||||
# Send a message to a channel or DM
|
||||
interface = interface1 if nodeInt == 1 else interface2
|
||||
# Check if the message is empty
|
||||
if message == "" or message == None or len(message) == 0:
|
||||
return False
|
||||
interface = interface1 if nodeInt == 1 else interface2
|
||||
# Split the message into chunks if it exceeds the MESSAGE_CHUNK_SIZE
|
||||
message_list = messageChunker(message)
|
||||
|
||||
if not bypassChuncking:
|
||||
# Split the message into chunks if it exceeds the MESSAGE_CHUNK_SIZE
|
||||
message_list = messageChunker(message)
|
||||
else:
|
||||
message_list = [message]
|
||||
|
||||
if isinstance(message_list, list):
|
||||
# Send the message to the channel or DM
|
||||
@@ -479,13 +484,22 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
|
||||
chunkOf = f"{message_list.index(m)+1}/{num_chunks}"
|
||||
if nodeid == 0:
|
||||
# Send to channel
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
|
||||
interface.sendText(text=m, channelIndex=ch)
|
||||
if wantAck:
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"req.ACK " + "Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
|
||||
interface.sendText(text=m, channelIndex=ch, wantAck=True)
|
||||
else:
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
|
||||
interface.sendText(text=m, channelIndex=ch)
|
||||
else:
|
||||
# Send to DM
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + f"Chunker{chunkOf} Sending DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
if wantAck:
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + f"req.ACK " + f"Chunker{chunkOf} Sending DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
interface.sendText(text=m, channelIndex=ch, destinationId=nodeid)
|
||||
interface.sendText(text=m, channelIndex=ch, destinationId=nodeid, wantAck=True)
|
||||
else:
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + f"Chunker{chunkOf} Sending DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
interface.sendText(text=m, channelIndex=ch, destinationId=nodeid)
|
||||
|
||||
# Throttle the message sending to prevent spamming the device
|
||||
if (message_list.index(m)+1) % 4 == 0:
|
||||
@@ -499,13 +513,22 @@ def send_message(message, ch, nodeid=0, nodeInt=1):
|
||||
else: # message is less than MESSAGE_CHUNK_SIZE characters
|
||||
if nodeid == 0:
|
||||
# Send to channel
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "SendingChannel: " + CustomFormatter.white + message.replace('\n', ' '))
|
||||
interface.sendText(text=message, channelIndex=ch)
|
||||
if wantAck:
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "req.ACK " + "SendingChannel: " + CustomFormatter.white + message.replace('\n', ' '))
|
||||
interface.sendText(text=message, channelIndex=ch, wantAck=True)
|
||||
else:
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "SendingChannel: " + CustomFormatter.white + message.replace('\n', ' '))
|
||||
interface.sendText(text=message, channelIndex=ch)
|
||||
else:
|
||||
# Send to DM
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
interface.sendText(text=message, channelIndex=ch, destinationId=nodeid)
|
||||
if wantAck:
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "req.ACK " + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
interface.sendText(text=message, channelIndex=ch, destinationId=nodeid, wantAck=True)
|
||||
else:
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
interface.sendText(text=message, channelIndex=ch, destinationId=nodeid)
|
||||
return True
|
||||
|
||||
def get_wikipedia_summary(search_term):
|
||||
@@ -578,13 +601,38 @@ def handleMultiPing(nodeID=0, deviceID=1):
|
||||
count = mPlCpy[i]['count']
|
||||
type = mPlCpy[i]['type']
|
||||
deviceID = mPlCpy[i]['deviceID']
|
||||
channel_number = mPlCpy[i]['channel_number']
|
||||
start_count = mPlCpy[i]['startCount']
|
||||
|
||||
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)
|
||||
# handle bufferTest
|
||||
if type == '🎙TEST':
|
||||
buffer = ''.join(random.choice(['0', '1']) for i in range(maxBuffer))
|
||||
# divide buffer by start_count and get resolution
|
||||
resolution = maxBuffer // start_count
|
||||
slice = resolution * count
|
||||
if slice > maxBuffer:
|
||||
slice = maxBuffer
|
||||
# set the type as a portion of the buffer
|
||||
type = buffer[slice - resolution:]
|
||||
# if exceed the maxBuffer, remove the excess
|
||||
count = len(type + "🔂 ")
|
||||
if count > maxBuffer:
|
||||
type = type[:maxBuffer - count]
|
||||
# final length count of the message for display
|
||||
count = len(type + "🔂 ")
|
||||
if count < 99:
|
||||
count -= 1
|
||||
|
||||
# send the DM
|
||||
send_message(f"🔂{count} {type}", channel_number, message_id_from, deviceID, bypassChuncking=True)
|
||||
time.sleep(responseDelay + 1)
|
||||
if count < 2:
|
||||
# remove the item from the list
|
||||
for j in range(len(multiPingList)):
|
||||
@@ -592,6 +640,27 @@ def handleMultiPing(nodeID=0, deviceID=1):
|
||||
multiPingList.pop(j)
|
||||
break
|
||||
|
||||
|
||||
def handleWxBroadcast(deviceID=1):
|
||||
# only allow API call every 20 minutes
|
||||
# the watchdog will call this function 3 times, seeing possible throttling on the API
|
||||
clock = datetime.now()
|
||||
if clock.minute % 20 != 0:
|
||||
return False
|
||||
if clock.second > 17:
|
||||
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 +889,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 +919,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 +996,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 +1003,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 +1017,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 +1051,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 +1083,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:
|
||||
|
||||
@@ -14,6 +14,4 @@ geopy
|
||||
schedule
|
||||
wikipedia
|
||||
ollama
|
||||
langchain
|
||||
langchain-ollama
|
||||
googlesearch-python
|
||||
|
||||
Reference in New Issue
Block a user