Compare commits

...

104 Commits

Author SHA1 Message Date
SpudGunMan
bb051f4225 errata 2024-12-05 12:49:29 -08:00
SpudGunMan
61c5be1a08 throttleEAS 2024-12-05 11:39:36 -08:00
SpudGunMan
bc7d47b2a7 Update README.md 2024-12-05 11:32:46 -08:00
SpudGunMan
24bcd5cbf9 readNews
on demand return of the file news.txt for readnews onair
2024-12-05 11:08:11 -08:00
SpudGunMan
8407512b0f fineTuneEAS 2024-12-04 23:31:29 -08:00
SpudGunMan
6f4e8615a3 Update system.py 2024-12-04 20:06:33 -08:00
SpudGunMan
314d36e0dc fixReporter 2024-12-04 15:28:18 -08:00
SpudGunMan
27accb0d4a Update bbstools.py 2024-12-02 16:33:30 -08:00
SpudGunMan
fd84505ad1 typo 2024-12-02 16:25:48 -08:00
SpudGunMan
8f75b13c4d Update mesh_bot.py 2024-12-02 16:23:35 -08:00
SpudGunMan
31d05f8aa7 Update mesh_bot.py 2024-12-02 16:21:40 -08:00
SpudGunMan
cdfe4bb844 Update bbstools.py 2024-12-02 16:14:40 -08:00
SpudGunMan
f30e9cd8b8 Update bbstools.py 2024-12-02 16:14:01 -08:00
SpudGunMan
931bc7b9f7 Update bbstools.py 2024-12-02 16:08:22 -08:00
SpudGunMan
049c0d5ad7 bbsLinkEnhancments 2024-12-02 16:05:14 -08:00
SpudGunMan
a5f1e452e4 Update bbstools.py 2024-12-01 13:13:20 -08:00
SpudGunMan
d89cd8598d limitAutoPing 2024-11-29 20:18:52 -08:00
SpudGunMan
d4e3ea60e3 Update settings.py 2024-11-29 18:23:22 -08:00
SpudGunMan
b98bc8429a Update mesh_bot.py 2024-11-29 18:17:17 -08:00
SpudGunMan
4bb7c9296a Update README.md 2024-11-29 18:06:42 -08:00
SpudGunMan
bb7b5b1c90 scheduler work 2024-11-29 18:05:07 -08:00
SpudGunMan
c400f6f998 Update README.md 2024-11-29 17:27:00 -08:00
SpudGunMan
fce6c0b2e4 Update mesh_bot.py 2024-11-29 17:13:30 -08:00
SpudGunMan
0d0288ba18 Update mesh_bot.py 2024-11-29 17:11:26 -08:00
SpudGunMan
c25d7bc8de Update mesh_bot.py 2024-11-29 17:07:00 -08:00
SpudGunMan
d42fa72d54 fix bbslink/ack 2024-11-29 17:05:28 -08:00
SpudGunMan
bc7176c1cf Update README.md 2024-11-29 11:23:57 -08:00
SpudGunMan
15d454f93a Update eas_alert_parser.py 2024-11-29 10:37:52 -08:00
SpudGunMan
249ee3bb5a Update README.md 2024-11-29 00:27:38 -08:00
SpudGunMan
a3b3d4ea0e Update mesh_bot.py 2024-11-28 23:28:42 -08:00
SpudGunMan
27f9d04538 Update system.py 2024-11-28 23:02:16 -08:00
SpudGunMan
03f1869b23 Update mesh_bot.py 2024-11-28 22:59:49 -08:00
SpudGunMan
479e177a64 exceed the maxBuffer fix 2024-11-28 22:24:41 -08:00
SpudGunMan
5cf166af87 Update mesh_bot.py 2024-11-28 22:06:07 -08:00
SpudGunMan
e24bcd7d38 Update mesh_bot.py 2024-11-28 21:43:03 -08:00
SpudGunMan
768898df64 Update system.py 2024-11-28 21:38:37 -08:00
SpudGunMan
cf282e04bb Update system.py 2024-11-28 21:16:09 -08:00
SpudGunMan
db4edac083 enhance maBuffer Logic 2024-11-28 21:14:41 -08:00
SpudGunMan
877d0cf7f8 enhance MaxBuffer Test
sets the lower limit to 150
2024-11-28 19:35:18 -08:00
SpudGunMan
e78c441a6e Update README.md 2024-11-28 17:23:09 -08:00
SpudGunMan
e945819365 Update mesh_bot.py 2024-11-28 17:12:29 -08:00
SpudGunMan
23e8db50fd Update README.md 2024-11-28 17:11:04 -08:00
SpudGunMan
193ffe6394 Update system.py 2024-11-28 16:53:55 -08:00
SpudGunMan
87016186d8 Update system.py 2024-11-28 16:53:04 -08:00
SpudGunMan
d7d96a89cf Update system.py 2024-11-28 16:51:58 -08:00
SpudGunMan
aa5ef23363 maxBuffer
test 4 will divide the maxBuffer value and send junk data to test a radio or network
2024-11-28 16:41:49 -08:00
SpudGunMan
c18e0401e4 auto TEST Buffer 2024-11-28 16:17:57 -08:00
SpudGunMan
8568990295 Update system.py 2024-11-28 15:55:44 -08:00
SpudGunMan
44e6460224 Update mesh_bot.py 2024-11-28 15:28:58 -08:00
SpudGunMan
d53480290c wantAck 2024-11-28 14:56:49 -08:00
SpudGunMan
1499d883bc dadJokes🥔🚫 2024-11-28 13:20:30 -08:00
SpudGunMan
883a6902fa Update locationdata.py 2024-11-28 13:06:28 -08:00
SpudGunMan
6d3b754c6c bugFix 2024-11-28 00:02:26 -08:00
SpudGunMan
62f73ce2e6 Update README.md 2024-11-27 23:17:05 -08:00
Kelly
eeab9f3fb1 Merge pull request #86 from SpudGunMan/lab
Enhancements 🦃
2024-11-27 23:01:45 -08:00
SpudGunMan
c21a67d1cf Update README.md 2024-11-27 22:58:47 -08:00
SpudGunMan
afe48a44da fixEAS Multi Channel 2024-11-27 22:50:30 -08:00
SpudGunMan
7e4822e4ec Update locationdata.py 2024-11-27 22:22:19 -08:00
SpudGunMan
705ab6a980 fixClosesNodex2 2024-11-27 21:31:52 -08:00
SpudGunMan
963b29eea4 fixEnhanceAutoPing 2024-11-27 21:20:08 -08:00
SpudGunMan
b3f889c4c7 fixBugClosestNodes 2024-11-27 20:40:08 -08:00
SpudGunMan
545b4891b4 error2Warning 2024-11-27 20:38:38 -08:00
SpudGunMan
c89f14b3c2 fix that needed for later 2024-11-25 11:51:25 -08:00
SpudGunMan
c416b00383 newLogRotation 2024-11-25 11:49:39 -08:00
SpudGunMan
669a891eeb Update log.py 2024-11-25 11:46:22 -08:00
SpudGunMan
520d58b262 Update log.py 2024-11-25 11:35:36 -08:00
SpudGunMan
24dff868ff RotateLogger
default is 32 days of logs configure if needed otherwise.
2024-11-24 19:52:25 -08:00
SpudGunMan
cf45bb5060 LOG Rotation Update
update log handler
2024-11-24 19:43:47 -08:00
SpudGunMan
0f9064f2c3 EAS API Alerts
Enable EAS API Messages to Mesh
Fix multiping Device 2
2024-11-23 16:27:55 -08:00
SpudGunMan
f94f329b1f Create eas_alert_parser.py 2024-11-23 15:50:04 -08:00
SpudGunMan
dc4560081d dropLangChain 2024-11-23 15:49:27 -08:00
SpudGunMan
b42cd0e6dc Update README.md 2024-11-20 16:06:28 -08:00
SpudGunMan
bbe1e45541 Update README.md 2024-11-20 16:03:22 -08:00
SpudGunMan
2c61db1215 fix that enhance 2024-11-19 19:52:04 -08:00
SpudGunMan
fde2bb94d9 enhance 2024-11-19 19:51:40 -08:00
SpudGunMan
436a43d3ad Update README.md 2024-11-19 19:49:46 -08:00
SpudGunMan
6b2a6f3a83 enhanceFileMon 2024-11-19 19:44:49 -08:00
SpudGunMan
8e5773115c FileMon
Enhancement with FileMon to watch a file and deliver its goods to the mesh
2024-11-19 19:41:14 -08:00
SpudGunMan
626a5dfe16 moveAPItide 2024-11-15 16:21:56 -08:00
SpudGunMan
e63f4816c4 Update locationdata.py 2024-11-15 14:16:35 -08:00
SpudGunMan
13852b194b enhance 2024-11-11 15:11:25 -08:00
SpudGunMan
a68c20098b ScrubUno
gone but not forgotten
2024-11-11 14:41:14 -08:00
Kelly
432b5a767e Merge pull request #85 from SpudGunMan/lab
BBS LiNK
2024-11-07 15:01:54 -08:00
SpudGunMan
952659198c fixes 2024-11-05 07:45:19 -08:00
SpudGunMan
4e518758e5 Update bbstools.py 2024-10-31 16:56:43 -07:00
SpudGunMan
e1b3dd311f Update bbstools.py 2024-10-31 16:42:09 -07:00
SpudGunMan
bb0f923155 bbslink
first attempt at giving BBS link over the air
2024-10-31 16:38:16 -07:00
SpudGunMan
ab86f02bd7 Update llm.py 2024-10-27 17:12:28 -07:00
SpudGunMan
43067cfb07 Update llm.py 2024-10-27 16:58:05 -07:00
SpudGunMan
3300694059 Update mesh_bot.py 2024-10-26 18:54:48 -07:00
SpudGunMan
f59b8715dd Update simulator.py 2024-10-26 17:31:05 -07:00
SpudGunMan
60abadd1fc Update llm.py 2024-10-24 19:49:01 -07:00
SpudGunMan
4297c91c5e Update llm.py 2024-10-24 19:47:19 -07:00
SpudGunMan
c8eddc3787 Update llm.py 2024-10-24 19:44:51 -07:00
SpudGunMan
d01d81a6d7 openWebUI 2024-10-24 19:21:23 -07:00
SpudGunMan
40b31fd8af Update dopewar.py 2024-10-23 22:13:48 -07:00
SpudGunMan
7b995b35cd 💊
cleanup display on some things
2024-10-23 22:07:46 -07:00
SpudGunMan
00885d57c9 Update dopewar.py 2024-10-23 21:21:44 -07:00
SpudGunMan
d03d7dbc47 Update llm.py 2024-10-21 19:36:02 -07:00
SpudGunMan
7fd4074bd3 Update videopoker.py 2024-10-20 22:56:58 -07:00
SpudGunMan
8367bca4d5 Update joke.py 2024-10-20 17:42:00 -07:00
SpudGunMan
5059990adb Update mesh_bot.py 2024-10-20 17:24:22 -07:00
SpudGunMan
9dd9d39df4 Update llm.py 2024-10-19 08:14:23 -07:00
SpudGunMan
87f89fea6d Update llm.py 2024-10-18 10:50:29 -07:00
21 changed files with 679 additions and 398 deletions

122
README.md
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ from modules.log import *
from modules.system import *
# list of commands to remove from the default list for DM only
restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind", "uno"]
restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golfsim", "mastermind"]
restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
# Global Variables
@@ -25,19 +25,21 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
# Command List
default_commands = {
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM),
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
"ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
"askai": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
"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:

View File

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

View File

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

View File

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

View File

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

View File

@@ -165,7 +165,7 @@ class PlayerVP:
except Exception as e:
pass
return "Re-Draw/Deal ex:1,3,4 to hold cards 1,3 and 4, or (N)o to keep current (H)and"
return "ex:1,3,4 deals them new, and keeps 2,5 or (N)o to keep current (H)and"
# Method for scoring hand, calculating winnings, and outputting message
def score_hand(self, resetHand = True):

View File

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

View File

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

View File

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

View File

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

View File

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

1
news.txt Normal file
View File

@@ -0,0 +1 @@
no new news is good news!

View File

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