mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
312 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8308c2f98c | ||
|
|
5050f1c5eb | ||
|
|
553137d228 | ||
|
|
d3e7a4d5e4 | ||
|
|
2e26819c2f | ||
|
|
f03b85cebe | ||
|
|
99f25345e8 | ||
|
|
4952bb3ecc | ||
|
|
f79f714317 | ||
|
|
a6db9bc878 | ||
|
|
774f76ecf1 | ||
|
|
3038d72996 | ||
|
|
33245e8443 | ||
|
|
54fb30d048 | ||
|
|
86c86d2f97 | ||
|
|
bb46981a85 | ||
|
|
e32cdf803c | ||
|
|
c6e8feefd7 | ||
|
|
1f48e9a4aa | ||
|
|
571c9c521f | ||
|
|
52b88ce16b | ||
|
|
6f38bff473 | ||
|
|
2b964390f8 | ||
|
|
a305acc492 | ||
|
|
cf66556fe6 | ||
|
|
76e9bd8677 | ||
|
|
c8c3c0f80b | ||
|
|
3a9330d831 | ||
|
|
8d3b0ce4bf | ||
|
|
bb0a22c69b | ||
|
|
df369c3d29 | ||
|
|
db1ca48f0a | ||
|
|
a38d0a6ed7 | ||
|
|
ac8d308f58 | ||
|
|
08b54d9009 | ||
|
|
9da6416433 | ||
|
|
efcfb749dc | ||
|
|
493d2792d6 | ||
|
|
aa68ce120e | ||
|
|
e3a9a00c92 | ||
|
|
f1feb1be0d | ||
|
|
290695327d | ||
|
|
c3ea07fde5 | ||
|
|
e551f1252a | ||
|
|
a8b2aefa28 | ||
|
|
849565cacb | ||
|
|
fdec3a6754 | ||
|
|
f3a97bc567 | ||
|
|
02625ad0f2 | ||
|
|
b4ba4b0daf | ||
|
|
fd7f8a94f5 | ||
|
|
d252250edd | ||
|
|
d6410e0461 | ||
|
|
050b4ab3ce | ||
|
|
8ac1a1eed7 | ||
|
|
370a417ce6 | ||
|
|
378b05df35 | ||
|
|
d002c5ede8 | ||
|
|
cd03cc56b4 | ||
|
|
d4fd484706 | ||
|
|
82d519279e | ||
|
|
09302e8c91 | ||
|
|
91fc4605ec | ||
|
|
abc6c07ee3 | ||
|
|
bbf8b04bd3 | ||
|
|
5fd293c990 | ||
|
|
a9a65a6c6d | ||
|
|
34e95c86d6 | ||
|
|
80891090c3 | ||
|
|
1b098fbf7b | ||
|
|
165d76cf8d | ||
|
|
045c9d433b | ||
|
|
fbe5e008de | ||
|
|
004adc7d9a | ||
|
|
9a2033452f | ||
|
|
5638204f82 | ||
|
|
e5c3b0cceb | ||
|
|
18ac53b230 | ||
|
|
4aa65dad6a | ||
|
|
f65a7b7934 | ||
|
|
886293087a | ||
|
|
e05e6f3451 | ||
|
|
4b5dd934e9 | ||
|
|
008ddfb5a2 | ||
|
|
e1330b9b9e | ||
|
|
7b43213094 | ||
|
|
b17c2b17ee | ||
|
|
b6505ee577 | ||
|
|
f7379b7ca5 | ||
|
|
ec9a1d88db | ||
|
|
a339570afe | ||
|
|
f5af9f419a | ||
|
|
ad5c1c90da | ||
|
|
eb1e0c82ea | ||
|
|
8d2277bc59 | ||
|
|
dc9908a72c | ||
|
|
21123d2993 | ||
|
|
7dc3134d0b | ||
|
|
b125178492 | ||
|
|
2ad9e84c33 | ||
|
|
63bd288caa | ||
|
|
5c7d199831 | ||
|
|
f56a39eeb6 | ||
|
|
ae5991ee39 | ||
|
|
1324f83f17 | ||
|
|
08ae8c31a0 | ||
|
|
957e803951 | ||
|
|
2de3441d67 | ||
|
|
2b420022f9 | ||
|
|
d01f143adf | ||
|
|
5f5aeeadac | ||
|
|
d57826613c | ||
|
|
af09dc0cf9 | ||
|
|
011bac41f2 | ||
|
|
20467ea886 | ||
|
|
bbfd71f011 | ||
|
|
e1ff87a197 | ||
|
|
a859f830bb | ||
|
|
d99698e7f3 | ||
|
|
5ecc563e96 | ||
|
|
eeeb43cacc | ||
|
|
9fdcea56fc | ||
|
|
24a33fe882 | ||
|
|
5710cebf39 | ||
|
|
b66487863d | ||
|
|
b3c4d208b7 | ||
|
|
f41ff2d5f7 | ||
|
|
48366bc595 | ||
|
|
02dd64382d | ||
|
|
731b48ad65 | ||
|
|
69a7082669 | ||
|
|
fafa7d8a51 | ||
|
|
6e69b5f014 | ||
|
|
03895248cd | ||
|
|
a79de8a325 | ||
|
|
740b53f02f | ||
|
|
76e75551c6 | ||
|
|
51752ae896 | ||
|
|
d81e773c0c | ||
|
|
1f1ed1ca70 | ||
|
|
df9f3806a3 | ||
|
|
081ccd9e2e | ||
|
|
d9a7dafe6e | ||
|
|
921225965b | ||
|
|
3659254785 | ||
|
|
7c502608f6 | ||
|
|
427c25f80b | ||
|
|
c3f15390ea | ||
|
|
e1476a44c6 | ||
|
|
72070fef3e | ||
|
|
b63ea677f6 | ||
|
|
f8389500b8 | ||
|
|
b257625a45 | ||
|
|
a233d8c7b3 | ||
|
|
11c9742ebe | ||
|
|
5af28c3dc2 | ||
|
|
aebb9e3c20 | ||
|
|
d5916f4ccc | ||
|
|
056159a3f3 | ||
|
|
2f6049d94b | ||
|
|
a2d7f664ab | ||
|
|
b26491b646 | ||
|
|
22e97b0eec | ||
|
|
f540866d08 | ||
|
|
c9729c8214 | ||
|
|
49901cbbee | ||
|
|
2aa2b80935 | ||
|
|
95695f4f58 | ||
|
|
b641d2b5e8 | ||
|
|
51d8faab12 | ||
|
|
7a1396b99d | ||
|
|
819bbbcaf4 | ||
|
|
0eeda96670 | ||
|
|
18cca4ffdd | ||
|
|
d169fe2dff | ||
|
|
1c732dfe17 | ||
|
|
bdad3927e5 | ||
|
|
0e0d6416d9 | ||
|
|
0da780371a | ||
|
|
37bf30cbc0 | ||
|
|
817a8601dd | ||
|
|
47cca409be | ||
|
|
e08a82ec39 | ||
|
|
345541dfb5 | ||
|
|
6e89762f1d | ||
|
|
0fb26bc16a | ||
|
|
f1ad5966af | ||
|
|
ac57d4683f | ||
|
|
eab099e5ee | ||
|
|
685bd3491d | ||
|
|
b8d64f3a9e | ||
|
|
852d491030 | ||
|
|
76565c5546 | ||
|
|
af1ec1630e | ||
|
|
0c2b36a206 | ||
|
|
c0934096f0 | ||
|
|
819bfaba90 | ||
|
|
8041a1296b | ||
|
|
10d93b4fd3 | ||
|
|
19dedef1e6 | ||
|
|
d4af0c7e8b | ||
|
|
8730f0fd38 | ||
|
|
9cda8daf65 | ||
|
|
a9223f1613 | ||
|
|
04ca4c99b8 | ||
|
|
3072520e63 | ||
|
|
bd6603766b | ||
|
|
075a23bd2b | ||
|
|
a8e4f653ed | ||
|
|
374a44f4a9 | ||
|
|
3c8d2e646e | ||
|
|
e5df983244 | ||
|
|
fa5f9250c4 | ||
|
|
3f7a831690 | ||
|
|
89aaaddae9 | ||
|
|
e1919616c2 | ||
|
|
8b9e637006 | ||
|
|
0df3e32901 | ||
|
|
1c2fa174ea | ||
|
|
c97aefcef1 | ||
|
|
dfb94c3993 | ||
|
|
7d62f69f12 | ||
|
|
cf896767fb | ||
|
|
1eb4cf71ed | ||
|
|
e959124eac | ||
|
|
d787c72812 | ||
|
|
9f0dd56d43 | ||
|
|
aa71e6045a | ||
|
|
a140ad83cd | ||
|
|
93c2d731e8 | ||
|
|
d8da553af9 | ||
|
|
9d9f070908 | ||
|
|
0f2061af55 | ||
|
|
d8423584d4 | ||
|
|
843320d268 | ||
|
|
216128b15a | ||
|
|
f8bc574753 | ||
|
|
6193c5933f | ||
|
|
b668965bda | ||
|
|
ae039b5baf | ||
|
|
824d43f16e | ||
|
|
2de76e6c5e | ||
|
|
afb02602fd | ||
|
|
99528c2bcf | ||
|
|
b53f5821f3 | ||
|
|
93fc6547b8 | ||
|
|
9a7e321dff | ||
|
|
39257f2d39 | ||
|
|
8c5abecac3 | ||
|
|
16dcc96037 | ||
|
|
b1d32a7745 | ||
|
|
631a2f53ea | ||
|
|
32903c97e3 | ||
|
|
6e61e8122d | ||
|
|
d109803f9d | ||
|
|
09ed4f57cf | ||
|
|
acfb8078a9 | ||
|
|
84f9693833 | ||
|
|
50fdcf486d | ||
|
|
eab5afccc8 | ||
|
|
ea9db47c2d | ||
|
|
cf3a9c5b43 | ||
|
|
adedaa092c | ||
|
|
f204237a63 | ||
|
|
057a400041 | ||
|
|
4cdf68f074 | ||
|
|
003a11c557 | ||
|
|
8d309fa579 | ||
|
|
232f9c24db | ||
|
|
39dccd149b | ||
|
|
b921c73fa7 | ||
|
|
f3ec1cbe93 | ||
|
|
a6bcfda0ac | ||
|
|
51cd2002af | ||
|
|
b40f41f41c | ||
|
|
4c33b30f14 | ||
|
|
b7490afb99 | ||
|
|
8b57ed727c | ||
|
|
fd5d64b9fb | ||
|
|
00af152c2c | ||
|
|
31f0abc8c8 | ||
|
|
6b7d795a31 | ||
|
|
1f093c4bc2 | ||
|
|
fe1c4a1ad0 | ||
|
|
11687cb7ba | ||
|
|
b07a7fb0cc | ||
|
|
b876d87ba9 | ||
|
|
0a63e89633 | ||
|
|
848f5609c2 | ||
|
|
0ccbed6165 | ||
|
|
646517db71 | ||
|
|
7d347bb80a | ||
|
|
e199d4f5eb | ||
|
|
a9767b58c4 | ||
|
|
69dfde047e | ||
|
|
da33b6f1b9 | ||
|
|
8a7125358b | ||
|
|
ae558052f7 | ||
|
|
5074d71eb7 | ||
|
|
632f42477a | ||
|
|
b3df38d15e | ||
|
|
b76b8ca718 | ||
|
|
d66a9e745b | ||
|
|
717bbccea3 | ||
|
|
50fd1c0410 | ||
|
|
ae89788ea4 | ||
|
|
4220b095ee | ||
|
|
ef28341cdb | ||
|
|
b5d610728c | ||
|
|
bc238ef476 | ||
|
|
feb3544014 | ||
|
|
31322dc0cd |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
18
.github/workflows/greetings.yml
vendored
Normal file
18
.github/workflows/greetings.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Greetings
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue_message: "Dependabot's first issue"
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
# config
|
||||
config.ini
|
||||
config_new.ini
|
||||
ini_merge_log.txt
|
||||
|
||||
# Pickle files
|
||||
*.pkl
|
||||
@@ -27,4 +29,8 @@ data/qrz.db
|
||||
bee.txt
|
||||
|
||||
# .csv files
|
||||
*.csv
|
||||
*.csv
|
||||
|
||||
# modules/custom_scheduler.py
|
||||
modules/custom_scheduler.py
|
||||
|
||||
|
||||
183
INSTALL.md
Normal file
183
INSTALL.md
Normal file
@@ -0,0 +1,183 @@
|
||||
# INSTALL.md
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [install.sh](#installsh)
|
||||
- [Purpose](#purpose)
|
||||
- [Usage](#usage)
|
||||
- [What it does](#what-it-does)
|
||||
- [When to use](#when-to-use)
|
||||
- [Note](#note)
|
||||
- [update.sh](#updatesh)
|
||||
- [Purpose](#purpose-1)
|
||||
- [Usage](#usage-1)
|
||||
- [What it does](#what-it-does-1)
|
||||
- [When to use](#when-to-use-1)
|
||||
- [Note](#note-1)
|
||||
- [launch.sh](#launchsh)
|
||||
- [Purpose](#purpose-2)
|
||||
- [How to Use](#how-to-use)
|
||||
- [What it does](#what-it-does-2)
|
||||
- [Note](#note-2)
|
||||
|
||||
---
|
||||
|
||||
### Manual Install
|
||||
Install the required dependencies using pip:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Copy the configuration template to `config.ini` and edit it to suit your needs:
|
||||
```sh
|
||||
cp config.template config.ini
|
||||
```
|
||||
|
||||
|
||||
### Docker Installation - handy for windows
|
||||
See further info on the [docker.md](script/docker/README.md)
|
||||
### Requirements
|
||||
Python 3.8? or later is needed (docker on 3.13). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
|
||||
|
||||
```sh
|
||||
pip install meshtastic
|
||||
pip install pubsub
|
||||
```
|
||||
|
||||
Mesh-bot enhancements:
|
||||
|
||||
```sh
|
||||
pip install pyephem
|
||||
pip install requests
|
||||
pip install geopy
|
||||
pip install maidenhead
|
||||
pip install beautifulsoup4
|
||||
pip install dadjokes
|
||||
pip install schedule
|
||||
pip install wikipedia
|
||||
```
|
||||
|
||||
For the Ollama LLM:
|
||||
|
||||
```sh
|
||||
pip install googlesearch-python
|
||||
```
|
||||
|
||||
To enable emoji in the Debian console, install the fonts:
|
||||
|
||||
```sh
|
||||
sudo apt-get install fonts-noto-color-emoji
|
||||
```
|
||||
|
||||
## install.sh
|
||||
|
||||
### Purpose
|
||||
`install.sh` is an installation and setup script for the Meshing Around Bot project. It automates installing dependencies, configuring the environment, setting up system services, and preparing the bot for use on Linux systems (especially Debian/Ubuntu/Raspberry Pi and embedded devices).
|
||||
|
||||
### Usage
|
||||
Run this script from the project root directory:
|
||||
```sh
|
||||
bash install.sh
|
||||
```
|
||||
|
||||
### What it does
|
||||
- Checks for existing installations and required permissions.
|
||||
- Optionally moves the project to `/opt/meshing-around` for standardization.
|
||||
- Installs Python and pip if not present (unless on embedded systems).
|
||||
- Adds the current user (or a dedicated `meshbot` user) to necessary groups for serial and Bluetooth access.
|
||||
- Copies and configures systemd service files for running the bot as a service.
|
||||
- Sets up configuration files, updating latitude/longitude automatically.
|
||||
- Offers to create and activate a Python virtual environment, or install dependencies system-wide.
|
||||
- Installs optional components (emoji fonts, Ollama LLM) if desired.
|
||||
- Sets permissions for log and data directories.
|
||||
- Optionally installs and enables the bot as a systemd service.
|
||||
- Provides post-installation notes and commands in `install_notes.txt`.
|
||||
- Offers to reboot the system to complete setup.
|
||||
|
||||
### When to use
|
||||
- For first-time installation of the Meshing Around Bot.
|
||||
- When migrating to a new device or environment.
|
||||
- After cloning or updating the repository to set up dependencies and services.
|
||||
|
||||
### Note
|
||||
- You may be prompted for input during installation (e.g., for embedded mode, virtual environment, or optional features).
|
||||
- Review and edit the script if you have custom requirements or are running on a non-standard system.
|
||||
|
||||
---
|
||||
|
||||
## update.sh
|
||||
|
||||
### Purpose
|
||||
`update.sh` is an update and maintenance script for the Meshing Around Bot project. It automates the process of safely updating your codebase, backing up data, and merging configuration changes.
|
||||
|
||||
### Usage
|
||||
Run this script from the project root directory:
|
||||
```sh
|
||||
bash update.sh
|
||||
```
|
||||
Or, after making it executable:
|
||||
```sh
|
||||
chmod +x update.sh
|
||||
./update.sh
|
||||
```
|
||||
|
||||
### What it does
|
||||
- Stops running Mesh Bot services to prevent conflicts during update.
|
||||
- Fetches and pulls the latest changes from the GitHub repository (using `git pull --rebase`).
|
||||
- Handles git conflicts, offering to reset to the latest remote version if needed.
|
||||
- Copies a custom scheduler template if not already present.
|
||||
- Backs up the `data/` directory (and `custom_scheduler.py` if present) to a compressed archive.
|
||||
- Merges your existing configuration with new defaults using `script/configMerge.py`, and logs the process.
|
||||
- Restarts services if they were stopped for the update.
|
||||
- Provides status messages and logs for troubleshooting.
|
||||
|
||||
### When to use
|
||||
- To update your Mesh Bot installation to the latest version.
|
||||
- Before making significant changes or troubleshooting, as it creates a backup of your data.
|
||||
|
||||
### Note
|
||||
- Review `ini_merge_log.txt` and `config_new.ini` after running for any configuration changes or errors.
|
||||
- You may be prompted if git conflicts are detected.
|
||||
|
||||
---
|
||||
|
||||
## launch.sh
|
||||
|
||||
### Purpose
|
||||
`launch.sh` is a convenience script for starting the Mesh Bot, Pong Bot, or generating reports within the Python virtual environment. It ensures the correct environment is activated and the appropriate script is run.
|
||||
|
||||
### How to Use
|
||||
From your project root, run one of the following commands:
|
||||
|
||||
- Launch Mesh Bot:
|
||||
```sh
|
||||
bash launch.sh mesh
|
||||
```
|
||||
- Launch Pong Bot:
|
||||
```sh
|
||||
bash launch.sh pong
|
||||
```
|
||||
- Generate HTML report:
|
||||
```sh
|
||||
bash launch.sh html
|
||||
```
|
||||
- Generate HTML5 report:
|
||||
```sh
|
||||
bash launch.sh html5
|
||||
```
|
||||
- Add a favorite (calls `script/addFav.py`):
|
||||
```sh
|
||||
bash launch.sh add
|
||||
```
|
||||
|
||||
### What it does
|
||||
- Ensures you are in the project directory.
|
||||
- Copies `config.template` to `config.ini` if no config exists.
|
||||
- Activates the Python virtual environment (`venv`).
|
||||
- Runs the selected Python script based on your argument.
|
||||
- Deactivates the virtual environment when done.
|
||||
|
||||
### Note
|
||||
- The script requires a Python virtual environment (`venv`) to be present in the project directory.
|
||||
- If `venv` is missing, the script will exit with an error message.
|
||||
- Always provide an argument (`mesh`, `pong`, `html`, `html5`, or `add`) to specify what you want to launch.
|
||||
595
README.md
595
README.md
@@ -1,90 +1,103 @@
|
||||
# Mesh Bot for Network Testing and BBS Activities
|
||||
|
||||
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, [mesh_bot.py](mesh_bot.py) has you covered.
|
||||
Mesh Bot is a feature-rich Python bot designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience. It provides powerful tools for network testing, messaging, games, and more—all via text-based message delivery. Whether you want to test your mesh, send messages, or play games, [mesh_bot.py](mesh_bot.py) has you covered.
|
||||
|
||||
* [Getting Started](#getting-started)
|
||||
* [Configuration](#configuration-guide)
|
||||
|
||||

|
||||
#### TLDR
|
||||
* [install.sh](INSTALL.md)
|
||||
* [modules/README.md](modules/README.md)
|
||||
* [modules/games/README.md](modules/games/README.md)
|
||||
|
||||
## Key Features
|
||||

|
||||
|
||||
### Intelligent Keyword Responder
|
||||
- **Automated Responses**: The bot detects 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.
|
||||
- **Emergency Response**: Monitor channels for keywords indicating emergencies and alert a wider audience.
|
||||
- **New Node Hello**: Greet new nodes on the mesh with a hello message
|
||||
- **Automated Responses**: Detects keywords like "ping" and replies with "pong" in direct messages (DMs) or group channels.
|
||||
- **Customizable Triggers**: Monitors group channels for specific keywords and sends custom responses.
|
||||
- **Emergency Detection**: Watches for emergency-related keywords and alerts a wider audience.
|
||||
- **New Node Greetings**: Automatically welcomes new nodes joining the mesh.
|
||||
|
||||
### 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
|
||||
- **Network Monitoring**: Alert on noisy nodes, node locations, and best placment for relay nodes.
|
||||
- **Mesh Testing**: Use `ping` to test message delivery with realistic packets.
|
||||
- **Hardware Testing**: The `test` command sends incrementally sized data to test radio buffer limits.
|
||||
- **Network Monitoring**: Alerts for noisy nodes, tracks node locations, and suggests optimal relay placement.
|
||||
|
||||
### Multi Radio/Node Support
|
||||
- **Simultaneous Monitoring**: Monitor up to nine networks at the same time.
|
||||
- **Flexible Messaging**: send mail and messages, between networks.
|
||||
### Multi-Radio/Node Support
|
||||
- **Simultaneous Monitoring**: Observe up to nine networks at once.
|
||||
- **Flexible Messaging**: Send mail and messages between networks.
|
||||
|
||||
### Advanced Messaging Capabilities
|
||||
- **Mail Messaging**: Leave messages for other devices, which are sent as DMs when the device is seen.
|
||||
- **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.
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS(Email) expanding visibility.
|
||||
- **New Node Hello**: Send a hello to any new node seen in text message.
|
||||
- **Mail Messaging**: Leave messages for other devices; delivered as DMs when the device is next seen. Use `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
|
||||
- **Message Scheduler**: Automate messages such as weather updates or net reminders.
|
||||
- **Store and Forward**: Retrieve missed messages with the `messages` command; optionally log messages to disk.
|
||||
- **BBS Linking**: Connect multiple bots to expand BBS coverage.
|
||||
- **E-Mail/SMS Integration**: Send mesh messages to email or SMS for broader reach.
|
||||
- **New Node Greetings**: Automatically greet new nodes via text.
|
||||
|
||||
### Interactive AI and Data Lookup
|
||||
- **NOAA/USGS location Data**: Get localized weather(alerts), Earthquake, River Flow, and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
|
||||
- **Wiki Integration**: Look up data using Wikipedia results.
|
||||
- **Ollama LLM AI**: Interact with the [Ollama](https://github.com/ollama/ollama/tree/main/docs) LLM AI for advanced queries and responses.
|
||||
- **Satellite Pass Info**: Get passes for satellite at your location.
|
||||
- **GeoMeasuring**: HowFar from point to point using collected GPS packets on the bot to plot a course or space. Find Center of points for Fox&Hound direction finding.
|
||||
- **Weather, Earthquake, River, and Tide Data**: Get local alerts and info from NOAA/USGS; uses Open-Meteo for areas outside NOAA coverage.
|
||||
- **Wikipedia Search**: Retrieve summaries from Wikipedia.
|
||||
- **Ollama LLM Integration**: Query the [Ollama](https://github.com/ollama/ollama/tree/main/docs) AI for advanced responses.
|
||||
- **Satellite Passes**: Find upcoming satellite passes for your location.
|
||||
- **GeoMeasuring Tools**: Calculate distances and midpoints using collected GPS data; supports Fox & Hound direction finding.
|
||||
|
||||
### Proximity Alerts
|
||||
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
|
||||
- **High Flying Alerts**: Get notified when nodes with high altitude are seen on mesh
|
||||
- **Location-Based Alerts**: Get notified when members arrive at a configured latitude/longitude—ideal for campsites, geo-fences, or remote locations. Optionally, trigger scripts, send emails, or automate actions (e.g., change node config, turn on lights, or drop an `alert.txt` file to start a survey or game).
|
||||
- **Customizable Triggers**: Use proximity events for creative applications like "king of the hill" or 🧭 geocache games by adjusting the alert cycle.
|
||||
- **High Flying Alerts**: Receive notifications when nodes with high altitude are detected on the mesh.
|
||||
- **Voice/Command Triggers**: Activate bot functions using keywords or voice commands (see [Voice Commands](#voice-commands-vox) for "Hey Chirpy!" support).
|
||||
|
||||
### CheckList / Check In Out
|
||||
- **Asset Tracking**: Maintain a list of node/asset checkin and checkout. Useful foraccountability of people, assets. Radio-Net, FEMA, Trailhead.
|
||||
#### Radio Frequency Monitoring
|
||||
- **SNR RF Activity Alerts**: Monitor radio frequencies and receive alerts when high SNR (Signal-to-Noise Ratio) activity is detected.
|
||||
- **Hamlib Integration**: Use Hamlib (rigctld) to monitor the S meter on a connected radio.
|
||||
- **Speech-to-Text Broadcasting**: Convert received audio to text using [Vosk](https://alphacephei.com/vosk/models) and broadcast it to the mesh.
|
||||
|
||||
### Check-In / Check-Out & Asset Tracking
|
||||
- **Asset Tracking**: Maintain a check-in/check-out list for nodes or assets—ideal for accountability of people and equipment (e.g., Radio-Net, FEMA, trailhead groups).
|
||||
|
||||
### Fun and Games
|
||||
- **Built-in Games**: Enjoy games like DopeWars, Lemonade Stand, BlackJack, and VideoPoker.
|
||||
- **FCC ARRL QuizBot**: The exam question pool quiz-bot.
|
||||
- **Command-Based Gameplay**: Issue `games` to display help and start playing.
|
||||
- **Telemetry Leaderboard**: Fun stats like lowest 🪫 battery or coldest temp 🥶
|
||||
- **Built-in Games**: Play classic games like DopeWars, Lemonade Stand, BlackJack, and Video Poker directly via DM.
|
||||
- **FCC ARRL QuizBot**: Practice for the ham radio exam with the integrated quiz bot.
|
||||
- **Command-Based Gameplay**: Use the `games` command to view available games and start playing.
|
||||
- **Telemetry Leaderboard**: Compete for fun stats like lowest battery or coldest temperature.
|
||||
|
||||
#### QuizMaster
|
||||
- **Interactive Group Quizzes**: The QuizMaster module allows admins to start and stop quiz games for groups. Players can join, leave, and answer questions directly via DM or channel.
|
||||
- **Scoring and Leaderboards**: Players can check their scores and see the top performers with `q: score` and `q: top`.
|
||||
- **Easy Participation**: Players answer questions by prefixing their answer with `q:`, e.g., `q: 42`.
|
||||
- **Group Quizzes**: Admins can start and stop quiz games for groups.
|
||||
- **Player Participation**: Players join with `q: join`, leave with `q: leave`, and answer questions by prefixing their answer with `q:`, e.g., `q: 42`.
|
||||
- **Scoring & Leaderboards**: Check your score with `q: score` and see the top performers with `q: top`.
|
||||
- **Admin Controls**: QuizMasters (from `bbs_admin_list`) can use `q: start`, `q: stop`, and `q: broadcast <message>` to manage games.
|
||||
|
||||
#### Survey Module
|
||||
- **Custom Surveys**: Easily create and deploy custom surveys by editing JSON files in `data/survey`. Multiple surveys can be managed (e.g., `survey snow`).
|
||||
- **User Feedback Collection**: Users can participate in surveys via DM, and responses are logged for later review.
|
||||
|
||||
### Radio Frequency Monitoring
|
||||
- **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.
|
||||
- **Custom Surveys**: Create and manage surveys by editing JSON files in `data/survey`. Multiple surveys are supported (e.g., `survey snow`).
|
||||
- **User Feedback**: Users participate via DM; responses are logged for review.
|
||||
- **Reporting**: Retrieve survey results with `survey report` or `survey report <surveyname>`.
|
||||
|
||||
### EAS Alerts
|
||||
- **FEMA iPAWS/EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from FEMA
|
||||
- **NOAA EAS Alerts via API**: Use an internet-connected node to message Emergency Alerts from NOAA.
|
||||
- **USGS Volcano Alerts via API**: Use an internet-connected node to message Emergency Alerts from USGS.
|
||||
- **EAS Alerts over the air**: Utilizing external tools to report EAS alerts offline over mesh.
|
||||
- **NINA alerts for Germany**: Emergency Alerts from xrepository.de feed
|
||||
- **FEMA iPAWS/EAS Alerts**: Receive Emergency Alerts from FEMA via API on internet-connected nodes.
|
||||
- **NOAA EAS Alerts**: Get Emergency Alerts from NOAA via API.
|
||||
- **USGS Volcano Alerts**: Receive volcano alerts from USGS via API.
|
||||
- **Offline EAS Alerts**: Report EAS alerts over the mesh using external tools, even without internet.
|
||||
- **NINA Alerts (Germany)**: Receive emergency alerts from the xrepository.de feed for Germany.
|
||||
|
||||
### File Monitor Alerts
|
||||
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
|
||||
- **News File**: On request of news, the contents of the file are returned. Can also call multiple news sources or files.
|
||||
- **Shell Command Access**: Pass commands via DM directly to the host OS with replay protection.
|
||||
- **File Monitoring**: Watch a text file for changes and broadcast updates to the mesh channel.
|
||||
- **News File Access**: Retrieve the contents of a news file on request; supports multiple news sources or files.
|
||||
- **Shell Command Access**: Execute shell commands via DM with replay protection (admin only).
|
||||
|
||||
### Data Reporting
|
||||
- **HTML Generator**: Visualize bot traffic and data flows with a built-in HTML generator for [data reporting](logs/README.md).
|
||||
- **RSS and news feeds**: Get data in mesh from many sources!
|
||||
- **HTML Reports**: Visualize bot traffic and data flows with a built-in HTML generator. See [data reporting](logs/README.md) for details.
|
||||
- **RSS & News Feeds**: Receive news and data from multiple sources directly on the mesh.
|
||||
|
||||
### Robust Message Handling
|
||||
- **Message Chunking**: Automatically chunk messages over 160 characters to ensure higher delivery success across hops.
|
||||
- **Automatic Message Chunking**: Messages over 160 characters are automatically split to ensure reliable delivery across multiple hops.
|
||||
|
||||
## Getting Started
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, see projects for embedding, [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), also see [femtofox](https://github.com/noon92/femtofox) for running on luckfox hardware. If you need a local console consider the [firefly](https://github.com/pdxlocations/firefly) project. 🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, see projects for embedding, armbian or [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), also see [femtofox](https://github.com/noon92/femtofox) for running on luckfox hardware. If you need a local console consider the [firefly](https://github.com/pdxlocations/firefly) project.
|
||||
|
||||
🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
|
||||
|
||||
### Quick Setup
|
||||
#### Clone the Repository
|
||||
@@ -92,450 +105,26 @@ If you dont have git you will need it `sudo apt-get install git`
|
||||
```sh
|
||||
git clone https://github.com/spudgunman/meshing-around
|
||||
```
|
||||
- **Automated Installation**: `install.sh` will automate optional venv and requirements installation.
|
||||
- **Launch Script**: `launch.sh` only used in a venv install, to launch the bot and the report generator.
|
||||
|
||||
## Full list of commands for the bot
|
||||
|
||||
### Networking
|
||||
| Command | Description | ✅ Works Off-Grid |
|
||||
|---------|-------------|-
|
||||
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15 via DM only) | ✅ |
|
||||
| `cmd` | Returns the list of commands (the help message) | ✅ |
|
||||
| `history` | Returns the last commands run by user(s) | ✅ |
|
||||
| `leaderboard` | Shows extreme mesh metrics like lowest battery 🪫 `leaderboard reset` allows admin reset | ✅ |
|
||||
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
|
||||
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
|
||||
| `sysinfo` | Returns the bot node telemetry info | ✅ |
|
||||
| `test` | used to test the limits of data transfer (`test 4` sends data to the maxBuffer limit default 200 charcters) via DM only | ✅ |
|
||||
| `whereami` | Returns the address of the sender's location if known |
|
||||
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
|
||||
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
|
||||
| `echo` | Echo string back, disabled by default | ✅ |
|
||||
|
||||
### Radio Propagation & Weather Forecasting
|
||||
| Command | Description | |
|
||||
|---------|-------------|-------------------
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or DE Headline or expanded details for USA | |
|
||||
| `earthquake` | Returns the largest and number of USGS events for the location | |
|
||||
| `hfcond` | Returns a table of HF solar conditions | |
|
||||
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
|
||||
| `riverflow` | Return information from NOAA for river flow info. | |
|
||||
| `solar` | Gives an idea of the x-ray flux | |
|
||||
| `sun` and `moon` | Return info on rise and set local time | ✅ |
|
||||
| `tide` | Returns the local tides (NOAA data source) | |
|
||||
| `valert` | Returns USGS Volcano Data | |
|
||||
| `wx` | Return local weather forecast, NOAA or Open Meteo (which also has `wxc` for metric and imperial) | |
|
||||
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | |
|
||||
| `mwx` | Return the NOAA Coastal Marine Forecast data | |
|
||||
|
||||
### Bulletin Board & Mail
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `bbshelp` | Returns the following help message | ✅ |
|
||||
| `bbslist` | Lists the messages by ID and subject | ✅ |
|
||||
| `bbsread` | Reads a message. Example: `bbsread #1` | ✅ |
|
||||
| `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) | ✅ |
|
||||
| `bbslink` | Links Bulletin Messages between BBS Systems | ✅ |
|
||||
| `email:` | Sends email to address on file for the node or `email: bob@test.net # hello from mesh` | |
|
||||
| `sms:` | Send sms-email to multiple address on file | |
|
||||
| `setemail`| Sets the email for easy communications | |
|
||||
| `setsms` | Adds the SMS-Email for quick communications | |
|
||||
| `clearsms` | Clears all SMS-Emails on file for node | |
|
||||
|
||||
### Data Lookup
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
|
||||
| `messages` | Replays the last messages heard on device, like Store and Forward, returns the PublicChannel and Current | ✅ |
|
||||
| `readnews` | returns the contents of a file (data/news.txt, by default) can also `news mesh` via the chunker on air | ✅ |
|
||||
| `readrss` | returns a set RSS feed on air | |
|
||||
| `satpass` | returns the pass info from API for defined NORAD ID in config or Example: `satpass 25544,33591`| |
|
||||
| `wiki:` | Searches Wikipedia (or local Kiwix server) and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
|
||||
| `howfar` | returns the distance you have traveled since your last HowFar. `howfar reset` to start over | ✅ |
|
||||
| `howtall` | returns height of something you give a shadow by using sun angle | ✅ |
|
||||
|
||||
### CheckList
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `checkin` | Check in the node to the checklist database, you can add a note like `checkin ICO` or `checkin radio4` | ✅ |
|
||||
| `checkout` | Checkout the node in the checklist database, checkout all from node | ✅ |
|
||||
| `checklist` | Display the checklist database, with note | ✅ |
|
||||
|
||||
### Games (via DM only)
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
|
||||
| `dopewars` | Plays the classic drug trader game | ✅ |
|
||||
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
|
||||
| `hamtest` | FCC/ARRL Quiz `hamtest general` or `hamtest extra` and `score` | ✅ |
|
||||
| `hangman` | Plays the classic word guess game | ✅ |
|
||||
| `joke` | Tells a joke | |
|
||||
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
|
||||
| `mastermind` | Plays the classic code-breaking game | ✅ |
|
||||
| `survey` | Issues out a survey to the user | ✅ |
|
||||
| `quiz` | QuizMaster Bot `q: ?` for more | ✅ |
|
||||
| `tic-tac-toe`| Plays the game classic game | ✅ |
|
||||
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
|
||||
|
||||
#### QuizMaster
|
||||
To use QuizMaster the bbs_admin_list is the QuizMaster, who can `q: start` and `q: stop` to start and stop the game, `q: broadcast <message>` to send a message to all players.
|
||||
Players can `q: join` to join the game, `q: leave` to leave the game, `q: score` to see their score, and `q: top` to see the top 3 players.
|
||||
To Answer a question, just type the answer prefixed with `q: <answer>`
|
||||
|
||||
#### Survey
|
||||
To use the Survey feature edit the json files in data/survey multiple surveys are possible such as `survey snow`
|
||||
|
||||
## Other Install Options
|
||||
- **Automated Installation**: [install.sh](INSTALL.md) will automate optional venv and requirements installation.
|
||||
- **Launch Script**: [laynch.sh](INSTALL.md) only used in a venv install, to launch the bot and the report generator.
|
||||
|
||||
### Docker Installation - handy for windows
|
||||
See further info on the [docker.md](script/docker/README.md)
|
||||
|
||||
### Manual Install
|
||||
Install the required dependencies using pip:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
## Full list of commands for the bot
|
||||
[modules/README.md](modules/README.md)
|
||||
|
||||
Copy the configuration template to `config.ini` and edit it to suit your needs:
|
||||
```sh
|
||||
cp config.template config.ini
|
||||
```
|
||||
|
||||
### Configuration Guide
|
||||
The following is documentation for the config.ini file
|
||||
|
||||
If you have not done so, or want to 'factory reset', copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
|
||||
```sh
|
||||
meshtastic --ble-scan
|
||||
```
|
||||
|
||||
**Note**: The code has been tested with a single BLE device and is written to support only one BLE port.
|
||||
|
||||
```ini
|
||||
# config.ini
|
||||
# type can be serial, tcp, or ble.
|
||||
# port is the serial port to use; commented out will try to auto-detect
|
||||
# hostname is the IP address of the device to connect to for TCP type
|
||||
# mac is the MAC address of the device to connect to for BLE type
|
||||
|
||||
[interface]
|
||||
type = serial
|
||||
# port = '/dev/ttyUSB0'
|
||||
# hostname = 192.168.0.1
|
||||
# mac = 00:11:22:33:44:55
|
||||
|
||||
# Additional interface for dual radio support. See config.template for more.
|
||||
[interface2]
|
||||
enabled = False
|
||||
```
|
||||
|
||||
### General Settings
|
||||
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index. You can also have the bot ignore the defaultChannel for any commands, but still observe the channel.
|
||||
|
||||
```ini
|
||||
[general]
|
||||
respond_by_dm_only = True
|
||||
defaultChannel = 0
|
||||
ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
ignoreChannels = # ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
|
||||
cmdBang = False # require ! to be the first character in a command
|
||||
explicitCmd = True # require explicit command, the message will only be processed if it starts with a command word disable to get more activity
|
||||
```
|
||||
|
||||
### Location Settings
|
||||
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, as well as the default for all NOAA, repeater lookup. It is also the center of radius for Sentry.
|
||||
|
||||
```ini
|
||||
[location]
|
||||
enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
UseMeteoWxAPI = True
|
||||
|
||||
coastalEnabled = False # NOAA Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
|
||||
# Find the correct coastal weather directory at https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/
|
||||
# this map can help https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map'
|
||||
myCoastalZone = https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt # myCoastalZone is the .txt file with the forecast data
|
||||
coastalForecastDays = 3 # number of data points to return, default is 3
|
||||
```
|
||||
|
||||
### Module Settings
|
||||
Modules can be enabled or disabled as needed. They are essentally larger functions of code which you may not want on your mesh or in memory space.
|
||||
|
||||
```ini
|
||||
[bbs]
|
||||
enabled = False
|
||||
|
||||
[general]
|
||||
DadJokes = False
|
||||
StoreForward = False
|
||||
```
|
||||
|
||||
### History
|
||||
The history command shows the last commands the user ran, and [`lheard`] reflects the last users on the bot.
|
||||
|
||||
```ini
|
||||
enableCmdHistory = True # history command enabler
|
||||
lheardCmdIgnoreNodes = # command history ignore list ex: 2813308004,4258675309
|
||||
```
|
||||
|
||||
### Sentry Settings
|
||||
|
||||
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
|
||||
emailSentryAlerts = True # if SMTP enabled send alert to sysop email list
|
||||
SentryRadius = 100 # radius in meters to detect someone close to the bot
|
||||
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
|
||||
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
highFlyingAlert = True # HighFlying Node alert
|
||||
highFlyingAlertAltitude = 2000 # Altitude in meters to trigger the alert
|
||||
highflyOpenskynetwork = True # check with OpenSkyNetwork if highfly detected for aircraft
|
||||
```
|
||||
|
||||
### E-Mail / SMS Settings
|
||||
To enable connectivity with SMTP allows messages from meshtastic into SMTP. The term SMS here is for connection via [carrier email](https://avtech.com/articles/138/list-of-email-to-sms-addresses/)
|
||||
|
||||
```ini
|
||||
[smtp]
|
||||
# enable or disable the SMTP module, minimum required for outbound notifications
|
||||
enableSMTP = True # enable or disable the IMAP module for inbound email, not implemented yet
|
||||
enableImap = False # list of Sysop Emails separate with commas, used only in emergency responder currently
|
||||
sysopEmails =
|
||||
# See config.template for all the SMTP settings
|
||||
SMTP_SERVER = smtp.gmail.com
|
||||
SMTP_AUTH = True
|
||||
EMAIL_SUBJECT = Meshtastic✉️
|
||||
```
|
||||
|
||||
### Emergency Response Handler
|
||||
Traps the following ("emergency", "911", "112", "999", "police", "fire", "ambulance", "rescue") keywords. Responds to the user, and calls attention to the text message in logs and via another network or channel.
|
||||
|
||||
```ini
|
||||
[emergencyHandler]
|
||||
enabled = True # enable or disable the emergency response handler
|
||||
alert_channel = 2 # channel to send a message to when the emergency handler is triggered
|
||||
alert_interface = 1
|
||||
```
|
||||
|
||||
### EAS Alerting
|
||||
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
|
||||
|
||||
#### FEMA iPAWS/EAS and NINA
|
||||
This uses USA: SAME, FIPS, to locate the alerts in the feed. By default ignoring Test messages.
|
||||
|
||||
```ini
|
||||
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
|
||||
eAlertBroadcastCh = 2,3 # Goverment Emergency IPAWS/CAP Alert Broadcast Channels
|
||||
ignoreFEMAenable = True # Ignore any headline that includes followig word list
|
||||
ignoreFEMAwords = test,exercise
|
||||
# comma separated list of FIPS codes to trigger local alert. find your FIPS codes at https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code
|
||||
myFIPSList = 57,58,53
|
||||
# find your SAME https://www.weather.gov/nwr/counties comma separated list of SAME code to further refine local alert.
|
||||
mySAMEList = 053029,053073
|
||||
|
||||
# To use other country services enable only a single optional serivce
|
||||
enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
|
||||
myRegionalKeysDE = 110000000000,120510000000
|
||||
```
|
||||
|
||||
#### NOAA EAS
|
||||
This uses the defined lat-long of the bot for collecting of data from the API. see [File-Monitoring](#File-Monitoring) for ideas to collect EAS alerts from a RTL-SDR.
|
||||
|
||||
```ini
|
||||
|
||||
wxAlertBroadcastEnabled = True # EAS Alert Broadcast
|
||||
wxAlertBroadcastCh = 2,4 # EAS Alert Broadcast Channels
|
||||
ignoreEASenable = True # Ignore any headline that includes followig word list
|
||||
ignoreEASwords = test,advisory
|
||||
```
|
||||
|
||||
#### USGS River flow data and Volcano alerts
|
||||
Using the USGS water data page locate a water flow device, for example Columbia River at Vancouver, WA - USGS-14144700
|
||||
|
||||
Volcano Alerts use lat/long to determine ~1000km radius
|
||||
```ini
|
||||
[location]
|
||||
# USGS Hydrology unique identifiers, LID or USGS ID https://waterdata.usgs.gov
|
||||
riverList = 14144700 # example Mouth of Columbia River
|
||||
|
||||
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
|
||||
volcanoAlertBroadcastEnabled = False
|
||||
volcanoAlertBroadcastCh = 2
|
||||
```
|
||||
|
||||
### Repeater Settings
|
||||
A repeater function for two different nodes and cross-posting messages. The `repeater_channels` is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
|
||||
|
||||
```ini
|
||||
[repeater] # repeater module
|
||||
enabled = True
|
||||
repeater_channels = [2, 3]
|
||||
```
|
||||
|
||||
### 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 `gemma3:270m`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server) works on a pi58GB with 40 second or less response time.
|
||||
|
||||
```ini
|
||||
# Enable ollama LLM see more at https://ollama.com
|
||||
ollama = True # Ollama model to use (defaults to gemma2:2b)
|
||||
ollamaModel = gemma3:latest # Ollama model to use (defaults to gemma3:270m)
|
||||
ollamaHostName = http://localhost:11434 # server instance to use (defaults to local machine install)
|
||||
```
|
||||
|
||||
Also see `llm.py` for changing the defaults of:
|
||||
|
||||
```ini
|
||||
# LLM System Variables
|
||||
rawQuery = True # if True, the input is sent raw to the LLM if False, it is processed by the meshBotAI template
|
||||
|
||||
# Used in the meshBotAI template (legacy)
|
||||
llmEnableHistory = True # enable history for the LLM model to use in responses adds to compute time
|
||||
llmContext_fromGoogle = True # enable context from google search results helps with responses accuracy
|
||||
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
|
||||
```
|
||||
Note for LLM in docker with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html). Needed for the container with ollama running.
|
||||
|
||||
### Wikipedia Search Settings
|
||||
The Wikipedia search module can use either the online Wikipedia API or a local Kiwix server for offline wiki access. Kiwix is especially useful for mesh networks operating in remote or offline environments.
|
||||
|
||||
```ini
|
||||
# Enable or disable the wikipedia search module
|
||||
wikipedia = True
|
||||
|
||||
# Use local Kiwix server instead of online Wikipedia
|
||||
# Set to False to use online Wikipedia (default)
|
||||
useKiwixServer = False
|
||||
|
||||
# Kiwix server URL (only used if useKiwixServer is True)
|
||||
kiwixURL = http://127.0.0.1:8080
|
||||
|
||||
# Kiwix library name (e.g., wikipedia_en_100_nopic_2024-06)
|
||||
# Find available libraries at https://library.kiwix.org/
|
||||
kiwixLibraryName = wikipedia_en_100_nopic_2024-06
|
||||
```
|
||||
|
||||
To set up a local Kiwix server:
|
||||
1. Install Kiwix tools: https://kiwix.org/en/ `sudo apt install kiwix-tools -y`
|
||||
2. Download a Wikipedia ZIM file to `data/`: https://library.kiwix.org/ `wget https://download.kiwix.org/zim/wikipedia/wikipedia_en_100_nopic_2025-09.zim`
|
||||
3. Run the server: `kiwix-serve --port 8080 wikipedia_en_100_nopic_2025-09.zim`
|
||||
4. Set `useKiwixServer = True` in your config.ini
|
||||
|
||||
The bot will automatically extract and truncate content to fit Meshtastic's message size limits (~500 characters).
|
||||
|
||||
### 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 = True
|
||||
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 # text file to monitor for changes
|
||||
broadcastCh = 2 # channel to send the message to can be 2,3 multiple channels comma separated
|
||||
enable_read_news = False # news command will return the contents of a text file
|
||||
news_file_path = news.txt
|
||||
news_random_line = False # only return a single random line from the news file
|
||||
enable_runShellCmd = False # enable the use of exernal shell commands, this enables some data in `sysinfo`
|
||||
# if runShellCmd and you think it is safe to allow the x: command to run
|
||||
# direct shell command handler the x: command in DMs user must be in bbs_admin_list
|
||||
allowXcmd = True
|
||||
```
|
||||
|
||||
#### Offline EAS
|
||||
|
||||
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
|
||||
Maintain multiple news sources. Each source should be a file named `{source}_news.txt` in the `data/` directory (for example, `data/mesh_news.txt`).
|
||||
- To read the default news, use the `readnews` command (reads from `data/news.txt`.
|
||||
- To read a specific source, use `readnews abc` to read from `data/abc_news.txt`.
|
||||
|
||||
This allows you to organize and access different news feeds or categories easily.
|
||||
External scripts can update these files as needed, and the bot will serve the latest content on request.
|
||||
|
||||
### Greet new nodes QRZ module
|
||||
This isnt QRZ.com this is Q code for who is calling me, this will track new nodes and say hello
|
||||
```ini
|
||||
[qrz]
|
||||
enabled = True # QRZ Hello to new nodes
|
||||
qrz_hello_string = "send CMD or DM me for more info." # will be sent to all heard nodes once
|
||||
training = True # Training mode will not send the hello message to new nodes, use this to build up database
|
||||
```
|
||||
|
||||
### Scheduler
|
||||
In the config.ini enable the module
|
||||
```ini
|
||||
[scheduler]
|
||||
enabled = False # enable or disable the scheduler module
|
||||
interface = 1 # channel to send the message to
|
||||
channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
interval = # interval to use when time is not set (e.g. every 2 days)
|
||||
time = # time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
```
|
||||
The basic brodcast message can be setup in condig.ini. For advanced, See mesh_bot.py around the bottom of file, line [1491](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1491) 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
|
||||
schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
|
||||
|
||||
#Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
|
||||
schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
```
|
||||
|
||||
#### BBS Link
|
||||
The scheduler also handles the BBS Link Broadcast message, this would be an example of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initiator, 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
|
||||
bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
```
|
||||
### Games (via DM only)
|
||||
[modules/games/README.md](modules/games/README.md)
|
||||
|
||||
### Firmware 2.6 DM Key, and 2.7 CLIENT_BASE Favorite Nodes
|
||||
Firmware 2.6 introduced [PKC](https://meshtastic.org/blog/introducing-new-public-key-cryptography-in-v2_5/), enabling secure private messaging by adding necessary keys to each node. To fully utilize this feature, you should add favorite nodes—such as BBS admins—to your node’s favorites list to ensure their keys are retained. A helper script is provided to simplify this process:
|
||||
- Run the helper script from the main program directory: `python3 script/addFav.py`
|
||||
- By default, this script adds nodes from `bbs_admin_list` and `bbslink_whitelist`
|
||||
- If using a virtual environment, run: `launch.sh addfav`
|
||||
- The API will not work-fully today to set nodes this is a WIP
|
||||
|
||||
Additionally, you can just DM a bot to "auto favorite." If your node is set to not be messageable, DMs won't work—be advised.
|
||||
|
||||
To configure favorite nodes, add their numbers to your config file:
|
||||
```conf
|
||||
@@ -579,43 +168,11 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
- **mikecarper**: ideas, and testing. hamtest
|
||||
- **c.merphy360**: high altitude alerts
|
||||
- **Iris**: testing and finding 🐞
|
||||
- **Cisien, bitflip, Woof, propstg, snydermesh, trs2982, FJRPilot, Josh, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **FJRPiolt**: testing bugs out!!
|
||||
- **Cisien, bitflip, Woof, propstg, snydermesh, trs2982, F0X, Malice, mesb1, 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)
|
||||
|
||||
### Requirements
|
||||
Python 3.8? or later is needed (docker on 3.13). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
|
||||
|
||||
```sh
|
||||
pip install meshtastic
|
||||
pip install pubsub
|
||||
```
|
||||
|
||||
Mesh-bot enhancements:
|
||||
|
||||
```sh
|
||||
pip install pyephem
|
||||
pip install requests
|
||||
pip install geopy
|
||||
pip install maidenhead
|
||||
pip install beautifulsoup4
|
||||
pip install dadjokes
|
||||
pip install schedule
|
||||
pip install wikipedia
|
||||
```
|
||||
|
||||
For the Ollama LLM:
|
||||
|
||||
```sh
|
||||
pip install googlesearch-python
|
||||
```
|
||||
|
||||
To enable emoji in the Debian console, install the fonts:
|
||||
|
||||
```sh
|
||||
sudo apt-get install fonts-noto-color-emoji
|
||||
```
|
||||
|
||||
Meshtastic® is a registered trademark of Meshtastic LLC. Meshtastic software components are released under various licenses, see GitHub for details. No warranty is provided - use at your own risk.
|
||||
|
||||
14
SECURITY.md
Normal file
14
SECURITY.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| git pull| :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If its serious, its likley big. otherwise post issues, reachout on discord.
|
||||
@@ -1,7 +1,7 @@
|
||||
#config.ini
|
||||
# type can be serial, tcp, or ble
|
||||
# port is the serial port to use, commented out will try to auto-detect
|
||||
# hostname is the IP address of the device to connect to for tcp type
|
||||
# hostname is the IP/DNS and port for tcp type default is host:4403
|
||||
# mac is the MAC address of the device to connect to for ble type
|
||||
|
||||
[interface]
|
||||
@@ -127,18 +127,28 @@ alert_interface = 1
|
||||
[sentry]
|
||||
# detect anyone close to the bot
|
||||
SentryEnabled = True
|
||||
emailSentryAlerts = False
|
||||
# radius in meters to detect someone close to the bot
|
||||
SentryRadius = 100
|
||||
# device interface and channel to send the alert message to
|
||||
SentryInterface = 1
|
||||
SentryChannel = 2
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 9
|
||||
emailSentryAlerts = False
|
||||
# Enable detection sensor alert, requires external GPIO sensor connected to node
|
||||
detectionSensorAlert = False
|
||||
|
||||
# list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
sentryIgnoreList =
|
||||
# Enable detection sensor alert, requires external sensor connected to node
|
||||
detectionSensorAlert = False
|
||||
# list of watched nodes numbers ex: 2813308004,4258675309
|
||||
sentryWatchList =
|
||||
|
||||
# radius in meters to detect someone close to the bot
|
||||
SentryRadius = 100
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 9
|
||||
|
||||
# Enable running external shell command when sentry alert is triggered
|
||||
cmdShellSentryAlerts = False
|
||||
# External shell command to run when sentry alert is triggered
|
||||
sentryAlertNear = sentry_alert_near.sh
|
||||
sentryAlertAway = sentry_alert_away.sh
|
||||
|
||||
# HighFlying Node alert
|
||||
highFlyingAlert = True
|
||||
@@ -171,6 +181,8 @@ bbsAPI_enabled = False
|
||||
enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
fuzzConfigLocation = True
|
||||
fuzzItAll = False
|
||||
|
||||
# Default to metric units rather than imperial
|
||||
useMetric = False
|
||||
@@ -236,7 +248,7 @@ enableDEalerts = False
|
||||
myRegionalKeysDE = 110000000000,120510000000
|
||||
|
||||
# Satalite Pass Prediction
|
||||
# Register for free API https://www.n2yo.com/login/
|
||||
# Register for free API https://www.n2yo.com/login/ personal data page at bottom 'Are you developer?'
|
||||
n2yoAPIKey =
|
||||
# NORAD list https://www.n2yo.com/satellites/
|
||||
satList = 25544,7530
|
||||
@@ -274,7 +286,9 @@ channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
# enable overides the above and uses the motd as the message
|
||||
schedulerMotd = False
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun.
|
||||
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
|
||||
# custom for module/scheduler.py custom schedule examples
|
||||
value =
|
||||
# interval to use when time is not set (e.g. every 2 days)
|
||||
interval =
|
||||
@@ -300,6 +314,13 @@ signalCycleLimit = 5
|
||||
voxDetectionEnabled = False
|
||||
# description to use in the alert message
|
||||
voxDescription = VOX
|
||||
useLocalVoxModel = False
|
||||
voxLanguage = en-us
|
||||
voxInputDevice = default
|
||||
voxOnTrapList = True
|
||||
voxTrapList = chirpy
|
||||
voxEnableCmd = True
|
||||
|
||||
|
||||
[fileMon]
|
||||
filemon_enabled = False
|
||||
@@ -364,12 +385,15 @@ golfsim = True
|
||||
hangman = True
|
||||
hamtest = True
|
||||
tictactoe = True
|
||||
wordOfTheDay = True
|
||||
|
||||
# enable or disable the quiz game module questions are in data/quiz.json
|
||||
quiz = False
|
||||
|
||||
# enable or disable the survey game module questions are in data/survey/survey.json
|
||||
# enable or disable the survey game module questions are in data/survey/*_survey.json
|
||||
survey = False
|
||||
# this is the default survey to use when command givcen, from data/survey/example_survey.json
|
||||
defaultSurvey = example
|
||||
# Whether to record user ID in responses
|
||||
surveyRecordID=True
|
||||
# Whether to record location on start of survey
|
||||
@@ -384,7 +408,7 @@ splitDelay = 2.5
|
||||
MESSAGE_CHUNK_SIZE = 160
|
||||
# Request Acknowledgement of message OTA
|
||||
wantAck = False
|
||||
# Max limit buffer for radio testing
|
||||
# Max limit buffer for radio testing in bytes
|
||||
maxBuffer = 200
|
||||
#Enable Extra logging of Hop count data
|
||||
enableHopLogs = False
|
||||
|
||||
75
etc/README.md
Normal file
75
etc/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# etc Directory
|
||||
|
||||
This folder contains supporting files and resources for the Mesh Bot project. Typical contents include:
|
||||
|
||||
- **Images**: Visual assets used in documentation (e.g., `pong-bot.jpg`).
|
||||
- **Custom Scripts**: Example or utility scripts for advanced configuration (e.g., `custom_scheduler.py` for scheduled tasks).
|
||||
- **tmp**: Temp files for install
|
||||
|
||||
## db_admin.py
|
||||
|
||||
**Purpose:**
|
||||
`db_admin.py` is a simple administrative tool for viewing the contents of the Mesh Bot’s data and high score databases. It loads and prints out messages, direct messages, email/SMS records, and game high score tables stored in the `/data` directory.
|
||||
|
||||
**Usage:**
|
||||
Run this script from the command line to display the current contents of the bot’s databases. This is useful for debugging, verifying data integrity, or reviewing stored messages and game scores.
|
||||
|
||||
```sh
|
||||
python etc/db_admin.py
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Attempts to load various `.pkl` and `.pickle` files from the `data` directory.
|
||||
- Prints out the contents of BBS messages, direct messages, email and SMS databases.
|
||||
- Displays high scores for supported games (Lemonade Stand, DopeWars, BlackJack, Video Poker, Mastermind, GolfSim).
|
||||
- If a file is missing, it will print a message indicating so.
|
||||
|
||||
**Note:**
|
||||
This tool is for administrative and debugging purposes only. It does not modify any data.
|
||||
|
||||
## eas_alert_parser.py
|
||||
|
||||
**Purpose:**
|
||||
`eas_alert_parser.py` is a utility script for processing and cleaning up output from `multimon-ng` to extract and convert Emergency Alert System (EAS) messages for further use, such as with EAS2Text.
|
||||
|
||||
**Usage:**
|
||||
This script is intended to be used with piped input, typically from `multimon-ng` decoding SAME/EAS messages. It filters and processes EAS lines, converts them to readable text using EAS2Text, and writes the results to `alert.txt`.
|
||||
|
||||
**Example usage:**
|
||||
```sh
|
||||
multimon-ng -a EAS ... | python etc/eas_alert_parser.py
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Reads input line-by-line (supports piped or redirected input).
|
||||
- Filters for lines starting with `EAS:` or `EAS (part):`.
|
||||
- Avoids duplicate messages and only processes new alerts.
|
||||
- Uses the EAS2Text library to convert EAS codes to human-readable messages.
|
||||
- Writes completed alerts to `alert.txt` for further processing or notification.
|
||||
|
||||
**Note:**
|
||||
This script is intended for experimental or hobbyist use and may require customization for your specific workflow.
|
||||
|
||||
## simulator.py
|
||||
|
||||
**Purpose:**
|
||||
`simulator.py` is a development and testing tool that simulates the behavior of the Mesh Bot in a controlled environment. It allows you to prototype and test handler functions without needing real hardware or a live mesh network.
|
||||
|
||||
**Usage:**
|
||||
Run this script from the command line to interactively test handler functions. You can input messages as if you were a mesh node, and see how your handler responds.
|
||||
|
||||
```sh
|
||||
python etc/simulator.py
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Simulates node IDs, device IDs, and random GPS locations.
|
||||
- Lets you specify which handler function to test (by setting `projectName`).
|
||||
- Prompts for user input, passes it to the handler, and displays the response.
|
||||
- Logs simulated message sending and handler output for review.
|
||||
- Useful for rapid prototyping and debugging new features or message handlers.
|
||||
|
||||
**Note:**
|
||||
Edit the `projectName` variable to match the handler function you want to test. You can expand this script to test additional handlers or scenarios as needed.
|
||||
|
||||
Feel free to add or update resources here as needed for documentation, configuration, or project support.
|
||||
48
etc/custom_scheduler.py
Normal file
48
etc/custom_scheduler.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import schedule
|
||||
from modules.log import logger
|
||||
from modules.system import send_message
|
||||
|
||||
def setup_custom_schedules(send_message, tell_joke, welcome_message, handle_wxc, MOTD, schedulerChannel, schedulerInterface):
|
||||
# custom scheduler job to run the schedule see examples below
|
||||
logger.debug(f"System: Starting the custom scheduler default to send reminder every Monday at noon on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Enabled Reminder"))
|
||||
|
||||
|
||||
# Enhanced Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
|
||||
# Send a joke every 2 minutes
|
||||
#schedule.every(2).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
|
||||
|
||||
# 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))
|
||||
|
||||
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
|
||||
|
||||
# Send Weather Channel Notice Wed. Noon on channel 2, device 1
|
||||
#schedule.every().wednesday.at("12:00").do(lambda: send_message("Weather alerts available on 'Alerts' channel with default 'AQ==' key.", 2, 0, 1))
|
||||
|
||||
# Send config URL for Medium Fast Network Use every other day at 10:00 to default channel 2 on device 1
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("Join us on Medium Fast https://meshtastic.org/e/#CgcSAQE6AggNEg4IARAEOAFAA0gBUB5oAQ", 2, 0, 1))
|
||||
|
||||
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
|
||||
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", 2, 0, 1)).day(15, 25)
|
||||
|
||||
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00
|
||||
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", schedulerChannel, 0, schedulerInterface)).day(15, 25)
|
||||
|
||||
# Send a joke every 6 hours
|
||||
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
|
||||
|
||||
# Send the Welcome Message every other day at 08:00
|
||||
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, schedulerChannel, 0, schedulerInterface))
|
||||
|
||||
# Send the MOTD every day at 13:00
|
||||
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, schedulerChannel, 0, schedulerInterface))
|
||||
|
||||
# Send bbslink looking for peers every other day at 10:00
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface))
|
||||
@@ -14,6 +14,8 @@ Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 mesh_bot.py
|
||||
ExecStop=pkill -f mesh_bot.py
|
||||
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
|
||||
@@ -14,6 +14,8 @@ Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 modules/web.py
|
||||
ExecStop=pkill -f mesh_bot_w3.py
|
||||
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
@@ -21,3 +23,6 @@ Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
Restart=on-failure
|
||||
Type=notify #try simple if any problems
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
||||
@@ -14,6 +14,8 @@ Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 pong_bot.py
|
||||
ExecStop=pkill -f pong_bot.py
|
||||
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
|
||||
32
install.sh
32
install.sh
@@ -13,13 +13,18 @@ printf "Installer works best in raspian/debian/ubuntu or foxbuntu embedded syste
|
||||
printf "If there is a problem, try running the installer again.\n"
|
||||
printf "\nChecking for dependencies...\n"
|
||||
|
||||
# fuse check for existing installation
|
||||
if [[ -f config.ini ]]; then
|
||||
printf "\nDetected existing installation, please backup and remove existing installation before proceeding\n"
|
||||
exit 1
|
||||
fi
|
||||
# check if we are in /opt/meshing-around
|
||||
if [ $program_path != "/opt/meshing-around" ]; then
|
||||
if [[ "$program_path" != "/opt/meshing-around" ]]; then
|
||||
printf "\nIt is suggested to project path to /opt/meshing-around\n"
|
||||
printf "Do you want to move the project to /opt/meshing-around? (y/n)"
|
||||
read move
|
||||
if [[ $(echo "$move" | grep -i "^y") ]]; then
|
||||
sudo mv $program_path /opt/meshing-around
|
||||
sudo mv "$program_path" /opt/meshing-around
|
||||
cd /opt/meshing-around
|
||||
printf "\nProject moved to /opt/meshing-around. re-run the installer\n"
|
||||
exit 0
|
||||
@@ -83,6 +88,12 @@ cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
|
||||
cp etc/mesh_bot_w3.tmp etc/mesh_bot_w3.service
|
||||
|
||||
# copy modules/custom_scheduler.py template if it does not exist
|
||||
if [[ ! -f modules/custom_scheduler.py ]]; then
|
||||
cp etc/custom_scheduler.py modules/custom_scheduler.py
|
||||
printf "\nCustom scheduler template copied to modules/custom_scheduler.py\n"
|
||||
fi
|
||||
|
||||
# generate config file, check if it exists
|
||||
if [[ -f config.ini ]]; then
|
||||
printf "\nConfig file already exists, moving to backup config.old\n"
|
||||
@@ -203,10 +214,17 @@ sudo usermod -a -G tty $whoami
|
||||
sudo usermod -a -G bluetooth $whoami
|
||||
echo "Added user $whoami to dialout, tty, and bluetooth groups"
|
||||
|
||||
sudo chown -R $whoami:$whoami $program_path/logs
|
||||
sudo chown -R $whoami:$whoami $program_path/data
|
||||
sudo chown -R "$whoami:$whoami" "$program_path/logs"
|
||||
sudo chown -R "$whoami:$whoami" "$program_path/data"
|
||||
echo "Permissions set for meshbot on logs and data directories"
|
||||
|
||||
# check and see if some sort of NTP is running
|
||||
if ! systemctl is-active --quiet ntp.service && \
|
||||
! systemctl is-active --quiet systemd-timesyncd.service && \
|
||||
! systemctl is-active --quiet chronyd.service; then
|
||||
printf "\nNo NTP service detected, it is recommended to have NTP running for proper bot operation.\n"
|
||||
fi
|
||||
|
||||
# set the correct user in the service file
|
||||
replace="s|User=pi|User=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
@@ -287,7 +305,7 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
|
||||
# document the service install
|
||||
printf "To install the %s service and keep notes, reference following commands:\n\n" "$service" > install_notes.txt
|
||||
printf "sudo cp %s/etc/%s.service /etc/systemd/system/etc/%s.service\n" "$program_path" "$service" "$service" >> install_notes.txt
|
||||
printf "sudo cp %s/etc/%s.service /etc/systemd/system/%s.service\n" "$program_path" "$service" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl daemon-reload\n" >> install_notes.txt
|
||||
printf "sudo systemctl enable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
|
||||
@@ -299,6 +317,7 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "Reporting chron job added to run report_generator5.py\n" >> install_notes.txt
|
||||
printf "chronjob: %s\n" "$chronjob" >> install_notes.txt
|
||||
printf "*** Stay Up to date using 'bash update.sh' ***\n" >> install_notes.txt
|
||||
|
||||
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
|
||||
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n" >> install_notes.txt
|
||||
@@ -344,9 +363,10 @@ else
|
||||
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "*** Stay Up to date using 'bash update.sh' ***\n" >> install_notes.txt
|
||||
fi
|
||||
|
||||
printf "\nInstallation complete!\n"
|
||||
printf "\nInstallation complete?\n"
|
||||
|
||||
exit 0
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
# Logs and Reports
|
||||
Logs will collect here. Give a day of logs or a bunch of messages to have good reports.
|
||||
|
||||
## Reporting Note
|
||||
Reporting is via [../etc/report_generator5.py](../etc/report_generator5.py). The report_generator5 has newer feel and HTML5 coding. The index.html output is published in [../etc/www](../etc/www) there is a .cfg file created on first run for configuring values as needed (like moving web root)
|
||||
- Make sure to have `SyslogToFile = True` and default of DEBUG log level to fully enable reporting! ‼️
|
||||
- If you are in a venv and using launch.sh you can `launch.sh html5`
|
||||
This directory stores log files generated by the Mesh Bot. To generate useful reports, ensure you have at least a day's worth of logs or a substantial number of messages.
|
||||
|
||||
## Reporting
|
||||
|
||||
Reports are generated using [`../etc/report_generator5.py`](../etc/report_generator5.py), which produces modern HTML5 reports. The output (`index.html`) is saved in [`../etc/www`](../etc/www) by default. A `.cfg` configuration file is created on first run, allowing you to customize settings such as the web root directory.
|
||||
|
||||
- Ensure `SyslogToFile = True` and `sysloglevel = DEBUG` in your configuration to enable full reporting.
|
||||
- If using a virtual environment and `launch.sh`, you can run:
|
||||
```sh
|
||||
launch.sh html5
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
825
mesh_bot.py
825
mesh_bot.py
File diff suppressed because it is too large
Load Diff
@@ -1,54 +1,741 @@
|
||||
# Modules and Adding stuff
|
||||
# Meshtastic Mesh-Bot Modules
|
||||
|
||||
To help with code testing see `etc/simulator.py` to simulate a bot. I also enjoy meshtasticd(linux-native) in noradio with MQTT server and client to just emulate a mesh.
|
||||
This document provides an overview of all modules available in the Mesh-Bot project, including their features, usage, and configuration.
|
||||
Updated Oct-2025 "ver 1.9.8.4"
|
||||
|
||||
## By following these steps, you can add a new bbs option to the bot.
|
||||
---
|
||||
|
||||
1. **Define the Command Handler**:
|
||||
Add a new function in mesh_bot.py to handle the new command. For example, if you want to add a command `newcommand`:
|
||||
```python
|
||||
def handle_newcommand(message, message_from_id, deviceID):
|
||||
return "This is a response from the new command."
|
||||
```
|
||||
Additionally you can add a whole new module.py, I recommend doing this if you need to import more stuff, try and wedge it into similar spots if you can. You will need to import the file as well, look further at `modules/system.py` for more.
|
||||
2. **Add the Command to the Auto Response**:
|
||||
Update the auto_response function in mesh_bot.py to include the new command:
|
||||
```python
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
#...
|
||||
"newcommand": lambda: handle_newcommand(message, message_from_id, deviceID),
|
||||
#...
|
||||
```
|
||||
3. **Update the Trap List and Help**:
|
||||
A quick way to do this is to edit the line 16/17 in `modules/system.py` to include the new command:
|
||||
```python
|
||||
#...
|
||||
trap_list = ("cmd", "cmd?", "newcommand") # default trap list, with the new command added
|
||||
help_message = "Bot CMD?:newcommand, "
|
||||
#...
|
||||
```
|
||||
## Table of Contents
|
||||
|
||||
**If looking to merge** the prefered way would be to update `modules/system.py` Adding this block below `ping` which ends around line 28:
|
||||
```python
|
||||
# newcommand Configuration
|
||||
newcommand_enabled = True # settings.py handles the config.ini values; this is a placeholder
|
||||
if newcommand_enabled:
|
||||
trap_list_newcommand = ("newcommand",)
|
||||
trap_list = trap_list + trap_list_newcommand
|
||||
help_message = help_message + ", newcommand"
|
||||
```
|
||||
- [Overview](#overview)
|
||||
- [Networking](#networking)
|
||||
- [Games](#games)
|
||||
- [BBS (Bulletin Board System)](#bbs-bulletin-board-system)
|
||||
- [Checklist](#checklist)
|
||||
- [Location & Weather](#location--weather)
|
||||
- [EAS & Emergency Alerts](#eas--emergency-alerts)
|
||||
- [File Monitoring & News](#file-monitoring--news)
|
||||
- [Radio Monitoring](#radio-monitoring)
|
||||
- [Voice Commands (VOX)](#voice-commands-vox)
|
||||
- [Ollama LLM/AI](#ollama-llmai)
|
||||
- [Wikipedia Search](#wikipedia-search)
|
||||
- [Scheduler](#scheduler)
|
||||
- [Other Utilities](#other-utilities)
|
||||
- [Configuration](#configuration)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Adding your Own](adding_more.md)
|
||||
- [Configuration Guide](#configuration-guide)
|
||||
|
||||
5. **Test the New Command**:
|
||||
Run MeshBot and test the new command by sending a message with the command `newcommand` to ensure it responds correctly.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Modules are Python files in the `modules/` directory that add features to the bot. Enable or disable them via `config.ini`.
|
||||
See [modules/adding_more.md](adding_more.md) for developer notes.
|
||||
|
||||
---
|
||||
|
||||
## Networking
|
||||
|
||||
| Command | Description | ✅ Works Off-Grid |
|
||||
|--------------|-------------|------------------|
|
||||
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15 via DM only) you can also ping @NODE short name and if BBS DM enabled it will send them a joke | ✅ |
|
||||
| `cmd` | Returns the list of commands (the help message) | ✅ |
|
||||
| `history` | Returns the last commands run by user(s) | ✅ |
|
||||
| `leaderboard` | Shows extreme mesh metrics like lowest battery 🪫 `leaderboard reset` allows admin reset | ✅ |
|
||||
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
|
||||
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
|
||||
| `sysinfo` | Returns the bot node telemetry info | ✅ |
|
||||
| `test` | used to test the limits of data transfer (`test 4` sends data to the maxBuffer limit default 200 charcters) via DM only | ✅ |
|
||||
| `whereami` | Returns the address of the sender's location if known |
|
||||
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
|
||||
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
|
||||
| `echo` | Echo string back, disabled by default | ✅ |
|
||||
| `bannode` | Admin option to prevent a node from using bot. `bannode list` will load and use the data/bbs_ban_list.txt db | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Games
|
||||
|
||||
All games are played via DM to the bot. See [modules/games/README.md](games/README.md) for detailed rules and examples.
|
||||
|
||||
| Command | Description |
|
||||
|----------------|------------------------------------|
|
||||
| `blackjack` | Play Blackjack (Casino 21) |
|
||||
| `dopewars` | Classic trading game |
|
||||
| `golfsim` | 9-hole Golf Simulator |
|
||||
| `lemonstand` | Lemonade Stand business sim |
|
||||
| `tictactoe` | Tic-Tac-Toe vs. the bot |
|
||||
| `mastermind` | Code-breaking game |
|
||||
| `videopoker` | Video Poker (five-card draw) |
|
||||
| `joke` | Tells a dad joke |
|
||||
| `hamtest` | FCC/ARRL QuizBot |
|
||||
| `hangman` | Classic word guess game |
|
||||
| `survey` | Take a custom survey |
|
||||
| `quiz` | QuizMaster group quiz |
|
||||
|
||||
Enable/disable games in `[games]` section of `config.ini`.
|
||||
|
||||
---
|
||||
|
||||
## BBS (Bulletin Board System)
|
||||
|
||||
| Command | Description |
|
||||
|--------------|-----------------------------------------------|
|
||||
| `bbshelp` | Show BBS help |
|
||||
| `bbslist` | List messages |
|
||||
| `bbsread` | Read a message by ID |
|
||||
| `bbspost` | Post a message or DM |
|
||||
| `bbsdelete` | Delete a message |
|
||||
| `bbsinfo` | BBS stats (sysop) |
|
||||
| `bbslink` | Link messages between BBS systems |
|
||||
|
||||
Enable in `[bbs]` section of `config.ini`.
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
| Command | Description |
|
||||
|--------------|-----------------------------------------------|
|
||||
| `checkin` | Check in a node/asset |
|
||||
| `checkout` | Check out a node/asset |
|
||||
| `checklist` | Show checklist database |
|
||||
|
||||
Enable in `[checklist]` section of `config.ini`.
|
||||
|
||||
---
|
||||
|
||||
## Location & Weather
|
||||
|
||||
| Command | Description |
|
||||
|--------------|-----------------------------------------------|
|
||||
| `wx` | Local weather forecast (NOAA/Open-Meteo) |
|
||||
| `wxc` | Weather in metric/imperial |
|
||||
| `wxa` | NOAA alerts |
|
||||
| `wxalert` | NOAA alerts (expanded) |
|
||||
| `mwx` | NOAA Coastal Marine Forecast |
|
||||
| `tide` | NOAA tide info |
|
||||
| `riverflow` | NOAA river flow info |
|
||||
| `earthquake` | USGS earthquake info |
|
||||
| `valert` | USGS volcano alerts |
|
||||
| `rlist` | Nearby repeaters from RepeaterBook |
|
||||
| `satpass` | Satellite pass info |
|
||||
| `howfar` | Distance traveled since last check |
|
||||
| `howtall` | Calculate height using sun angle |
|
||||
| `whereami` | Show current location |
|
||||
|
||||
Configure in `[location]` section of `config.ini`.
|
||||
|
||||
---
|
||||
|
||||
## EAS & Emergency Alerts
|
||||
|
||||
| Command | Description |
|
||||
|--------------|-----------------------------------------------|
|
||||
| `ea`/`ealert`| FEMA iPAWS/EAS alerts (USA/DE) |
|
||||
|
||||
Enable in `[eas]` section of `config.ini`.
|
||||
|
||||
---
|
||||
|
||||
## File Monitoring & News
|
||||
|
||||
| Command | Description |
|
||||
|--------------|-----------------------------------------------|
|
||||
| `readnews` | Read contents of a news file |
|
||||
| `readrss` | Read RSS feed |
|
||||
| `x:` | Run shell command (if enabled) |
|
||||
|
||||
Configure in `[fileMon]` section of `config.ini`.
|
||||
|
||||
---
|
||||
|
||||
## Radio Monitoring
|
||||
|
||||
| Command | Description |
|
||||
|--------------|-----------------------------------------------|
|
||||
| `radio` | Monitor radio SNR via Hamlib |
|
||||
|
||||
Configure in `[radioMon]` section of `config.ini`.
|
||||
|
||||
---
|
||||
|
||||
## Voice Commands (VOX)
|
||||
|
||||
You can trigger select bot functions using voice commands with the "Hey Chirpy!" wake word.
|
||||
Just say "Hey Chirpy..." followed by one of the supported commands:
|
||||
|
||||
| Voice Command | Description |
|
||||
|---------------|---------------------------------------------|
|
||||
| `joke` | Tells a joke |
|
||||
| `weather` | Returns local weather forecast |
|
||||
| `moon` | Returns moonrise/set and phase info |
|
||||
| `daylight` | Returns sunrise/sunset times |
|
||||
| `river` | Returns NOAA river flow info |
|
||||
| `tide` | Returns NOAA tide information |
|
||||
| `satellite` | Returns satellite pass info |
|
||||
|
||||
Enable and configure VOX features in the `[vox]` section of `config.ini`.
|
||||
|
||||
---
|
||||
|
||||
## Ollama LLM/AI
|
||||
|
||||
| Command | Description |
|
||||
|--------------|-----------------------------------------------|
|
||||
| `askai` | Ask Ollama LLM AI |
|
||||
| `ask:` | Ask Ollama LLM AI (raw) |
|
||||
|
||||
Configure in `[ollama]` section of `config.ini`.
|
||||
|
||||
---
|
||||
|
||||
## Wikipedia Search
|
||||
|
||||
| Command | Description |
|
||||
|--------------|-----------------------------------------------|
|
||||
| `wiki:` | Search Wikipedia or local Kiwix server |
|
||||
|
||||
Configure in `[wikipedia]` section of `config.ini`.
|
||||
|
||||
---
|
||||
|
||||
## Scheduler
|
||||
|
||||
Automate messages and tasks using the scheduler module.
|
||||
|
||||
Configure in `[scheduler]` section of `config.ini`.
|
||||
See modules/custom_scheduler.py for advanced scheduling using python
|
||||
|
||||
**Purpose:**
|
||||
`scheduler.py` provides automated scheduling for Mesh Bot, allowing you to send messages, jokes, weather updates, and custom actions at specific times or intervals.
|
||||
|
||||
**How to Use:**
|
||||
- The scheduler is configured via your bot’s settings or commands, specifying what to send, when, and on which channel/interface.
|
||||
- Supports daily, weekly, hourly, and minutely schedules, as well as special jobs like jokes and weather.
|
||||
- For advanced automation, you can define your own schedules in `etc/custom_scheduler.py` (copied to `modules/custom_scheduler.py` at install).
|
||||
|
||||
**Features:**
|
||||
- **Basic Scheduling:** Send messages on a set schedule (e.g., every day at 09:00, every Monday at noon, every hour, etc.).
|
||||
- **Joke Scheduler:** Automatically send jokes at a chosen interval.
|
||||
- **Weather Scheduler:** Send weather updates at a chosen interval.
|
||||
- **Custom Scheduler:** Import and run your own scheduled jobs by editing `custom_scheduler.py`.
|
||||
- **Logging:** All scheduling actions are logged for debugging and monitoring.
|
||||
|
||||
**Example Configuration:**
|
||||
To send a daily message at 09:00:
|
||||
- `schedulerValue = 'day'`
|
||||
- `schedulerTime = '09:00'`
|
||||
- `schedulerMessage = 'Good morning, mesh!'`
|
||||
|
||||
**Custom Schedules:**
|
||||
1. Edit `etc/custom_scheduler.py` to define your custom jobs.
|
||||
2. On install, this file is copied to `modules/custom_scheduler.py`.
|
||||
3. Set `schedulerValue = 'custom'` to activate your custom schedules.
|
||||
|
||||
**Note:**
|
||||
- The scheduler uses the [schedule](https://schedule.readthedocs.io/en/stable/) Python library.
|
||||
- All scheduled jobs run asynchronously as long as the bot is running.
|
||||
- For troubleshooting, check the logs for scheduler activity and errors.
|
||||
|
||||
---
|
||||
|
||||
## Other Utilities
|
||||
|
||||
- `motd` — Message of the day
|
||||
- `leaderboard` — Mesh telemetry stats
|
||||
- `lheard` — Last heard nodes
|
||||
- `history` — Command history
|
||||
- `cmd`/`cmd?` — Show help message (the bot avoids the use of saying or using help)
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
- Edit `config.ini` to enable/disable modules and set options.
|
||||
- See `config.template` for all available settings.
|
||||
- Each module section in `config.ini` has an `enabled` flag.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Use the `logger` module for debug output.
|
||||
- See [modules/README.md](modules/README.md) for developer help.
|
||||
- Use `etc/simulator.py` for local testing.
|
||||
- Check the logs in the `logs/` directory for errors.
|
||||
|
||||
### .ini Settings
|
||||
|
||||
If you encounter issues with modules or bot behavior, you can use the `.ini` configuration settings to help diagnose and resolve problems:
|
||||
|
||||
### Enable Debug Logging
|
||||
|
||||
Increase the logging level to capture more detailed information:
|
||||
```ini
|
||||
[general]
|
||||
sysloglevel = DEBUG
|
||||
SyslogToFile = True
|
||||
```
|
||||
This will log detailed system messages to disk, which you can review in the `logs/` directory.
|
||||
|
||||
### Module-Specific Troubleshooting
|
||||
|
||||
- **Games Not Working:**
|
||||
Ensure the relevant game is enabled in the `[games]` section:
|
||||
```ini
|
||||
[games]
|
||||
blackjack = True
|
||||
dopeWars = True
|
||||
# ...other games
|
||||
```
|
||||
- **Weather/Location Issues:**
|
||||
Make sure `[location]` and weather modules are enabled and configured:
|
||||
```ini
|
||||
[location]
|
||||
enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
```
|
||||
- **BBS Not Responding:**
|
||||
Check that BBS is enabled and you are not on the ban list:
|
||||
```ini
|
||||
[bbs]
|
||||
enabled = True
|
||||
bbs_ban_list =
|
||||
```
|
||||
- **Scheduler Not Running:**
|
||||
Confirm the scheduler is enabled and properly configured:
|
||||
```ini
|
||||
[scheduler]
|
||||
enabled = True
|
||||
value = day
|
||||
time = 09:00
|
||||
```
|
||||
- **File Monitoring Not Working:**
|
||||
Verify file monitoring is enabled and the correct file path is set:
|
||||
```ini
|
||||
[fileMon]
|
||||
filemon_enabled = True
|
||||
file_path = alert.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Messaging Settings
|
||||
|
||||
The `[messagingSettings]` section in your `config.ini` controls how messages are handled, split, acknowledged, and logged by Mesh Bot. Adjust these settings to optimize performance, reliability, and debugging for your mesh network.
|
||||
|
||||
### Key Options
|
||||
|
||||
- **responseDelay**
|
||||
*Default: 2.2*
|
||||
Sets the delay (in seconds) before the bot responds to a message. Increase this if you experience message collisions or throttling on busy networks.
|
||||
|
||||
- **splitDelay**
|
||||
*Default: 2.5*
|
||||
Sets the delay (in seconds) between sending split message chunks. Useful for avoiding collisions when sending long messages that are broken into parts.
|
||||
|
||||
- **MESSAGE_CHUNK_SIZE**
|
||||
*Default: 160*
|
||||
The maximum number of characters per message chunk. Messages longer than this are automatically split. (The chunker may allow up to 3 extra characters.)
|
||||
|
||||
- **wantAck**
|
||||
*Default: False*
|
||||
If set to `True`, the bot will request over-the-air (OTA) acknowledgements for sent messages. Enable this for critical messages, but note it may increase network traffic.
|
||||
|
||||
- **maxBuffer**
|
||||
*Default: 200*
|
||||
Sets the maximum buffer size (in bytes) for radio testing. Increase or decrease based on your hardware’s capabilities.
|
||||
|
||||
- **enableHopLogs**
|
||||
*Default: False*
|
||||
If `True`, enables extra logging of hop count data for each message. Useful for analyzing mesh performance.
|
||||
|
||||
- **noisyNodeLogging**
|
||||
*Default: False*
|
||||
Enables logging for nodes that generate excessive telemetry or noise. Helps identify problematic devices.
|
||||
|
||||
- **noisyTelemetryLimit**
|
||||
*Default: 5*
|
||||
The threshold for how many noisy packets trigger logging for a node.
|
||||
|
||||
- **logMetaStats**
|
||||
*Default: True*
|
||||
Enables logging of metadata statistics for analysis.
|
||||
|
||||
- **DEBUGpacket**
|
||||
*Default: False*
|
||||
If `True`, logs all packet details for advanced debugging. Only enable if you need deep diagnostics, as this can generate large log files.
|
||||
|
||||
- **debugMetadata**
|
||||
*Default: False*
|
||||
Enables detailed logging for metaPackets. Use the `metadataFilter` to control which packet types are logged.
|
||||
|
||||
- **metadataFilter**
|
||||
*Default: TELEMETRY_APP,POSITION_APP*
|
||||
Comma-separated list of packet types to include in metaPacket logging. Adjust to focus on specific data types.
|
||||
|
||||
### Troubleshooting Tips
|
||||
|
||||
- If you see message collisions or dropped messages, try increasing `responseDelay` and `splitDelay`.
|
||||
- Enable `DEBUGpacket` and `enableHopLogs` for detailed diagnostics if you’re troubleshooting delivery or routing issues.
|
||||
- Use `noisyNodeLogging` to identify and address problematic nodes on your mesh.
|
||||
|
||||
---
|
||||
|
||||
**Tip:**
|
||||
Refer to the comments in `config.template` for further guidance on each setting.
|
||||
|
||||
### General Tips
|
||||
|
||||
- After changing `.ini` settings, restart the bot to apply changes.
|
||||
- Check the logs in the `logs/` directory for errors or warnings.
|
||||
- Use `explicitCmd = True` in `[general]` to require explicit commands, which can help avoid accidental triggers.
|
||||
- For advanced debugging, set `DEBUGpacket = True` in `[messagingSettings]` to log all packet details.
|
||||
|
||||
---
|
||||
|
||||
If you continue to have issues, review the logs for error messages and consult the comments in `config.template` for further guidance.
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Running a Shell command
|
||||
|
||||
Using the above example and enabling the filemon module, you can make a command which calls a bash file to do things on the system.
|
||||
|
||||
### Configuration Guide
|
||||
The following is documentation for the config.ini file
|
||||
|
||||
If you have not done so, or want to 'factory reset', copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
|
||||
```sh
|
||||
meshtastic --ble-scan
|
||||
```
|
||||
|
||||
**Note**: The code has been tested with a single BLE device and is written to support only one BLE port.
|
||||
|
||||
```ini
|
||||
# config.ini
|
||||
# type can be serial, tcp, or ble.
|
||||
# port is the serial port to use; commented out will try to auto-detect
|
||||
# hostname is the IP/DNS and port for tcp type default is host:4403
|
||||
# mac is the MAC address of the device to connect to for BLE type
|
||||
|
||||
[interface]
|
||||
type = serial
|
||||
# port = '/dev/ttyUSB0'
|
||||
# hostname = 192.168.0.1
|
||||
# mac = 00:11:22:33:44:55
|
||||
|
||||
# Additional interface for dual radio support. See config.template for more.
|
||||
[interface2]
|
||||
enabled = False
|
||||
```
|
||||
|
||||
### General Settings
|
||||
The following settings determine how the bot responds. By default, the bot will not spam the default channel. Setting `respond_by_dm_only` to `True` will force all messages to be sent via DM, which may not be desired. Setting it to [`False`] will allow responses in the channel for all to see. If you have no default channel you can set this value to `-1` or any unused channel index. You can also have the bot ignore the defaultChannel for any commands, but still observe the channel.
|
||||
|
||||
```ini
|
||||
[general]
|
||||
respond_by_dm_only = True
|
||||
defaultChannel = 0
|
||||
ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
ignoreChannels = # ignoreChannels is a comma separated list of channels to ignore, e.g. 4,5
|
||||
cmdBang = False # require ! to be the first character in a command
|
||||
explicitCmd = True # require explicit command, the message will only be processed if it starts with a command word disable to get more activity
|
||||
```
|
||||
|
||||
### Location Settings
|
||||
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, as well as the default for all NOAA, repeater lookup. It is also the center of radius for Sentry.
|
||||
|
||||
```ini
|
||||
[location]
|
||||
enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
# To fuzz the location of the above
|
||||
fuzzConfigLocation = True
|
||||
# Fuzz all values in all data
|
||||
fuzzItAll = False
|
||||
|
||||
UseMeteoWxAPI = True
|
||||
|
||||
coastalEnabled = False # NOAA Coastal Data Enable NOAA Coastal Waters Forecasts and Tide
|
||||
# Find the correct coastal weather directory at https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/
|
||||
# this map can help https://www.weather.gov/marine select location and then look at the 'Forecast-by-Zone Map'
|
||||
myCoastalZone = https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/pz/pzz135.txt # myCoastalZone is the .txt file with the forecast data
|
||||
coastalForecastDays = 3 # number of data points to return, default is 3
|
||||
```
|
||||
|
||||
### Module Settings
|
||||
Modules can be enabled or disabled as needed. They are essentally larger functions of code which you may not want on your mesh or in memory space.
|
||||
|
||||
```ini
|
||||
[bbs]
|
||||
enabled = False
|
||||
|
||||
[general]
|
||||
DadJokes = False
|
||||
StoreForward = False
|
||||
```
|
||||
|
||||
### History
|
||||
The history command shows the last commands the user ran, and [`lheard`] reflects the last users on the bot.
|
||||
|
||||
```ini
|
||||
enableCmdHistory = True # history command enabler
|
||||
lheardCmdIgnoreNodes = # command history ignore list ex: 2813308004,4258675309
|
||||
```
|
||||
|
||||
### Sentry Settings
|
||||
|
||||
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
|
||||
emailSentryAlerts = True # if SMTP enabled send alert to sysop email list
|
||||
SentryRadius = 100 # radius in meters to detect someone close to the bot
|
||||
SentryChannel = 9 # holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 2 # channel to send a message to when the watchdog is triggered
|
||||
sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
highFlyingAlert = True # HighFlying Node alert
|
||||
highFlyingAlertAltitude = 2000 # Altitude in meters to trigger the alert
|
||||
highflyOpenskynetwork = True # check with OpenSkyNetwork if highfly detected for aircraft
|
||||
```
|
||||
|
||||
### E-Mail / SMS Settings
|
||||
To enable connectivity with SMTP allows messages from meshtastic into SMTP. The term SMS here is for connection via [carrier email](https://avtech.com/articles/138/list-of-email-to-sms-addresses/)
|
||||
|
||||
```ini
|
||||
[smtp]
|
||||
# enable or disable the SMTP module, minimum required for outbound notifications
|
||||
enableSMTP = True # enable or disable the IMAP module for inbound email, not implemented yet
|
||||
enableImap = False # list of Sysop Emails separate with commas, used only in emergency responder currently
|
||||
sysopEmails =
|
||||
# See config.template for all the SMTP settings
|
||||
SMTP_SERVER = smtp.gmail.com
|
||||
SMTP_AUTH = True
|
||||
EMAIL_SUBJECT = Meshtastic✉️
|
||||
```
|
||||
|
||||
### Emergency Response Handler
|
||||
Traps the following ("emergency", "911", "112", "999", "police", "fire", "ambulance", "rescue") keywords. Responds to the user, and calls attention to the text message in logs and via another network or channel.
|
||||
|
||||
```ini
|
||||
[emergencyHandler]
|
||||
enabled = True # enable or disable the emergency response handler
|
||||
alert_channel = 2 # channel to send a message to when the emergency handler is triggered
|
||||
alert_interface = 1
|
||||
```
|
||||
|
||||
### EAS Alerting
|
||||
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
|
||||
|
||||
#### FEMA iPAWS/EAS and NINA
|
||||
This uses USA: SAME, FIPS, to locate the alerts in the feed. By default ignoring Test messages.
|
||||
|
||||
```ini
|
||||
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
|
||||
eAlertBroadcastCh = 2,3 # Goverment Emergency IPAWS/CAP Alert Broadcast Channels
|
||||
ignoreFEMAenable = True # Ignore any headline that includes followig word list
|
||||
ignoreFEMAwords = test,exercise
|
||||
# comma separated list of FIPS codes to trigger local alert. find your FIPS codes at https://en.wikipedia.org/wiki/Federal_Information_Processing_Standard_state_code
|
||||
myFIPSList = 57,58,53
|
||||
# find your SAME https://www.weather.gov/nwr/counties comma separated list of SAME code to further refine local alert.
|
||||
mySAMEList = 053029,053073
|
||||
|
||||
# To use other country services enable only a single optional serivce
|
||||
enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
|
||||
myRegionalKeysDE = 110000000000,120510000000
|
||||
```
|
||||
|
||||
#### NOAA EAS
|
||||
This uses the defined lat-long of the bot for collecting of data from the API. see [File-Monitoring](#File-Monitoring) for ideas to collect EAS alerts from a RTL-SDR.
|
||||
|
||||
```ini
|
||||
|
||||
wxAlertBroadcastEnabled = True # EAS Alert Broadcast
|
||||
wxAlertBroadcastCh = 2,4 # EAS Alert Broadcast Channels
|
||||
ignoreEASenable = True # Ignore any headline that includes followig word list
|
||||
ignoreEASwords = test,advisory
|
||||
```
|
||||
|
||||
#### USGS River flow data and Volcano alerts
|
||||
Using the USGS water data page locate a water flow device, for example Columbia River at Vancouver, WA - USGS-14144700
|
||||
|
||||
Volcano Alerts use lat/long to determine ~1000km radius
|
||||
```ini
|
||||
[location]
|
||||
# USGS Hydrology unique identifiers, LID or USGS ID https://waterdata.usgs.gov
|
||||
riverList = 14144700 # example Mouth of Columbia River
|
||||
|
||||
# USGS Volcano alerts Enable USGS Volcano Alert Broadcast
|
||||
volcanoAlertBroadcastEnabled = False
|
||||
volcanoAlertBroadcastCh = 2
|
||||
```
|
||||
|
||||
### Repeater Settings
|
||||
A repeater function for two different nodes and cross-posting messages. The `repeater_channels` is a list of repeater channels that will be consumed and rebroadcast on the same number channel on the other device, node, or interface. Each node should have matching channel numbers. The channel names and PSK do not need to be the same on the nodes. Use this feature responsibly to avoid creating a feedback loop.
|
||||
|
||||
```ini
|
||||
[repeater] # repeater module
|
||||
enabled = True
|
||||
repeater_channels = [2, 3]
|
||||
```
|
||||
|
||||
### 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 `gemma3:270m`. Ollama can be remote [Ollama Server](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server) works on a pi58GB with 40 second or less response time.
|
||||
|
||||
```ini
|
||||
# Enable ollama LLM see more at https://ollama.com
|
||||
ollama = True # Ollama model to use (defaults to gemma2:2b)
|
||||
ollamaModel = gemma3:latest # Ollama model to use (defaults to gemma3:270m)
|
||||
ollamaHostName = http://localhost:11434 # server instance to use (defaults to local machine install)
|
||||
```
|
||||
|
||||
Also see `llm.py` for changing the defaults of:
|
||||
|
||||
```ini
|
||||
# LLM System Variables
|
||||
rawQuery = True # if True, the input is sent raw to the LLM if False, it is processed by the meshBotAI template
|
||||
|
||||
# Used in the meshBotAI template (legacy)
|
||||
llmEnableHistory = True # enable history for the LLM model to use in responses adds to compute time
|
||||
llmContext_fromGoogle = True # enable context from google search results helps with responses accuracy
|
||||
googleSearchResults = 3 # number of google search results to include in the context more results = more compute time
|
||||
```
|
||||
Note for LLM in docker with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html). Needed for the container with ollama running.
|
||||
|
||||
### Wikipedia Search Settings
|
||||
The Wikipedia search module can use either the online Wikipedia API or a local Kiwix server for offline wiki access. Kiwix is especially useful for mesh networks operating in remote or offline environments.
|
||||
|
||||
```ini
|
||||
# Enable or disable the wikipedia search module
|
||||
wikipedia = True
|
||||
|
||||
# Use local Kiwix server instead of online Wikipedia
|
||||
# Set to False to use online Wikipedia (default)
|
||||
useKiwixServer = False
|
||||
|
||||
# Kiwix server URL (only used if useKiwixServer is True)
|
||||
kiwixURL = http://127.0.0.1:8080
|
||||
|
||||
# Kiwix library name (e.g., wikipedia_en_100_nopic_2024-06)
|
||||
# Find available libraries at https://library.kiwix.org/
|
||||
kiwixLibraryName = wikipedia_en_100_nopic_2024-06
|
||||
```
|
||||
|
||||
To set up a local Kiwix server:
|
||||
1. Install Kiwix tools: https://kiwix.org/en/ `sudo apt install kiwix-tools -y`
|
||||
2. Download a Wikipedia ZIM file to `data/`: https://library.kiwix.org/ `wget https://download.kiwix.org/zim/wikipedia/wikipedia_en_100_nopic_2025-09.zim`
|
||||
3. Run the server: `kiwix-serve --port 8080 wikipedia_en_100_nopic_2025-09.zim`
|
||||
4. Set `useKiwixServer = True` in your config.ini
|
||||
|
||||
The bot will automatically extract and truncate content to fit Meshtastic's message size limits (~500 characters).
|
||||
|
||||
### 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 = True
|
||||
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 # text file to monitor for changes
|
||||
broadcastCh = 2 # channel to send the message to can be 2,3 multiple channels comma separated
|
||||
enable_read_news = False # news command will return the contents of a text file
|
||||
news_file_path = news.txt
|
||||
news_random_line = False # only return a single random line from the news file
|
||||
enable_runShellCmd = False # enable the use of exernal shell commands, this enables more data in `sysinfo` DM
|
||||
# if runShellCmd and you think it is safe to allow the x: command to run
|
||||
# direct shell command handler the x: command in DMs user must be in bbs_admin_list
|
||||
allowXcmd = True
|
||||
```
|
||||
|
||||
#### Offline EAS
|
||||
|
||||
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
|
||||
Maintain multiple news sources. Each source should be a file named `{source}_news.txt` in the `data/` directory (for example, `data/mesh_news.txt`).
|
||||
- To read the default news, use the `readnews` command (reads from `data/news.txt`.
|
||||
- To read a specific source, use `readnews abc` to read from `data/abc_news.txt`.
|
||||
|
||||
This allows you to organize and access different news feeds or categories easily.
|
||||
External scripts can update these files as needed, and the bot will serve the latest content on request.
|
||||
|
||||
### Greet new nodes QRZ module
|
||||
This isnt QRZ.com this is Q code for who is calling me, this will track new nodes and say hello
|
||||
```ini
|
||||
[qrz]
|
||||
enabled = True # QRZ Hello to new nodes
|
||||
qrz_hello_string = "send CMD or DM me for more info." # will be sent to all heard nodes once
|
||||
training = True # Training mode will not send the hello message to new nodes, use this to build up database
|
||||
```
|
||||
|
||||
### Scheduler
|
||||
In the config.ini enable the module
|
||||
```ini
|
||||
[scheduler]
|
||||
enabled = False # enable or disable the scheduler module
|
||||
interface = 1 # channel to send the message to
|
||||
channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun.
|
||||
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
|
||||
# custom for module/scheduler.py custom schedule examples
|
||||
interval = # interval to use when time is not set (e.g. every 2 days)
|
||||
time = # time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
```
|
||||
The basic brodcast message can be setup in condig.ini. For advanced, See the [modules/scheduler.py](modules/scheduler.py) 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
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
#...
|
||||
"switchON": lambda: call_external_script(message)
|
||||
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
|
||||
|
||||
#Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
|
||||
schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
```
|
||||
This would call the default script located in script/runShell.sh and return its output.
|
||||
|
||||
#### BBS Link
|
||||
The scheduler also handles the BBS Link Broadcast message, this would be an example of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initiator, one direction pull. The message just needs to have bbslink
|
||||
```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
|
||||
bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
```
|
||||
|
||||
Happy meshing!
|
||||
132
modules/adding_more.md
Normal file
132
modules/adding_more.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Modules and Adding Features
|
||||
|
||||
This document explains how to add new modules and commands to your Meshtastic mesh-bot project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Adding a New Command](#adding-a-new-command)
|
||||
- [Running a Shell Command](#running-a-shell-command)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Technical Assistance & Troubleshooting](#technical-assistance--troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
For code testing, see `etc/simulator.py` to simulate a bot.
|
||||
You can also use `meshtasticd` (Linux-native) in `noradio` mode with MQTT server and client to emulate a mesh network.
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Command
|
||||
|
||||
Follow these steps to add a new BBS option or command to the bot:
|
||||
|
||||
### 1. Define the Command Handler
|
||||
|
||||
Add a new function in `mesh_bot.py` to handle your command.
|
||||
Example for a command called `newcommand`:
|
||||
|
||||
```python
|
||||
def handle_newcommand(message, message_from_id, deviceID):
|
||||
return "This is a response from the new command."
|
||||
```
|
||||
|
||||
If your command is complex, consider creating a new module (e.g., `modules/newcommand.py`).
|
||||
Import your new module where needed (see `modules/system.py` for examples).
|
||||
|
||||
---
|
||||
|
||||
### 2. Add the Command to the Auto Response
|
||||
|
||||
Update the `auto_response` function in `mesh_bot.py` to include your new command:
|
||||
|
||||
```python
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
#...
|
||||
"newcommand": lambda: handle_newcommand(message, message_from_id, deviceID),
|
||||
#...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Update the Trap List and Help
|
||||
|
||||
Edit `modules/system.py` to include your new command in the trap list and help message:
|
||||
|
||||
```python
|
||||
#...
|
||||
trap_list = ("cmd", "cmd?", "newcommand") # Add your command here
|
||||
help_message = "Bot CMD?:newcommand, "
|
||||
#...
|
||||
```
|
||||
|
||||
**Preferred method:**
|
||||
Add a configuration block below `ping` (around line 28):
|
||||
|
||||
```python
|
||||
# newcommand Configuration
|
||||
newcommand_enabled = True # settings.py handles config.ini values; this is a placeholder
|
||||
if newcommand_enabled:
|
||||
trap_list_newcommand = ("newcommand",)
|
||||
trap_list = trap_list + trap_list_newcommand
|
||||
help_message = help_message + ", newcommand"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Test the New Command
|
||||
|
||||
Run MeshBot and test your new command by sending a message with `newcommand` to ensure it responds correctly.
|
||||
|
||||
---
|
||||
|
||||
## Running a Shell Command
|
||||
|
||||
You can make a command that calls a bash script on the system (requires the `filemon` module):
|
||||
|
||||
```python
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
#...
|
||||
"switchON": lambda: call_external_script(message)
|
||||
```
|
||||
|
||||
This will call the default script located at `script/runShell.sh` and return its output.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Modularize:** Place complex or reusable logic in its own module.
|
||||
- **Document:** Add docstrings and comments to your functions.
|
||||
- **Test:** Use the simulator or a test mesh to verify new features.
|
||||
- **Update Help:** Keep the help message and trap list up to date for users.
|
||||
- **Configuration:** Use `settings.py` and `config.ini` for feature toggles and settings.
|
||||
|
||||
---
|
||||
|
||||
## Technical Assistance & Troubleshooting
|
||||
|
||||
- **Debug Logging:**
|
||||
Use the `logger` module for debug output. Check logs for errors or unexpected behavior.
|
||||
- **Common Issues:**
|
||||
- *Module Import Errors:* Ensure your new module is in the `modules/` directory and imported correctly.
|
||||
- *Command Not Responding:* Verify your command is in the trap list and auto_response dictionary.
|
||||
- *Configuration Problems:* Double-check `settings.py` and `config.ini` for typos or missing entries.
|
||||
- **Testing:**
|
||||
- Use `etc/simulator.py` for local testing without radio hardware.
|
||||
- Use `meshtasticd` in `noradio` mode for network emulation.
|
||||
- **Python Environment:**
|
||||
- Use a virtual environment (`python3 -m venv venv`) to manage dependencies.
|
||||
- Install requirements with `pip install -r requirements.txt`.
|
||||
- **Updating Dependencies:**
|
||||
- try not to I want to remove some.
|
||||
- **Getting Help:**
|
||||
- Check the project wiki or issues page for common questions.
|
||||
- Use inline comments and docstrings for clarity.
|
||||
- If you’re stuck, ask for help on the project’s GitHub Discussions or Issues tab.
|
||||
|
||||
---
|
||||
|
||||
Happy hacking!
|
||||
@@ -4,6 +4,13 @@
|
||||
import pickle # pip install pickle
|
||||
from modules.log import *
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
useSynchCompression = False
|
||||
|
||||
if useSynchCompression:
|
||||
import zlib
|
||||
from modules.system import send_raw_bytes
|
||||
|
||||
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
|
||||
|
||||
@@ -11,6 +18,7 @@ trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsin
|
||||
bbs_messages = []
|
||||
bbs_dm = []
|
||||
|
||||
|
||||
def load_bbsdb():
|
||||
global bbs_messages
|
||||
# load the bbs messages from the database file
|
||||
@@ -88,7 +96,11 @@ def bbs_delete_message(messageID = 0, fromNode = 0):
|
||||
else:
|
||||
return "Please specify a message number to delete."
|
||||
|
||||
def bbs_post_message(subject, message, fromNode):
|
||||
def bbs_post_message(subject, message, fromNode, threadID=0, replytoID=0):
|
||||
# post a message to the bbsdb
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
thread = threadID
|
||||
replyto = replytoID
|
||||
# post a message to the bbsdb and assign a messageID
|
||||
messageID = len(bbs_messages) + 1
|
||||
|
||||
@@ -106,7 +118,7 @@ def bbs_post_message(subject, message, fromNode):
|
||||
return "Message posted. ID is: " + str(messageID)
|
||||
# validate its not overlength by keeping in chunker limit
|
||||
# append the message to the list
|
||||
bbs_messages.append([messageID, subject, message, fromNode])
|
||||
bbs_messages.append([messageID, subject, message, fromNode, now, thread, replyto])
|
||||
logger.info(f"System: NEW Message Posted, subject: {subject}, message: {message} from {fromNode}")
|
||||
|
||||
# save the bbsdb
|
||||
@@ -197,6 +209,32 @@ def bbs_delete_dm(toNode, message):
|
||||
return "System: cleared mail for" + str(toNode)
|
||||
return "System: No DM found for node " + str(toNode)
|
||||
|
||||
def compress_data(data_to_compress):
|
||||
# Prepare message as bytes
|
||||
compressed = zlib.compress(data_to_compress.encode('utf-8'))
|
||||
return compressed
|
||||
|
||||
def decompress_data(data_bytes):
|
||||
try:
|
||||
decompressed = zlib.decompress(data_bytes)
|
||||
msg = decompressed.decode('utf-8')
|
||||
return msg
|
||||
except Exception as e:
|
||||
logger.warning(f"Error decompressing data: {e}")
|
||||
return False
|
||||
|
||||
def bbs_receive_compressed(data_bytes, fromNode, RxNode):
|
||||
try:
|
||||
decompressed = zlib.decompress(data_bytes)
|
||||
msg = decompressed.decode('utf-8')
|
||||
|
||||
bbs_sync_posts(msg, fromNode, RxNode)
|
||||
|
||||
return msg
|
||||
except Exception as e:
|
||||
logger.error(f"Error decompressing BBS message: {e}")
|
||||
return None
|
||||
|
||||
def bbs_sync_posts(input, peerNode, RxNode):
|
||||
messageID = 0
|
||||
|
||||
@@ -241,7 +279,13 @@ def bbs_sync_posts(input, peerNode, RxNode):
|
||||
if messageID % 5 == 0:
|
||||
time.sleep(10 + responseDelay)
|
||||
logger.debug(f"System: Sending bbslink message {messageID} of {len(bbs_messages)} to peer " + str(peerNode))
|
||||
return f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]} @{fromNodeHex}"
|
||||
msg = f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]} @{fromNodeHex}"
|
||||
if useSynchCompression:
|
||||
compressed = compress_data(msg)
|
||||
send_raw_bytes(peerNode, compressed)
|
||||
logger.debug("System: Sent compressed bbslink message to peer " + str(peerNode))
|
||||
else:
|
||||
return msg
|
||||
else:
|
||||
logger.debug("System: bbslink sync complete with peer " + str(peerNode))
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import asyncio
|
||||
import random
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
trap_list_filemon = ("readnews",)
|
||||
|
||||
@@ -69,22 +70,33 @@ async def watch_file():
|
||||
return content
|
||||
await asyncio.sleep(1) # Check every
|
||||
|
||||
def call_external_script(message, script="script/runShell.sh"):
|
||||
# Call an external script with the message as an argument this is a example only
|
||||
def call_external_script(message, script="runShell.sh"):
|
||||
# If no path is given, assume script/ directory
|
||||
if "/" not in script and "\\" not in script:
|
||||
script = os.path.join("script", script)
|
||||
try:
|
||||
# Debugging: Print the current working directory and resolved script path
|
||||
current_working_directory = os.getcwd()
|
||||
script_path = os.path.join(current_working_directory, script)
|
||||
|
||||
if not os.path.exists(script_path):
|
||||
# try the raw script name
|
||||
# Try the raw script name
|
||||
script_path = script
|
||||
if not os.path.exists(script_path):
|
||||
logger.warning(f"FileMon: Script not found: {script_path}")
|
||||
return "sorry I can't do that"
|
||||
|
||||
output = os.popen(f"bash {script_path} {message}").read().encode('utf-8').decode('utf-8')
|
||||
return output
|
||||
|
||||
result = subprocess.run(
|
||||
["bash", script_path, message],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error(f"FileMon: Script error: {result.stderr.strip()}")
|
||||
return None
|
||||
|
||||
output = result.stdout.strip()
|
||||
return output if output else None
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error calling external script: {e}")
|
||||
return None
|
||||
|
||||
541
modules/games/README.md
Normal file
541
modules/games/README.md
Normal file
@@ -0,0 +1,541 @@
|
||||
# Meshtastic Mesh-Bot Games
|
||||
|
||||
## Game Index
|
||||
|
||||
- [Blackjack](#blackjack-game-module)
|
||||
- [DopeWars](#dopewars-game-module)
|
||||
- [GolfSim](#golfsim-game-module)
|
||||
- [Lemonade Stand](#lemonade-stand-game-module)
|
||||
- [Tic-Tac-Toe](#tic-tac-toe-game-module)
|
||||
- [MasterMind](#mastermind-game-module)
|
||||
- [Video Poker](#video-poker-game-module)
|
||||
- [Word of the Day Game](#word-of-the-day-game--rules--features)
|
||||
|
||||
---
|
||||
|
||||
|
||||
# Blackjack Game Module
|
||||
|
||||
This module implements a classic game of Blackjack (Casino 21) for the Meshtastic mesh-bot.
|
||||
|
||||
## How to Play
|
||||
|
||||
- **Start the Game:**
|
||||
Send the command `blackjack` via DM to the bot to start a new game session.
|
||||
- **Place a Bet:**
|
||||
When prompted, enter the amount you wish to wager (e.g., `5`). Minimum bet is 1 chip, maximum is your current chip total.
|
||||
- **Gameplay Commands:**
|
||||
After betting, you will be dealt two cards. The dealer will also have two cards (one face up).
|
||||
- `h` or `hit` — Draw another card.
|
||||
- `s` or `stand` — End your turn and let the dealer play.
|
||||
- `d` or `double` — Double your bet and draw one more card (if you have enough chips).
|
||||
- `f` or `forfit` — Forfeit half your bet and end the round.
|
||||
- `r` or `resend` — Resend your current hand status.
|
||||
- `l` or `leave` — Leave the table and end your session.
|
||||
|
||||
- **Winning:**
|
||||
- Get as close to 21 as possible without going over.
|
||||
- If your hand exceeds 21, you bust and lose your bet.
|
||||
- If you beat the dealer without busting, you win your bet.
|
||||
- If you get a Blackjack (21 with two cards), you win 1.5x your bet.
|
||||
- If you tie the dealer, it's a push (no win/loss).
|
||||
|
||||
- **High Scores:**
|
||||
The module tracks the highest chip total achieved. If you beat the high score, you'll be notified!
|
||||
|
||||
## Notes
|
||||
|
||||
- Each player starts with 100 chips.
|
||||
- If you run out of chips, your balance will reset to 100.
|
||||
- The game state is tracked per player using your node ID.
|
||||
- Game progress and high scores are saved in `data/blackjack_hs.pkl`.
|
||||
- Only one game session per player is supported at a time.
|
||||
- For best results, play via DM to avoid interfering with other users' sessions.
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
You have 100 chips. Whats your bet?
|
||||
> 10
|
||||
|
||||
Player[14] 8♠️, 6♥️
|
||||
Dealer[10] 10♦️
|
||||
🧠Hit: 38% 👎, 62% 👍
|
||||
(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table
|
||||
> h
|
||||
|
||||
Player[18] 8♠️, 6♥️, 4♣️
|
||||
Dealer[10] 10♦️
|
||||
🧠Hit: 77% 👎, 23% 👍
|
||||
[H,S,F,D]
|
||||
> s
|
||||
|
||||
Player[18] 8♠️, 6♥️, 4♣️
|
||||
Dealer[20] 10♦️, Q♠️
|
||||
👎DEALER WINS
|
||||
📊🏆P:0,D:1,T:0
|
||||
💰You have 90 chips
|
||||
Bet or Leave?
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
- Ported from [Himan10/BlackJack](https://github.com/Himan10/BlackJack)
|
||||
- Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
|
||||
# DopeWars Game Module
|
||||
|
||||
A text-based trading game inspired by the classic DopeWars/DrugWars, adapted for the Meshtastic mesh-bot.
|
||||
|
||||
## How to Play
|
||||
|
||||
- **Start the Game:**
|
||||
Send the command `dopewars` via DM to the bot to begin a new session.
|
||||
|
||||
- **Objective:**
|
||||
Travel between cities, buy and sell drugs, and try to maximize your cash in 7 days.
|
||||
|
||||
- **Game Flow:**
|
||||
1. **Pick a Starting City:**
|
||||
You’ll be shown a list of cities. Enter the number to choose your starting location.
|
||||
2. **Each Day:**
|
||||
- You’ll see drug prices, your inventory, and your cash.
|
||||
- You can buy, sell, or fly to a new city.
|
||||
- Random events may occur (police, market changes, or finding cash/drugs).
|
||||
3. **Commands:**
|
||||
- **Buy:** `b,drug#,qty#` (e.g., `b,1,10` buys 10 of drug 1)
|
||||
- **Sell:** `s,drug#,qty#` (e.g., `s,2,5` sells 5 of drug 2)
|
||||
- **Max:** Use `m` for max quantity (e.g., `b,1,m`)
|
||||
- **Sell All:** Just `s` to sell everything you have.
|
||||
- **Fly:** `f` to move to a new city (ends the day).
|
||||
- **Price List:** `p` to view current prices and inventory.
|
||||
- **End Game:** `e` to end your run early.
|
||||
4. **Repeat:**
|
||||
Each time you fly, a day passes. After 7 days, your final cash is your score.
|
||||
|
||||
- **Winning:**
|
||||
- Try to finish with as much cash as possible.
|
||||
- Beat the high score to be crowned the top dealer!
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
1. Red Deer 2. Edmonton 3. Calgary 4. Toronto 5. Vancouver 6. St. Johns Where do you want to 🛫?#
|
||||
> 2
|
||||
|
||||
🗺️Edmonton 📆1/7 🎒0/100 💵5,000
|
||||
#1.Cocaine$15,000(0) #2.Heroin$2,500(0) #3.Weed$800(0) ...
|
||||
Buy💸, Sell💰, (F)ly🛫? (P)riceList?
|
||||
> b,2,10
|
||||
|
||||
Heroin: you have🎒 0 The going price is: $2,500
|
||||
You bought 10 Heroin. Remaining cash: $47,500
|
||||
Buy💸, Sell💰, Fly🛫?
|
||||
> f
|
||||
|
||||
🗺️Toronto 📆2/7 🎒10/100 💵47,500
|
||||
...
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- You start with $5,000 and a 100-slot backpack.
|
||||
- Each drug has a random price per city and day.
|
||||
- Special events can spike or crash prices, or cause you to lose/gain cash or inventory.
|
||||
- Police may confiscate your drugs or cash.
|
||||
- High scores are saved in `data/dopewar_hs.pkl`.
|
||||
- Only one game session per player at a time.
|
||||
- Play via DM for best experience.
|
||||
|
||||
## Credits
|
||||
|
||||
- Ported from [Reconfirefly/drugwars](https://github.com/Reconfirefly/drugwars)
|
||||
- Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
|
||||
# GolfSim Game Module
|
||||
|
||||
A text-based golf simulator for the Meshtastic mesh-bot. Play a full 9-hole round, choose your clubs, and try to set a new course record!
|
||||
|
||||
## How to Play
|
||||
|
||||
- **Start the Game:**
|
||||
Send the command `golf` via DM to the bot to begin a new round.
|
||||
|
||||
- **Objective:**
|
||||
Complete 9 holes in as few strokes as possible.
|
||||
|
||||
- **Game Flow:**
|
||||
1. **Each Hole:**
|
||||
- The bot tells you the hole number, length, par, and any hazards or weather.
|
||||
- Choose your club for each shot by typing its name or initial:
|
||||
- `d` or `driver` — Longest club
|
||||
- `l` or `low` — Low iron
|
||||
- `m` or `mid` — Mid iron
|
||||
- `h` or `high` — High iron
|
||||
- `g` or `gap` — Gap wedge
|
||||
- `w` or `wedge` — Lob wedge
|
||||
- `c` or `caddy` — Get a caddy guess for club distances
|
||||
- The bot will tell you how far you hit and how far remains.
|
||||
- When you’re within 20 yards, you’ll automatically putt to finish the hole.
|
||||
2. **Scoring:**
|
||||
- The bot tracks your strokes and score relative to par.
|
||||
- After each hole, you’ll see your score for the hole and your running total.
|
||||
3. **Hazards & Surprises:**
|
||||
- Hazards (sand, water, trees, etc.) and random events may affect your shots.
|
||||
- Critters or weather can cause unexpected results!
|
||||
4. **End of Round:**
|
||||
- After 9 holes, your total strokes and score to par are shown.
|
||||
- If you set a new low score, you’ll be notified as the new club record holder!
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
⛳️#1 is a 410-yard Par 4.☀️
|
||||
Choose your club.
|
||||
> d
|
||||
🏌️Hit D 260yd.
|
||||
You have 150yd. ⛳️
|
||||
Club?[D, L, M, H, G, W]🏌️
|
||||
> m
|
||||
🏌️Hit M Iron 170yd. Overshot the green!🚀
|
||||
You have 20yd. ⛳️
|
||||
Club?[D, L, M, H, G, W]🏌️
|
||||
> w
|
||||
🏌️Hit L Wedge 30yd. You're on the green! After 2 putt(s), you're in for 5 strokes. +Bogey
|
||||
You've hit a total of 5 strokes today, for +Bogey
|
||||
...
|
||||
🎉Finished 9-hole round⛳️ 🏆New Club Record🏆
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Play via DM for best experience.
|
||||
- Hazards and weather are randomized for each hole.
|
||||
- High scores are saved in `data/golfsim_hs.pkl`.
|
||||
- Only one game session per player at a time.
|
||||
- Commands are not case-sensitive; you can use full club names or initials.
|
||||
|
||||
## Credits
|
||||
|
||||
- Ported from [danfriedman30/pythongame](https://github.com/danfriedman30/pythongame)
|
||||
- Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
|
||||
# Lemonade Stand Game Module
|
||||
|
||||
A text-based business simulation where you run your own lemonade stand! Buy supplies, set prices, and try to maximize your profits over a summer season.
|
||||
|
||||
## How to Play
|
||||
|
||||
- **Start the Game:**
|
||||
Send the command `lemonade` via DM to the bot to begin a new game.
|
||||
|
||||
- **Objective:**
|
||||
Make as much money as possible in 7 weeks by managing your lemonade stand.
|
||||
|
||||
- **Game Flow:**
|
||||
1. **Each Week:**
|
||||
- The bot will show you the weather, temperature, and sales potential.
|
||||
- Buy supplies: cups, lemons, and sugar. Enter the number of each to purchase, or `n` for none.
|
||||
- Set your selling price per cup.
|
||||
- The bot will simulate sales and show your results, profits, and remaining inventory.
|
||||
- Repeat for each week.
|
||||
2. **Commands:**
|
||||
- Enter a number to buy supplies or set price.
|
||||
- Use `n` to skip buying an item.
|
||||
- Enter `g` during pricing to go back and buy more supplies.
|
||||
- At the end of each week, choose to continue or end the game.
|
||||
3. **Scoring:**
|
||||
- Your score is based on your net profit and efficiency (profit vs. possible profit).
|
||||
- High scores are tracked and displayed at the end of the game.
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
LemonStand🍋Week #1 of 7. 85ºF Sunny ☀️
|
||||
SupplyCost $0.45 a cup.
|
||||
Sales Potential: 60 cups.
|
||||
Inventory: 🥤:0 🍋:0 🍚:0
|
||||
Prices:
|
||||
🥤:$2.50 📦 of 25.
|
||||
🍋:$4.00 🧺 of 8.
|
||||
🍚:$3.00 bag for 15🥤.
|
||||
💵:$30.00
|
||||
🥤 to buy?
|
||||
Have 0 Cost $2.50 a 📦 of 25
|
||||
> 2
|
||||
|
||||
Purchased 2 📦 50 🥤 in inventory. $25.00 remaining
|
||||
🍋 to buy?
|
||||
Have 0🥤 of 🍋 Cost $4.00 a 🧺 for 8🥤
|
||||
> 1
|
||||
|
||||
Purchased 1 🧺 8 🍋 in inventory. $21.00 remaining
|
||||
🍚 to buy?
|
||||
You have 0🥤 of 🍚, Cost $3.00 a bag for 15🥤
|
||||
> 1
|
||||
|
||||
Purchased 1 bag(s) of 🍚 for $3.00. 15🥤🍚 in inventory.
|
||||
Cost of goods is $0.45 per 🥤 $18.00 💵 remaining.
|
||||
Price to Sell? or (G)rocery to buy more 🥤🍋🍚
|
||||
> 1.25
|
||||
|
||||
Results Week📊#1 of 7 Cost/Price:$0.45/$1.25 P.Margin:$0.80 T.Sales:16@$1.25 G.Profit: $20.00 N.Profit:$12.80
|
||||
Remaining 🥤:34 🍋:0 🍚:0 💵:$38.00📊P&L📈$8.00
|
||||
Weekly📊#1. 16 sold x $1.25ea.
|
||||
Play another week🥤? or (E)nd Game
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- You start with $30.00 and must buy supplies each week.
|
||||
- Weather and temperature affect sales potential.
|
||||
- If you run out of any supply, you can't sell more lemonade that week.
|
||||
- High scores are saved in `data/lemonstand.pkl`.
|
||||
- Only one game session per player at a time.
|
||||
- Play via DM for best experience.
|
||||
|
||||
## Credits
|
||||
|
||||
- Ported from [tigerpointe/Lemonade-Stand](https://github.com/tigerpointe/Lemonade-Stand)
|
||||
- Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
|
||||
# Tic-Tac-Toe Game Module
|
||||
|
||||
A classic Tic-Tac-Toe game for the Meshtastic mesh-bot. Play against the bot, track your stats, and see if you can beat the AI!
|
||||
|
||||
## How to Play
|
||||
|
||||
- **Start the Game:**
|
||||
Send the command `tictactoe` via DM to the bot to begin a new game.
|
||||
|
||||
- **Objective:**
|
||||
Get three of your marks in a row (horizontally, vertically, or diagonally) before the bot does.
|
||||
|
||||
- **Game Flow:**
|
||||
1. **Board Layout:**
|
||||
- The board is numbered 1-9, left to right, top to bottom.
|
||||
- Example:
|
||||
```
|
||||
1 | 2 | 3
|
||||
4 | 5 | 6
|
||||
7 | 8 | 9
|
||||
```
|
||||
2. **Making Moves:**
|
||||
- On your turn, type the number (1-9) where you want to place your mark.
|
||||
- The bot will respond with the updated board and make its move.
|
||||
3. **Commands:**
|
||||
- `n` — Start a new game.
|
||||
- `e` or `q` — End the current game.
|
||||
- `b` — Show the current board.
|
||||
- Enter a number (1-9) to make a move.
|
||||
4. **Winning:**
|
||||
- The first to get three in a row wins.
|
||||
- If the board fills with no winner, it’s a tie.
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
❌ | 2 | 3
|
||||
4 | ⭕️ | 6
|
||||
7 | 8 | 9
|
||||
|
||||
Your turn! Pick 1-9:
|
||||
> 3
|
||||
|
||||
❌ | 2 | ❌
|
||||
4 | ⭕️ | 6
|
||||
7 | 8 | 9
|
||||
|
||||
🤖Bot wins! (n)ew (e)nd
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Emojis are used for X and O unless disabled in settings.
|
||||
- Your win/loss stats are tracked across games.
|
||||
- The bot will try to win, block you, or pick a random move.
|
||||
- Play via DM for best experience.
|
||||
- Only one game session per player at a time.
|
||||
|
||||
## Credits
|
||||
|
||||
- Written for Meshtastic mesh-bot by Martin
|
||||
|
||||
# MasterMind Game Module
|
||||
|
||||
A text-based version of the classic code-breaking game MasterMind for the Meshtastic mesh-bot. Try to guess the secret color code in as few turns as possible!
|
||||
|
||||
## How to Play
|
||||
|
||||
- **Start the Game:**
|
||||
Send the command `mmind` via DM to the bot to begin a new game.
|
||||
|
||||
- **Objective:**
|
||||
Guess the secret 4-color code in 10 turns or less.
|
||||
|
||||
- **Game Flow:**
|
||||
1. **Choose Difficulty:**
|
||||
- (N)ormal: 4 colors (R🔴, Y🟡, G🟢, B🔵)
|
||||
- (H)ard: 6 colors (R🔴, Y🟡, G🟢, B🔵, O🟠, P🟣)
|
||||
- e(X)pert: 8 colors (R🔴, Y🟡, G🟢, B🔵, O🟠, P🟣, W⚪, K⚫)
|
||||
- Type `n`, `h`, or `x` to select.
|
||||
2. **Guessing:**
|
||||
- Enter a 4-letter code using the color initials (e.g., `RGBY`).
|
||||
- The bot will respond with feedback:
|
||||
- ✅ color ✅ position: correct color in the correct spot
|
||||
- ✅ color 🚫 position: correct color, wrong spot
|
||||
- 🚫No pins: none of your colors are in the code
|
||||
- You have 10 turns to guess the code.
|
||||
3. **Winning:**
|
||||
- Guess the code exactly to win!
|
||||
- Your number of turns is tracked for high scores.
|
||||
- After a win or loss, you can play again by choosing a difficulty.
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
The colors to choose from are:
|
||||
R🔴, Y🟡, G🟢, B🔵
|
||||
Enter your guess (e.g., RGBY):
|
||||
> RGYB
|
||||
|
||||
Turn 1:
|
||||
Guess🔴🟢🟡🔵
|
||||
✅ color ✅ position: 2
|
||||
✅ color 🚫 position: 1
|
||||
|
||||
> RYGB
|
||||
|
||||
Turn 2:
|
||||
🏆Correct🔴🟡🟢🔵
|
||||
You are the master mind!🤯
|
||||
🏆 High Score:2 turns, Difficulty:n
|
||||
Would you like to play again? (N)ormal, (H)ard, or e(X)pert?
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Only one game session per player at a time.
|
||||
- High scores are saved in `data/mmind_hs.pkl`.
|
||||
- Play via DM for best experience.
|
||||
- Input is not case-sensitive, but guesses must be exactly 4 letters.
|
||||
|
||||
## Credits
|
||||
|
||||
- Ported from [pwdkramer/pythonMastermind](https://github.com/pwdkramer/pythonMastermind)
|
||||
- Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
|
||||
# Video Poker Game Module
|
||||
|
||||
A text-based Video Poker game for the Meshtastic mesh-bot. Play classic five-card draw poker, place your bets, and try to build your bankroll!
|
||||
|
||||
## How to Play
|
||||
|
||||
- **Start the Game:**
|
||||
Send the command `videopoker` via DM to the bot to begin a new session.
|
||||
|
||||
- **Objective:**
|
||||
Win as many coins as possible by making the best poker hands.
|
||||
|
||||
- **Game Flow:**
|
||||
1. **Place Your Bet:**
|
||||
- You start with 20 coins.
|
||||
- Enter your bet (1-5 coins) to begin each hand.
|
||||
2. **Draw Cards:**
|
||||
- You are dealt 5 cards.
|
||||
- The bot will show your hand and a hint about its strength.
|
||||
3. **Redraw:**
|
||||
- Choose which cards to replace:
|
||||
- Enter numbers (e.g., `1,3,4`) to redraw those cards.
|
||||
- Enter `a` to redraw all cards.
|
||||
- Enter `n` to keep your current hand.
|
||||
- Enter `h` to show your hand again.
|
||||
- You can only redraw once per hand.
|
||||
4. **Scoring:**
|
||||
- After the redraw, your hand is scored and winnings are paid out based on the hand type.
|
||||
- If you run out of coins, your balance resets to 20.
|
||||
- High scores are tracked and announced.
|
||||
5. **Continue:**
|
||||
- Place another bet to play again, or enter `l` to leave the table.
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
You have 20 coins,
|
||||
Whats your bet?
|
||||
> 5
|
||||
|
||||
K♠️ 7♦️ 7♣️ 2♥️ 9♠️
|
||||
Showing:Pair👯
|
||||
Deal new card?
|
||||
ex: 1,3,4 or (N)o,(A)ll (H)and
|
||||
> 1,4
|
||||
|
||||
7♦️ 7♣️ 9♠️ 3♣️ Q♥️
|
||||
Your hand, Pair👯. Your bankroll is now 22 coins.
|
||||
Place your Bet, or (L)eave Table.
|
||||
```
|
||||
|
||||
## Hand Rankings & Payouts
|
||||
|
||||
- 👑Royal Flush🚽 — 10x bet
|
||||
- 🧻Straight Flush🚽 — 9x bet
|
||||
- Flush🚽 — 8x bet
|
||||
- Full House🏠 — 7x bet
|
||||
- Four of a Kind👯👯 — 6x bet
|
||||
- Three of a Kind☘️ — 5x bet
|
||||
- Two Pair👯👯 — 4x bet
|
||||
- Straight📏 — 3x bet
|
||||
- Pair👯 — 2x bet
|
||||
- Bad Hand 🙈 — Lose bet
|
||||
|
||||
## Notes
|
||||
|
||||
- Only one game session per player at a time.
|
||||
- High scores are saved in `data/videopoker_hs.pkl`.
|
||||
- Play via DM for best experience.
|
||||
- Bets must be between 1 and 5 coins and not exceed your bankroll.
|
||||
|
||||
## Credits
|
||||
|
||||
- Ported from [devtronvarma/Video-Poker-Terminal-Game](https://github.com/devtronvarma/Video-Poker-Terminal-Game)
|
||||
- Adapted for Meshtastic mesh-bot by K7MHI Kelly Keeton 2024
|
||||
|
||||
|
||||
# Word of the Day Game — Rules & Features
|
||||
|
||||
- **Word of the Day:**
|
||||
Each day, a new word is chosen from `data/wotd.json` (or a default list if missing). Mention the word (or its leet/1337 variants) in chat to win and trigger a new word.
|
||||
- **Bingo Mini-Game:**
|
||||
A random 3x3 bingo card of words, drawn from `data/bingo.json` (or a default list if missing). Mention words from the card in chat. Complete a row, column, or diagonal to win BINGO and get a new card.
|
||||
- **Emoji Mini-Game:**
|
||||
Use emojis in chat to:
|
||||
- Play a slot machine: send the same emoji several times in a row to hit the JACKPOT!
|
||||
- **Data Files:**
|
||||
- `data/wotd.json`: List of words and definitions for the Word of the Day.
|
||||
[
|
||||
{
|
||||
"word": "serendipity",
|
||||
"meta": "The occurrence of events by chance in a happy or beneficial way."
|
||||
},
|
||||
{
|
||||
"word": "ephemeral",
|
||||
"meta": "Lasting for a very short time."
|
||||
},
|
||||
{
|
||||
"word": "sonder",
|
||||
"meta": "The realization that each passerby has a life as vivid and complex as your own."
|
||||
}
|
||||
]
|
||||
- `data/bingo.json`: List of words for bingo cards.
|
||||
[
|
||||
"dog",
|
||||
"cat",
|
||||
"fish",
|
||||
"bird",
|
||||
"hamster",
|
||||
"rabbit",
|
||||
"turtle",
|
||||
"lizard",
|
||||
"snake"
|
||||
]
|
||||
@@ -7,8 +7,7 @@ import time
|
||||
import pickle
|
||||
|
||||
jack_starting_cash = 100 # Replace 100 with your desired starting cash value
|
||||
jackTracker= [{'nodeID': 0, 'cmd': 'new', 'cash': jack_starting_cash,\
|
||||
'bet': 0, 'gameStats': {'p_win': 0, 'd_win': 0, 'draw': 0}, 'p_cards':[], 'd_cards':[], 'p_hand':[], 'd_hand':[], 'next_card':[],'last_played': time.time()}]
|
||||
from modules.settings import jackTracker
|
||||
|
||||
SUITS = ("♥️", "♦️", "♠️", "♣️")
|
||||
RANKS = (
|
||||
@@ -114,22 +113,35 @@ class jackChips:
|
||||
self.total -= self.bet
|
||||
self.winnings -= 1
|
||||
|
||||
def success_rate(card, obj_h):
|
||||
""" Calculate Success rate of 'HIT' new cards """
|
||||
msg = ""
|
||||
rate = 0
|
||||
diff = 21 - obj_h.value
|
||||
if diff != 0:
|
||||
rate = (VALUES[card[0][1]] / diff) * 100
|
||||
def success_rate(next_card, player_hand):
|
||||
# Estimate the chance of a successful 'HIT' (not busting) in blackjack.
|
||||
|
||||
if rate < 100:
|
||||
msg += f"If Hit, chance {int(rate)}% failure, {100-int(rate)}% success."
|
||||
else:
|
||||
l_rate = int(rate - (rate - 99)) # Round to 99
|
||||
if card[0][1] == "A":
|
||||
l_rate -= 99
|
||||
msg += f"If Hit, chance {100-l_rate}% failure, and {l_rate}% success"
|
||||
return msg
|
||||
# If player already has 21 or more, hitting will always bust
|
||||
if player_hand.value >= 21:
|
||||
return "\n🧠 What do you think?"
|
||||
|
||||
# Calculate how much more the player can add without busting
|
||||
max_safe = 21 - player_hand.value
|
||||
|
||||
safe_cards = 0
|
||||
total_cards = 0
|
||||
for rank in VALUES:
|
||||
# 4 cards of each rank in a standard deck
|
||||
count = 4
|
||||
card_value = VALUES[rank]
|
||||
# Ace can be 1 or 11, but here we treat it as 1 if 11 would bust
|
||||
if rank == "A":
|
||||
card_value = 1 if player_hand.value + 11 > 21 else 11
|
||||
# Count as safe if it won't bust the player
|
||||
if card_value <= max_safe:
|
||||
safe_cards += count
|
||||
total_cards += count
|
||||
|
||||
# Calculate probability
|
||||
success_chance = int((safe_cards / total_cards) * 100)
|
||||
fail_chance = 100 - success_chance
|
||||
|
||||
return f"\n🧠Hit: {fail_chance}% 👎, {success_chance}% 👍"
|
||||
|
||||
def hits(obj_de):
|
||||
new_card = [obj_de.deal_cards()[0][0]]
|
||||
@@ -147,12 +159,12 @@ def display_hand(hand):
|
||||
|
||||
def show_some(player_cards, dealer_cards, obj_h):
|
||||
msg = f"Player[{obj_h.value}] {display_hand(player_cards)} "
|
||||
msg += f"Dealer[{VALUES[dealer_cards[1][1]]}] {dealer_cards[1][1]}{dealer_cards[1][0]} "
|
||||
msg += f"\nDealer[{VALUES[dealer_cards[1][1]]}] {dealer_cards[1][1]}{dealer_cards[1][0]} "
|
||||
return msg
|
||||
|
||||
def show_all(player_cards, dealer_cards, obj_h, obj_d):
|
||||
msg = f"Player[{obj_h.value}] {display_hand(player_cards)} "
|
||||
msg += f"Dealer[{obj_d.value}] {display_hand(dealer_cards)}"
|
||||
msg += f"\nDealer[{obj_d.value}] {display_hand(dealer_cards)}"
|
||||
return msg
|
||||
|
||||
def player_bust(obj_h, obj_c):
|
||||
@@ -229,7 +241,7 @@ def loadHSJack():
|
||||
pickle.dump(highScore, file)
|
||||
return 0
|
||||
|
||||
def playBlackJack(nodeID, message):
|
||||
def playBlackJack(nodeID, message, last_cmd=None):
|
||||
# Initalize the Game
|
||||
msg, last_cmd = '', None
|
||||
blackJack = False
|
||||
@@ -267,10 +279,12 @@ def playBlackJack(nodeID, message):
|
||||
|
||||
if last_cmd is None:
|
||||
# create new player if not in tracker
|
||||
logger.debug(f"System: BlackJack: New Player {nodeID}")
|
||||
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'last_played': time.time(), 'cash': jack_starting_cash,\
|
||||
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
|
||||
return f"Welcome to ♠️♥️BlackJack♣️♦️ you have {p_chips.total} chips. Whats your bet?"
|
||||
if nodeID != 0:
|
||||
#logger.debug(f"System: BlackJack: New Player {nodeID}")
|
||||
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'last_played': time.time(), 'cash': jack_starting_cash,\
|
||||
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
|
||||
return f"You have {p_chips.total} chips. Whats your bet?"
|
||||
return "Error: Player not found."
|
||||
|
||||
if getLastCmdJack(nodeID) == "new":
|
||||
# Place Bet
|
||||
@@ -283,24 +297,26 @@ def playBlackJack(nodeID, message):
|
||||
#resend the hand
|
||||
msg += show_some(p_cards, d_cards, p_hand)
|
||||
return msg
|
||||
elif "blackjack" in message.lower():
|
||||
return f"\nTo place a bet, enter the amount you wish to wager."
|
||||
else:
|
||||
try:
|
||||
bet_money = int(message)
|
||||
except ValueError:
|
||||
return "Invalid Bet, please enter a valid number."
|
||||
return f"\nInvalid Bet, please enter a valid number."
|
||||
|
||||
if bet_money <= p_chips.total and bet_money >= 1:
|
||||
p_chips.bet = bet_money
|
||||
else:
|
||||
return f"Invalid Bet, the maximum bet you can place is {p_chips.total} and the minimum bet is 1."
|
||||
return f"\nInvalid Bet, the maximum bet you can place is {p_chips.total} and the minimum bet is 1."
|
||||
except ValueError:
|
||||
return f"Invalid Bet, the maximum bet, {p_chips.total}"
|
||||
return f"\nInvalid Bet, the maximum bet, {p_chips.total}"
|
||||
|
||||
# Show the cards
|
||||
msg += show_some(p_cards, d_cards, p_hand)
|
||||
# check for blackjack 21 and only two cards
|
||||
if p_hand.value == 21 and len(p_hand.cards) == 2:
|
||||
msg += "Player 🎰 BLAAAACKJACKKKK 💰"
|
||||
msg += f"\n🎰 BLAAAACKJACKKKK 💰"
|
||||
p_chips.total += round(p_chips.bet * 1.5)
|
||||
setLastCmdJack(nodeID, "dealerTurn")
|
||||
blackJack = True
|
||||
@@ -317,7 +333,7 @@ def playBlackJack(nodeID, message):
|
||||
|
||||
if getLastCmdJack(nodeID) == "betPlaced":
|
||||
setLastCmdJack(nodeID, "playing")
|
||||
msg += "(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
|
||||
msg += f"\n(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
|
||||
|
||||
# save the game state
|
||||
for i in range(len(jackTracker)):
|
||||
@@ -367,7 +383,7 @@ def playBlackJack(nodeID, message):
|
||||
# Check if player bust
|
||||
if player_bust(p_hand, p_chips):
|
||||
d_win += 1
|
||||
msg += "💥PlayerBUST💥"
|
||||
msg += f"\n💥PlayerBUST💥"
|
||||
setLastCmdJack(nodeID, "dealerTurn")
|
||||
|
||||
if getLastCmdJack(nodeID) == "playing":
|
||||
@@ -419,7 +435,7 @@ def playBlackJack(nodeID, message):
|
||||
d_hand.add_cards(d_card)
|
||||
if dealer_bust(d_hand, p_hand, p_chips):
|
||||
p_win += 1
|
||||
msg += "💰DealerBUST💥"
|
||||
msg += f"\n💰DealerBUST💥"
|
||||
break
|
||||
# Show all cards
|
||||
msg += show_all(p_hand.cards, d_hand.cards, p_hand, d_hand)
|
||||
@@ -427,15 +443,15 @@ def playBlackJack(nodeID, message):
|
||||
# Check who wins
|
||||
if push(p_hand, d_hand):
|
||||
draw += 1
|
||||
msg += "👌PUSH"
|
||||
msg += f"\n👌PUSH"
|
||||
elif player_wins(p_hand, d_hand, p_chips):
|
||||
p_win += 1
|
||||
msg += "🎉PLAYER WINS🎰"
|
||||
msg += f"\n🎉PLAYER WINS🎰"
|
||||
elif dealer_wins(p_hand, d_hand, p_chips):
|
||||
d_win += 1
|
||||
msg += "👎DEALER WINS"
|
||||
msg += f"\n👎DEALER WINS"
|
||||
else:
|
||||
msg += "👎DEALER WINS"
|
||||
msg += f"\n👎DEALER WINS"
|
||||
|
||||
# Display the Game Stats
|
||||
msg += gameStats(str(p_win), str(d_win), str(draw))
|
||||
@@ -443,20 +459,20 @@ def playBlackJack(nodeID, message):
|
||||
# Display the chips left
|
||||
if p_chips.total < 1:
|
||||
if p_chips.total > 0:
|
||||
msg += "🪙Keep the change you filthy animal!"
|
||||
msg += f"\n🪙Keep the change you filthy animal!"
|
||||
else:
|
||||
msg += "💸NO MORE CHIPS!🏧💳"
|
||||
msg += f"\n💸NO MORE CHIPS!🏧💳"
|
||||
p_chips.total = jack_starting_cash
|
||||
else:
|
||||
# check high score
|
||||
highScore = loadHSJack()
|
||||
if highScore != 0 and p_chips.total > highScore['highScore']:
|
||||
msg += f"💰HighScore💰{p_chips.total} "
|
||||
msg += f"\n💰HighScore💰{p_chips.total} "
|
||||
saveHSJack(nodeID, p_chips.total)
|
||||
else:
|
||||
msg += f"💰You have {p_chips.total} chips "
|
||||
msg += f"\n💰You have {p_chips.total} chips "
|
||||
|
||||
msg += " Bet or Leave?"
|
||||
msg += f"\nBet or Leave?"
|
||||
|
||||
# Reset the game
|
||||
setLastCmdJack(nodeID, "new")
|
||||
|
||||
@@ -14,7 +14,7 @@ dwInventoryDb = [{'userID': 1234567890, 'inventory': 0, 'priceList': [], 'amount
|
||||
dwCashDb = [{'userID': 1234567890, 'cash': starting_cash},]
|
||||
dwGameDayDb = [{'userID': 1234567890, 'day': 0},]
|
||||
dwLocationDb = [{'userID': 1234567890, 'location': 'USA', 'loc_choice': 0},]
|
||||
dwPlayerTracker = [{'userID': 1234567890, 'last_played': time.time(), 'cmd': 'start'},]
|
||||
from modules.settings import dwPlayerTracker
|
||||
# high score is saved in a pickle file
|
||||
dwHighScore = {}
|
||||
|
||||
@@ -366,7 +366,8 @@ def get_location_table(nodeID, choice=0):
|
||||
return loc_table_string
|
||||
|
||||
def endGameDw(nodeID):
|
||||
global dwCashDb, dwInventoryDb, dwLocationDb, dwGameDayDb, dwHighScore
|
||||
global dwCashDb, dwInventoryDb, dwLocationDb, dwGameDayDb, dwHighScore, dwPlayerTracker
|
||||
cash = 0
|
||||
msg = ''
|
||||
dwHighScore = getHighScoreDw()
|
||||
# Confirm the cash for the user
|
||||
@@ -375,23 +376,6 @@ def endGameDw(nodeID):
|
||||
cash = dwCashDb[i].get('cash')
|
||||
logger.debug("System: DopeWars: Game Over for user: " + str(nodeID) + " with cash: " + str(cash))
|
||||
|
||||
# remove the player from the game databases
|
||||
for i in range(0, len(dwCashDb)):
|
||||
if dwCashDb[i].get('userID') == nodeID:
|
||||
dwCashDb.pop(i)
|
||||
for i in range(0, len(dwInventoryDb)):
|
||||
if dwInventoryDb[i].get('userID') == nodeID:
|
||||
dwInventoryDb.pop(i)
|
||||
for i in range(0, len(dwLocationDb)):
|
||||
if dwLocationDb[i].get('userID') == nodeID:
|
||||
dwLocationDb.pop(i)
|
||||
for i in range(0, len(dwGameDayDb)):
|
||||
if dwGameDayDb[i].get('userID') == nodeID:
|
||||
dwGameDayDb.pop(i)
|
||||
for i in range(0, len(dwPlayerTracker)):
|
||||
if dwPlayerTracker[i].get('userID') == nodeID:
|
||||
dwPlayerTracker.pop(i)
|
||||
|
||||
# checks if the player's score is higher than the high score and writes a new high score if it is
|
||||
if cash > dwHighScore.get('cash'):
|
||||
dwHighScore = ({'userID': nodeID, 'cash': round(cash, 2)})
|
||||
|
||||
@@ -26,7 +26,7 @@ par4_5_range = par4_range + par5_range
|
||||
|
||||
# Player setup
|
||||
playingHole = False
|
||||
golfTracker = [{'nodeID': 0, 'last_played': time.time(), 'cmd': '', 'hole': 0, 'distance_remaining': 0, 'hole_shots': 0, 'hole_strokes': 0, 'hole_to_par': 0, 'total_strokes': 0, 'total_to_par': 0, 'par': 0, 'hazard': ''}]
|
||||
from modules.settings import golfTracker
|
||||
|
||||
# Club functions
|
||||
def hit_driver():
|
||||
@@ -122,9 +122,8 @@ def getHighScoreGolf(nodeID, strokes, par):
|
||||
return 0
|
||||
|
||||
# Main game loop
|
||||
def playGolf(nodeID, message, finishedHole=False):
|
||||
def playGolf(nodeID, message, finishedHole=False, last_cmd=''):
|
||||
msg = ''
|
||||
global golfTracker
|
||||
# Course setup
|
||||
par3_count = 0
|
||||
par4_count = 0
|
||||
@@ -146,8 +145,12 @@ def playGolf(nodeID, message, finishedHole=False):
|
||||
par = golfTracker[i]['par']
|
||||
total_strokes = golfTracker[i]['total_strokes']
|
||||
total_to_par = golfTracker[i]['total_to_par']
|
||||
|
||||
if last_cmd == "" or last_cmd == "new":
|
||||
#update last played time
|
||||
for i in range(len(golfTracker)):
|
||||
if golfTracker[i]['nodeID'] == nodeID:
|
||||
golfTracker[i]['last_played'] = time.time()
|
||||
|
||||
if last_cmd == "new":
|
||||
# Start a new hole
|
||||
if hole <= 9:
|
||||
# Set up hole count restrictions on par
|
||||
@@ -194,17 +197,19 @@ def playGolf(nodeID, message, finishedHole=False):
|
||||
# Set initial parameters before starting a hole
|
||||
distance_remaining = hole_length
|
||||
hole_shots = 0
|
||||
last_cmd = 'stroking'
|
||||
|
||||
# save player's current game state
|
||||
for i in range(len(golfTracker)):
|
||||
if golfTracker[i]['nodeID'] == nodeID:
|
||||
golfTracker[i]['cmd'] = last_cmd
|
||||
golfTracker[i]['hole'] = hole
|
||||
golfTracker[i]['distance_remaining'] = distance_remaining
|
||||
golfTracker[i]['cmd'] = 'stroking'
|
||||
golfTracker[i]['par'] = par
|
||||
golfTracker[i]['total_strokes'] = total_strokes
|
||||
golfTracker[i]['total_to_par'] = total_to_par
|
||||
golfTracker[i]['hazard'] = hazard
|
||||
golfTracker[i]['hole'] = hole
|
||||
golfTracker[i]['last_played'] = time.time()
|
||||
golfTracker[i]['hole_shots'] = hole_shots
|
||||
|
||||
@@ -321,8 +326,8 @@ def playGolf(nodeID, message, finishedHole=False):
|
||||
else:
|
||||
last_cmd = 'stroking'
|
||||
else:
|
||||
msg += "\nYou have " + str(distance_remaining) + "yd. ⛳️"
|
||||
msg += "\nClub?[D, L, M, H, G, W]🏌️"
|
||||
msg += f"\nYou have " + str(distance_remaining) + "yd. ⛳️"
|
||||
msg += f"\nClub?[D, L, M, H, G, W]🏌️"
|
||||
|
||||
|
||||
# save player's current game state, keep stroking
|
||||
@@ -366,7 +371,7 @@ def playGolf(nodeID, message, finishedHole=False):
|
||||
|
||||
if hole not in [1, 10]:
|
||||
# Show player total scoring info for the round, except hole 1 and 10
|
||||
msg += "\nYou've hit a total of " + str(total_strokes) + " strokes today, for"
|
||||
msg += f"\nYou've hit a total of " + str(total_strokes) + " strokes today, for"
|
||||
msg += getScorecardGolf(total_to_par)
|
||||
|
||||
# Move to next hole
|
||||
@@ -404,7 +409,7 @@ def playGolf(nodeID, message, finishedHole=False):
|
||||
logger.debug("System: GolfSim: Player " + str(nodeID) + " has finished their round.")
|
||||
else:
|
||||
# Show player the next hole
|
||||
msg += playGolf(nodeID, 'new', True)
|
||||
msg += "\n🏌️[D, L, M, H, G, W, End]🏌️"
|
||||
msg += playGolf(nodeID, '', True, last_cmd='new')
|
||||
msg += f"\n🏌️[D, L, M, H, G, W, End]🏌️"
|
||||
|
||||
return msg
|
||||
|
||||
@@ -46,15 +46,27 @@ lameJokes = [
|
||||
"Chuck Norris can kill two stones with one bird.",
|
||||
"Chuck Norris can speak braille.",
|
||||
"Chuck Norris can build a snowman out of rain.",
|
||||
"Chuck Norris can hear sign language.",
|
||||
"Death once had a near-Chuck Norris experience.",
|
||||
"Chuck Norris can unscramble an egg.",
|
||||
"Chuck Norris can win a game of Connect Four in only three moves.",
|
||||
"Chuck Norris can make a snowman out of rain.",
|
||||
"Chuck Norris can strangle you with a cordless phone.",
|
||||
"Chuck Norris can do a wheelie on a unicycle.",
|
||||
"Chuck Norris can kill two stones with one bird."]
|
||||
"This is a test. A test of the Joke Brodcast System. If this had been an actual joke, you would have been amused.",
|
||||
"Chuck Norris doesn't join mesh networks. Mesh networks join Chuck's topology.",
|
||||
"Every time Chuck Norris sends a packet, it arrives before he hits 'send'",
|
||||
"Chuck Norris doesn't need LoRa. His roundhouse kick has a 15km range with zero latency.",
|
||||
"When Chuck Norris uses a node, the bandwidth doubles out of fear.",
|
||||
"Chuck Norris once pinged a device. It replied with an apology and a firmware update.",
|
||||
"Chuck Norris doesn't use AES encryption. His packets are so secure, they punch hackers in the bits.",
|
||||
"The Meshtastic protocol has a hidden mode: “Chuck Norris mode.” It only activates when he blinks.",
|
||||
"Chuck Norris doesn't need a GPS fix. Satellites triangulate themselves around him.",
|
||||
"Chuck Norris's mesh node doesn't sleep. It meditates while transmitting at full power.",
|
||||
"Chuck Norris doesn't broadcast. He declares.",
|
||||
"Chuck Norris once bridged two mesh networks using a shoelace.",
|
||||
"Chuck Norris's packets don't hop. They teleport out of respect.",
|
||||
"Chuck Norris doesn't need a repeater. Client_Mute is set to 'Always'.",
|
||||
"Chuck Norris's mesh messages are entangled. When he sends one, it's already received.",
|
||||
"Chuck Norris doesn't mesh with others. Others mesh with Chuck.",
|
||||
"Chuck Norris's node doesn't need a case. The PCB is armored with his beard hair.",
|
||||
"Chuck Norris once typed “Hello World” and the world replied 'Hello Chuck.'",
|
||||
]
|
||||
|
||||
# pylint: disable=C0103, W0612
|
||||
imtellingyourightnowiAmTellingYouRightNowThatMotherfErBackThereIsNotReal = ["🐦", "🦅", "🦆", "🦉", "🦜", "🐤", "🐥", "🐣", "🐔", "🐧", "🦚", "🦢", "🦩", "🦤", "🦃", "🐓"]
|
||||
|
||||
def tableOfContents():
|
||||
@@ -168,14 +180,14 @@ def sendWithEmoji(message):
|
||||
i += 1
|
||||
return ' '.join(words)
|
||||
|
||||
def tell_joke(nodeID=0):
|
||||
def tell_joke(nodeID=0, vox=False):
|
||||
dadjoke = Dadjoke()
|
||||
try:
|
||||
if dad_jokes_emojiJokes:
|
||||
if dad_jokes_emojiJokes or vox:
|
||||
renderedLaugh = sendWithEmoji(dadjoke.joke)
|
||||
else:
|
||||
renderedLaugh = dadjoke.joke
|
||||
return renderedLaugh
|
||||
except Exception as e:
|
||||
return random.choice(lameJokes)
|
||||
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ locale.setlocale(locale.LC_ALL, '')
|
||||
lemon_starting_cash = 30.00
|
||||
lemon_total_weeks = 7
|
||||
|
||||
lemonadeTracker = [{'nodeID': 0, 'cups': 0, 'lemons': 0, 'sugar': 0, 'cash': lemon_starting_cash, 'start': lemon_starting_cash, 'cmd': 'new', 'last_played': time.time()}]
|
||||
lemonadeCups = [{'nodeID': 0, 'cost': 2.50, 'count': 25, 'min': 0.99, 'unit': 0.00}]
|
||||
lemonadeLemons = [{'nodeID': 0, 'cost': 4.00, 'count': 8, 'min': 2.00, 'unit': 0.00}]
|
||||
lemonadeSugar = [{'nodeID': 0, 'cost': 3.00, 'count': 15, 'min': 1.50, 'unit': 0.00}]
|
||||
lemonadeWeeks = [{'nodeID': 0, 'current': 1, 'total': lemon_total_weeks, 'sales': 99, 'potential': 0, 'unit': 0.00, 'price': 0.00, 'total_sales': 0}]
|
||||
lemonadeScore = [{'nodeID': 0, 'value': 0.00, 'total': 0.00}]
|
||||
from modules.settings import lemonadeTracker
|
||||
|
||||
def get_sales_amount(potential, unit, price):
|
||||
"""Gets the sales amount.
|
||||
@@ -50,13 +50,14 @@ def getHighScoreLemon():
|
||||
pickle.dump(high_score, file)
|
||||
return high_score
|
||||
|
||||
def playLemonstand(nodeID, message, celsius=False):
|
||||
def playLemonstand(nodeID, message, celsius=False, newgame=False):
|
||||
global lemonadeTracker, lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore
|
||||
msg = ""
|
||||
potential = 0
|
||||
unit = 0.0
|
||||
price = 0.0
|
||||
total_sales = 0
|
||||
lemonsLastCmd = ''
|
||||
|
||||
high_score = getHighScoreLemon()
|
||||
|
||||
@@ -95,33 +96,6 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
lemonadeScore[i]['value'] = score.value
|
||||
lemonadeScore[i]['total'] = score.total
|
||||
|
||||
def endGame(nodeID):
|
||||
# remove the player from the tracker
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker.pop(i)
|
||||
for i in range(len(lemonadeCups)):
|
||||
if lemonadeCups[i]['nodeID'] == nodeID:
|
||||
lemonadeCups.pop(i)
|
||||
for i in range(len(lemonadeLemons)):
|
||||
if lemonadeLemons[i]['nodeID'] == nodeID:
|
||||
lemonadeLemons.pop(i)
|
||||
for i in range(len(lemonadeSugar)):
|
||||
if lemonadeSugar[i]['nodeID'] == nodeID:
|
||||
lemonadeSugar.pop(i)
|
||||
for i in range(len(lemonadeWeeks)):
|
||||
if lemonadeWeeks[i]['nodeID'] == nodeID:
|
||||
lemonadeWeeks.pop(i)
|
||||
for i in range(len(lemonadeScore)):
|
||||
if lemonadeScore[i]['nodeID'] == nodeID:
|
||||
lemonadeScore.pop(i)
|
||||
logger.debug("System: Lemonade: Game Over for " + str(nodeID))
|
||||
|
||||
# Check for end of game
|
||||
if message.lower().startswith("e"):
|
||||
endGame(nodeID)
|
||||
return "Goodbye!👋"
|
||||
|
||||
title="LemonStand🍋"
|
||||
# Define the temperature unit symbols
|
||||
fahrenheit_unit = "ºF"
|
||||
@@ -240,22 +214,35 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
if lemonadeScore[i]['nodeID'] == nodeID:
|
||||
score.value = lemonadeScore[i]['value']
|
||||
score.total = lemonadeScore[i]['total']
|
||||
|
||||
#handle last command
|
||||
lemonsLastCmd = 'new'
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonsLastCmd = lemonadeTracker[i]['cmd']
|
||||
|
||||
if (newgame):
|
||||
# reset the game values
|
||||
inventory.cups = 0
|
||||
inventory.lemons = 0
|
||||
inventory.sugar = 0
|
||||
inventory.cash = lemon_starting_cash
|
||||
inventory.start = lemon_starting_cash
|
||||
cups.cost = 2.50
|
||||
cups.unit = round(cups.cost / cups.count, 2)
|
||||
lemons.cost = 4.00
|
||||
lemons.unit = round(lemons.cost / lemons.count, 2)
|
||||
sugar.cost = 3.00
|
||||
sugar.unit = round(sugar.cost / sugar.count, 2)
|
||||
weeks.current = 1
|
||||
weeks.total_sales = 0
|
||||
weeks.summary = []
|
||||
score.value = 0.00
|
||||
score.total = 0.00
|
||||
lemonsLastCmd = "cups"
|
||||
# set the last command to new in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cmd'] = "cups"
|
||||
lemonadeTracker[i]['last_played'] = time.time()
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
# Start the main loop
|
||||
if (weeks.current <= weeks.total):
|
||||
|
||||
if "new" in lemonsLastCmd:
|
||||
if newgame or "new" in lemonsLastCmd:
|
||||
logger.debug("System: Lemonade: New Game: " + str(nodeID))
|
||||
# set the last command to cups in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cmd'] = "cups"
|
||||
# Create a new display buffer for the text messages
|
||||
buffer= ""
|
||||
|
||||
@@ -272,7 +259,7 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
buffer += ". " + \
|
||||
formatted + temperature.units + " " + \
|
||||
forecastd[list(forecastd)[temperature.forecast]][2] + \
|
||||
" " + glyph
|
||||
" " + glyph + f"\n"
|
||||
|
||||
# Calculate the potential sales as a percentage of the maximum value
|
||||
# (lower temperature = fewer sales, severe weather = fewer sales)
|
||||
@@ -300,44 +287,39 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
sugar.unit = round(sugar.cost / sugar.count, 2)
|
||||
|
||||
# Calculate the unit cost and display the estimated sales from the forecast potential
|
||||
unit = cups.unit + lemons.unit + sugar.unit
|
||||
buffer += " SupplyCost" + locale.currency(unit, grouping=True) + " a cup."
|
||||
buffer += " Sales Potential:" + str(potential) + " cups."
|
||||
unit = max(0.01, min(cups.unit + lemons.unit + sugar.unit, 4.0)) # limit the unit cost between $0.01 and $4.00
|
||||
buffer += f"\nSupplyCost" + locale.currency(round(unit, 2), grouping=True) + " a cup."
|
||||
buffer += f"\nSales Potential:" + str(potential) + " cups."
|
||||
|
||||
# Display the current inventory
|
||||
buffer += " Inventory:"
|
||||
buffer += f"\nInventory:"
|
||||
buffer += "🥤:" + str(inventory.cups)
|
||||
buffer += "🍋:" + str(inventory.lemons)
|
||||
buffer += "🍚:" + str(inventory.sugar)
|
||||
|
||||
# Display the updated item prices
|
||||
buffer += f"\nPrices: "
|
||||
buffer += "🥤:" + \
|
||||
locale.currency(cups.cost, grouping=True) + " 📦 of " + str(cups.count) + "."
|
||||
buffer += " 🍋:" + \
|
||||
locale.currency(lemons.cost, grouping=True) + " 🧺 of " + str(lemons.count) + "."
|
||||
buffer += " 🍚:" + \
|
||||
locale.currency(sugar.cost, grouping=True) + " bag for " + str(sugar.count) + "🥤."
|
||||
|
||||
buffer += f"\nPrices:\n"
|
||||
buffer += f"\n🥤:" + locale.currency(round(cups.cost, 2), grouping=True) + " 📦 of " + str(cups.count) + "."
|
||||
buffer += f"\n🍋:" + locale.currency(round(lemons.cost, 2), grouping=True) + " 🧺 of " + str(lemons.count) + "."
|
||||
buffer += f"\n🍚:" + locale.currency(round(sugar.cost, 2), grouping=True) + " bag for " + str(sugar.count) + "🥤."
|
||||
# Display the current cash
|
||||
gainloss = inventory.cash - inventory.start
|
||||
buffer += " 💵:" + \
|
||||
locale.currency(inventory.cash, grouping=True)
|
||||
buffer += f"\n💵:" + locale.currency(round(inventory.cash, 2), grouping=True)
|
||||
|
||||
|
||||
# if the player is in the red
|
||||
pnl = locale.currency(gainloss, grouping=True)
|
||||
pnl = locale.currency(round(gainloss, 2), grouping=True)
|
||||
if "0.00" not in pnl:
|
||||
if pnl.startswith("-"):
|
||||
buffer += "📊P&L📉" + pnl
|
||||
else:
|
||||
buffer += "📊P&L📈" + pnl
|
||||
|
||||
buffer += f"\n🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
|
||||
buffer += f"\n🥤 to buy?\nHave {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
return buffer
|
||||
|
||||
if "cups" in lemonsLastCmd:
|
||||
if "cups" in lemonsLastCmd and not newgame:
|
||||
# Read the number of cup boxes to purchase
|
||||
newcups = -1
|
||||
if "n" in message.lower():
|
||||
@@ -351,22 +333,22 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
inventory.cups += (newcups * cups.count)
|
||||
inventory.cash -= cost
|
||||
msg = "Purchased " + str(newcups) + " 📦 "
|
||||
msg += str(inventory.cups) + " 🥤 in inventory. " + locale.currency(inventory.cash, grouping=True) + f" remaining"
|
||||
msg += str(inventory.cups) + " 🥤 in inventory. " + locale.currency(round(inventory.cash, 2), grouping=True) + f" remaining"
|
||||
else:
|
||||
msg = "No 🥤 were purchased"
|
||||
except Exception as e:
|
||||
return "invalid input, enter the number of 🥤 to purchase or (N)one"
|
||||
|
||||
msg += f"\n 🍋 to buy?\nHave {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
|
||||
# set the last command to lemons in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cmd'] = "lemons"
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
msg += f"\n 🍋 to buy? Have {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
|
||||
return msg
|
||||
|
||||
|
||||
if "lemons" in lemonsLastCmd:
|
||||
if "lemons" in lemonsLastCmd and not newgame:
|
||||
# Read the number of lemon bags to purchase
|
||||
newlemons = -1
|
||||
if "n" in message.lower():
|
||||
@@ -387,15 +369,15 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
newlemons = -1
|
||||
return "⛔️invalid input, enter the number of 🍋 to purchase"
|
||||
|
||||
msg += f"\n 🍚 to buy?\nYou have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
|
||||
# set the last command to sugar in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cmd'] = "sugar"
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
msg += f"\n 🍚 to buy? You have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
|
||||
return msg
|
||||
|
||||
if "sugar" in lemonsLastCmd:
|
||||
if "sugar" in lemonsLastCmd and not newgame:
|
||||
# Read the number of sugar bags to purchase
|
||||
newsugar = -1
|
||||
if "n" in message.lower():
|
||||
@@ -415,8 +397,8 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
except Exception as e:
|
||||
return "⛔️invalid input, enter the number of 🍚 bags to purchase"
|
||||
|
||||
msg += f"Cost of goods is {locale.currency(unit, grouping=True)}"
|
||||
msg += f"per 🥤 {locale.currency(inventory.cash, grouping=True)} 💵 remaining."
|
||||
msg += f"Cost of goods is {locale.currency(round(unit, 2), grouping=True)}"
|
||||
msg += f"per 🥤 {locale.currency(round(inventory.cash, 2), grouping=True)} 💵 remaining."
|
||||
msg += f"\nPrice to Sell? or (G)rocery to buy more 🥤🍋🍚"
|
||||
|
||||
# set the last command to price in the inventory db
|
||||
@@ -426,14 +408,14 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
return msg
|
||||
|
||||
if "price" in lemonsLastCmd:
|
||||
if "price" in lemonsLastCmd and not newgame:
|
||||
# set the last command to sales in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cmd'] = "sales"
|
||||
if "g" in message.lower():
|
||||
lemonadeTracker[i]['cmd'] = "cups"
|
||||
msg = f"#of🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
|
||||
msg = f"#of🥤\nto buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
|
||||
return msg
|
||||
else:
|
||||
lemonsLastCmd = "sales"
|
||||
@@ -456,7 +438,7 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
|
||||
|
||||
if "sales" in lemonsLastCmd:
|
||||
if "sales" in lemonsLastCmd and not newgame:
|
||||
# Calculate the weekly sales based on price and lowest inventory level
|
||||
# (higher markup price = fewer sales, limited by the inventory on-hand)
|
||||
sales = get_sales_amount(potential, unit, price)
|
||||
@@ -486,7 +468,7 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
msg += " N.Profit:" + locale.currency(net, grouping=True)
|
||||
|
||||
# Display the updated inventory levels
|
||||
msg += "\nRemaining"
|
||||
msg += f"\nRemaining"
|
||||
msg += " 🥤:" + str(inventory.cups)
|
||||
msg += " 🍋:" + str(inventory.lemons)
|
||||
msg += " 🍚:" + str(inventory.sugar)
|
||||
@@ -503,7 +485,7 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
pad_week = len(str(weeks.total))
|
||||
pad_sale = len(str(weeks.sales))
|
||||
total = 0
|
||||
msg += "\nWeekly📊"
|
||||
msg += f"\nWeekly📊"
|
||||
for i in range(len(weeks.summary)):
|
||||
msg += "#" + str(weeks.current).rjust(pad_week) + ". " + str(weeks.summary[i]['sales']).rjust(pad_sale) + \
|
||||
" sold x " + locale.currency(weeks.summary[i]['price'], grouping=True) + "ea. "
|
||||
@@ -543,7 +525,7 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
if (inventory.sugar <= 0):
|
||||
msg += " You ran out of sugar.🍚"
|
||||
else:
|
||||
msg += "\nCongratulations 🍋🍋 your sales were perfect!🎉"
|
||||
msg += f"\nCongratulations 🍋🍋 your sales were perfect!🎉"
|
||||
|
||||
# Increment the score counters
|
||||
score.value = score.value + minnet
|
||||
@@ -554,33 +536,31 @@ def playLemonstand(nodeID, message, celsius=False):
|
||||
if (weeks.current == weeks.total):
|
||||
# end of the game
|
||||
success = round((score.value / score.total) * 100)
|
||||
msg += "\nYou've made " + locale.currency(score.value, grouping=True) + " out of a possible " + \
|
||||
msg += f"\nYou've made " + locale.currency(score.value, grouping=True) + " out of a possible " + \
|
||||
locale.currency(score.total, grouping=True) + " for a score of " + str(success) + "% "
|
||||
msg += "You've sold " + str(weeks.total_sales) + " total 🥤🍋"
|
||||
msg += f"\nYou've sold " + str(weeks.total_sales) + " total 🥤🍋"
|
||||
|
||||
# check for high score
|
||||
high_score = getHighScoreLemon()
|
||||
if (inventory.cash > int(high_score['cash'])):
|
||||
msg += "\nCongratulations! You've set a new high score!🎉💰🍋"
|
||||
msg += f"\nCongratulations! You've set a new high score!🎉💰🍋"
|
||||
high_score['cash'] = inventory.cash
|
||||
high_score['success'] = success
|
||||
high_score['userID'] = nodeID
|
||||
with open('data/lemonstand.pkl', 'wb') as file:
|
||||
pickle.dump(high_score, file)
|
||||
endGame(nodeID)
|
||||
|
||||
else:
|
||||
# keep playing
|
||||
|
||||
weeks.current = weeks.current + 1
|
||||
|
||||
msg += f"\nPlay another week🥤? or (E)nd Game"
|
||||
# set the last command to new in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
lemonadeTracker[i]['cmd'] = "new"
|
||||
lemonadeTracker[i]['last_played'] = time.time()
|
||||
|
||||
weeks.current = weeks.current + 1
|
||||
|
||||
msg += f"Play another week🥤? or (E)nd Game"
|
||||
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
return msg
|
||||
else:
|
||||
|
||||
@@ -5,9 +5,7 @@ import random
|
||||
import time
|
||||
import pickle
|
||||
from modules.log import *
|
||||
|
||||
mindTracker = [{'nodeID': 0, 'last_played': time.time(), 'cmd': '', 'secret_code': '', 'diff': 'n', 'turns': 1}]
|
||||
|
||||
from modules.settings import mindTracker
|
||||
def chooseDifficultyMMind(message):
|
||||
usrInput = message.lower()
|
||||
msg = ''
|
||||
@@ -62,98 +60,63 @@ def makeCodeMMind(diff):
|
||||
return secret_code
|
||||
|
||||
#get guess from user
|
||||
def getGuessMMind(diff, guess):
|
||||
msg = ''
|
||||
if diff == "n":
|
||||
valid_colorsMMind = "RYGB"
|
||||
elif diff == "h":
|
||||
valid_colorsMMind = "RYGBOP"
|
||||
elif diff == "x":
|
||||
valid_colorsMMind = "RYGBOPWK"
|
||||
|
||||
user_guess = guess.upper()
|
||||
valid_guess = True
|
||||
if len(user_guess) != 4:
|
||||
valid_guess = False
|
||||
for i in range(len(user_guess)):
|
||||
if user_guess[i] not in valid_colorsMMind:
|
||||
valid_guess = False
|
||||
if valid_guess == False:
|
||||
user_guess = "XXXX"
|
||||
def getGuessMMind(diff, guess, nodeID):
|
||||
valid_colors = {
|
||||
"n": "RYGB",
|
||||
"h": "RYGBOP",
|
||||
"x": "RYGBOPWK"
|
||||
}
|
||||
user_guess = guess.strip().upper()
|
||||
if len(user_guess) != 4 or any(c not in valid_colors.get(diff, "RYGB") for c in user_guess):
|
||||
return "XXXX"
|
||||
|
||||
#increase the turn count and store in tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['turns'] += 1
|
||||
mindTracker[i]['last_played'] = time.time()
|
||||
mindTracker[i]['diff'] = diff
|
||||
return user_guess
|
||||
|
||||
def getHighScoreMMind(nodeID, turns, diff):
|
||||
# check if player is in high score list and pick the lowest score
|
||||
try:
|
||||
with open('mmind_hs.pkl', 'rb') as f:
|
||||
mindHighScore = pickle.load(f)
|
||||
except:
|
||||
logger.debug("System: MasterMind: High Score file not found.")
|
||||
mindHighScore = [{'nodeID': nodeID, 'turns': turns, 'diff': diff}]
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
import os
|
||||
hs_file = 'data/mmind_hs.pkl'
|
||||
# Try to load existing high scores
|
||||
if os.path.exists(hs_file):
|
||||
try:
|
||||
with open(hs_file, 'rb') as f:
|
||||
mindHighScore = pickle.load(f)
|
||||
except Exception as e:
|
||||
logger.debug(f"System: MasterMind: Error loading high score file: {e}")
|
||||
mindHighScore = []
|
||||
else:
|
||||
mindHighScore = []
|
||||
|
||||
# If nodeID==0, just return 0
|
||||
if nodeID == 0:
|
||||
# just return the high score
|
||||
mindHighScore = [{'nodeID': 0, 'turns': 0, 'diff': 'n'}]
|
||||
return mindHighScore
|
||||
|
||||
# calculate lowest score
|
||||
lowest_score = mindHighScore[0]['turns']
|
||||
# If no high score, add this one
|
||||
if not mindHighScore:
|
||||
mindHighScore = [{'nodeID': nodeID, 'turns': turns, 'diff': diff}]
|
||||
with open(hs_file, 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
|
||||
if mindHighScore[0]['diff'] == "n" and diff == "n":
|
||||
if lowest_score > turns:
|
||||
# update the high score for normal if new score is lower
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
# If the diff matches, compare and update if better
|
||||
if mindHighScore[0]['diff'] == diff:
|
||||
if turns < mindHighScore[0]['turns']:
|
||||
mindHighScore[0] = {'nodeID': nodeID, 'turns': turns, 'diff': diff}
|
||||
with open(hs_file, 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
elif mindHighScore[0]['diff'] == "n" and diff == "h":
|
||||
# update the high score for hard if normal is the only high score
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
elif mindHighScore[0]['diff'] == "h" and diff == "h":
|
||||
if lowest_score > turns:
|
||||
# update the high score for hard if new score is lower
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
elif mindHighScore[0]['diff'] == "n" or mindHighScore[0]['diff'] == "h" and diff == "x":
|
||||
# update the high score for expert if normal or high is the only high score
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
elif mindHighScore[0]['diff'] == "x" and diff == "x":
|
||||
if lowest_score > turns:
|
||||
# update the high score for expert if new score is lower
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
return 0
|
||||
|
||||
# If the diff is different, replace with new high score for new diff
|
||||
mindHighScore[0] = {'nodeID': nodeID, 'turns': turns, 'diff': diff}
|
||||
with open(hs_file, 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
|
||||
|
||||
def getEmojiMMind(secret_code):
|
||||
@@ -182,7 +145,7 @@ def getEmojiMMind(secret_code):
|
||||
return secret_code_emoji
|
||||
|
||||
#compare userGuess with secret code and provide feedback
|
||||
def compareCodeMMind(secret_code, user_guess):
|
||||
def compareCodeMMind(secret_code, user_guess, nodeID):
|
||||
game_won = False
|
||||
perfect_pins = 0
|
||||
wrong_position = 0
|
||||
@@ -210,9 +173,26 @@ def compareCodeMMind(secret_code, user_guess):
|
||||
temp_code.remove(guess) # Remove the first occurrence of the matched color
|
||||
# display feedback
|
||||
if game_won:
|
||||
msg += f"Correct{getEmojiMMind(user_guess)}\n"
|
||||
msg += f"\n🏆Correct{getEmojiMMind(user_guess)}\nYou are the master mind!🤯"
|
||||
# get turn count from tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
turns = mindTracker[i]['turns'] - 2 # subtract 2 to account for increment after last guess and starting at 1
|
||||
diff = mindTracker[i]['diff']
|
||||
# get high score
|
||||
high_score = getHighScoreMMind(nodeID, turns, diff)
|
||||
if high_score[0]['turns'] != 0:
|
||||
msg += f"\n🏆 High Score:{turns} turns, Difficulty:{diff}"
|
||||
# reset turn count in tracker
|
||||
msg += f"\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
|
||||
# reset turn count in tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['turns'] = 0
|
||||
mindTracker[i]['secret_code'] = ''
|
||||
mindTracker[i]['cmd'] = 'new'
|
||||
else:
|
||||
msg += f"Guess{getEmojiMMind(user_guess)}\n"
|
||||
msg += f"\nGuess{getEmojiMMind(user_guess)}\n"
|
||||
|
||||
if perfect_pins > 0 and game_won == False:
|
||||
msg += "✅ color ✅ position: {}".format(perfect_pins)
|
||||
@@ -231,11 +211,11 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
|
||||
msg = ''
|
||||
won = False
|
||||
if turn_count <= 10:
|
||||
user_guess = getGuessMMind(diff, message)
|
||||
user_guess = getGuessMMind(diff, message, nodeID)
|
||||
if user_guess == "XXXX":
|
||||
msg += f"⛔️Invalid guess. Please enter 4 valid colors letters.\n🔴🟢🔵🔴 is RGBR"
|
||||
return msg
|
||||
check_guess = compareCodeMMind(secret_code, user_guess)
|
||||
check_guess = compareCodeMMind(secret_code, user_guess, nodeID)
|
||||
|
||||
# display turn count and feedback
|
||||
msg += "Turn {}:".format(turn_count)
|
||||
@@ -245,18 +225,6 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
|
||||
|
||||
if won == True:
|
||||
msg += f"\n🎉🧠 you win 🥷🤯"
|
||||
# get high score
|
||||
high_score = getHighScoreMMind(nodeID, turn_count, diff)
|
||||
if high_score != 0:
|
||||
msg += f"\n🏆 High Score:{high_score[0]['turns']} turns, Difficulty:{high_score[0]['diff'].upper()}"
|
||||
|
||||
msg += "\nWould you like to play again?\n(N)ormal, (H)ard, e(X)pert (E)nd?"
|
||||
# reset turn count in tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['turns'] = 1
|
||||
mindTracker[i]['secret_code'] = ''
|
||||
mindTracker[i]['cmd'] = 'new'
|
||||
else:
|
||||
# increment turn count and keep playing
|
||||
turn_count += 1
|
||||
@@ -266,12 +234,12 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
|
||||
mindTracker[i]['turns'] = turn_count
|
||||
elif won == False:
|
||||
msg += f"🙉Game Over🙈\nThe code was: {getEmojiMMind(secret_code)}"
|
||||
msg += "\nYou have run out of turns.😿"
|
||||
msg += "\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
|
||||
msg += f"\nYou have run out of turns.😿"
|
||||
msg += f"\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
|
||||
# reset turn count in tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['turns'] = 1
|
||||
mindTracker[i]['turns'] = 0
|
||||
mindTracker[i]['secret_code'] = ''
|
||||
mindTracker[i]['cmd'] = 'new'
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
# 2025
|
||||
from modules.log import *
|
||||
import random
|
||||
# to molly and jake, I miss you both so much.
|
||||
import time
|
||||
|
||||
# to (max), molly and jake, I miss you both so much.
|
||||
|
||||
if disable_emojis_in_games:
|
||||
X = "X"
|
||||
@@ -29,7 +31,7 @@ class TicTacToe:
|
||||
if id in self.game:
|
||||
games = self.game[id]["games"]
|
||||
won = self.game[id]["won"]
|
||||
if games > 0:
|
||||
if games > 3:
|
||||
if won / games >= 3.14159265358979323846: # win rate > pi
|
||||
ret += random.choice(positiveThoughts) + "\n"
|
||||
else:
|
||||
@@ -47,6 +49,10 @@ class TicTacToe:
|
||||
ret += self.show_board(id)
|
||||
ret += "Pick 1-9:"
|
||||
return ret
|
||||
|
||||
def rndTeaPrice(self, tea=42):
|
||||
"""Return a random tea between 0 and tea."""
|
||||
return random.uniform(0, tea)
|
||||
|
||||
def show_board(self, id):
|
||||
"""Display compact board with move numbers"""
|
||||
@@ -90,19 +96,30 @@ class TicTacToe:
|
||||
return True
|
||||
|
||||
def bot_move(self, id):
|
||||
"""AI makes a move"""
|
||||
"""AI makes a move: tries to win, block, or pick random"""
|
||||
g = self.game[id]
|
||||
|
||||
# Simple AI: Try to win, block, or pick random
|
||||
move = self.find_winning_move(id, O) # Try to win
|
||||
if move == -1:
|
||||
move = self.find_winning_move(id, X) # Block player
|
||||
if move == -1:
|
||||
move = self.find_random_move(id) # Random move
|
||||
|
||||
board = g["board"]
|
||||
|
||||
# Try to win
|
||||
move = self.find_winning_move(id, O)
|
||||
if move != -1:
|
||||
g["board"][move] = O
|
||||
return move
|
||||
board[move] = O
|
||||
return move
|
||||
|
||||
# Try to block player
|
||||
move = self.find_winning_move(id, X)
|
||||
if move != -1:
|
||||
board[move] = O
|
||||
return move
|
||||
|
||||
# Pick random move
|
||||
move = self.find_random_move(id)
|
||||
if move != -1:
|
||||
board[move] = O
|
||||
return move
|
||||
|
||||
# No moves possible
|
||||
return -1
|
||||
|
||||
def find_winning_move(self, id, player):
|
||||
"""Find a winning move for the given player"""
|
||||
@@ -117,12 +134,22 @@ class TicTacToe:
|
||||
return i
|
||||
board[i] = " "
|
||||
return -1
|
||||
|
||||
def find_random_move(self, id):
|
||||
"""Find a random empty position"""
|
||||
g = self.game[id]
|
||||
empty = [i for i in range(9) if g["board"][i] == " "]
|
||||
return random.choice(empty) if empty else -1
|
||||
|
||||
def find_random_move(self, id: str, tea_price: float = 42.0) -> int:
|
||||
"""Find a random empty position, using time and tea_price for extra randomness."""
|
||||
board = self.game[id]["board"]
|
||||
empty = [i for i, cell in enumerate(board) if cell == " "]
|
||||
current_time = time.time()
|
||||
from_china = self.rndTeaPrice(time.time() % 7) # Correct usage
|
||||
tea_price = from_china
|
||||
tea_price = (42 * 7) - (13 / 2) + (tea_price % 5)
|
||||
if not empty:
|
||||
return -1
|
||||
# Combine time and tea_price for a seed
|
||||
seed = int(current_time * 1000) ^ int(tea_price * 1000)
|
||||
local_random = random.Random(seed)
|
||||
local_random.shuffle(empty)
|
||||
return empty[0]
|
||||
|
||||
def check_winner_on_board(self, board):
|
||||
"""Check winner on given board state"""
|
||||
@@ -156,7 +183,7 @@ class TicTacToe:
|
||||
if winner == X:
|
||||
g["won"] += 1
|
||||
return "🎉You won! (n)ew (e)nd"
|
||||
elif winner == X:
|
||||
elif winner == O:
|
||||
return "🤖Bot wins! (n)ew (e)nd"
|
||||
else:
|
||||
return "🤝Tie, The only winning move! (n)ew (e)nd"
|
||||
|
||||
@@ -6,8 +6,7 @@ import pickle
|
||||
from modules.log import *
|
||||
|
||||
vpStartingCash = 20
|
||||
vpTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0}]
|
||||
|
||||
from modules.settings import vpTracker
|
||||
# Define the Card class
|
||||
class CardVP:
|
||||
|
||||
@@ -304,7 +303,7 @@ def playVideoPoker(nodeID, message):
|
||||
# create new player if not in tracker
|
||||
logger.debug(f"System: VideoPoker: New Player {nodeID}")
|
||||
vpTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0})
|
||||
return f"Welcome to 🎰VideoPoker♥️ you have {vpStartingCash} coins, Whats your bet?"
|
||||
return f"You have {vpStartingCash} coins, \nWhats your bet?"
|
||||
|
||||
# Gather the player's bet
|
||||
if getLastCmdVp(nodeID) == "new" or getLastCmdVp(nodeID) == "gameOver":
|
||||
@@ -426,7 +425,7 @@ def playVideoPoker(nodeID, message):
|
||||
|
||||
if player.bankroll < 1:
|
||||
player.bankroll = vpStartingCash
|
||||
msg += "\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
|
||||
msg += f"\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
|
||||
elif player.bankroll > vpTracker[i]['highScore']:
|
||||
vpTracker[i]['highScore'] = player.bankroll
|
||||
msg += " 🎉HighScore!"
|
||||
|
||||
313
modules/games/wodt.py
Normal file
313
modules/games/wodt.py
Normal file
@@ -0,0 +1,313 @@
|
||||
# python word of the day game module for meshing-around bot
|
||||
# 2025 K7MHI Kelly Keeton
|
||||
from modules.log import *
|
||||
import random
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from itertools import product
|
||||
|
||||
class WordOfTheDayGame:
|
||||
def __init__(self):
|
||||
self.bingo_board_size = 3 # 3x3 bingo board good for small demos
|
||||
|
||||
default_word_list = [
|
||||
{'word': 'serendipity', 'meta': 'The occurrence of events by chance in a happy or beneficial way.'},
|
||||
{'word': 'ephemeral', 'meta': 'Lasting for a very short time.'},
|
||||
{'word': 'sonder', 'meta': 'The realization that each passerby has a life as vivid and complex as your own.'},
|
||||
{'word': 'petrichor', 'meta': 'A pleasant smell that frequently accompanies the first rain after a long period of warm, dry weather.'},
|
||||
]
|
||||
json_path = os.path.join('data', 'wotd.json')
|
||||
if os.path.exists(json_path):
|
||||
try:
|
||||
with open(json_path, 'r') as f:
|
||||
self.word_list = json.load(f)
|
||||
except FileNotFoundError:
|
||||
logger.debug("System: WoTd: Failed to load data/wotd.json, using default word list.")
|
||||
self.word_list = default_word_list
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("System: WoTd: JSON decode error in data/wotd.json, example format: [{\"word\": \"example\", \"definition\": \"An example definition.\"}]")
|
||||
self.word_list = default_word_list
|
||||
else:
|
||||
logger.debug("System: WoTd: data/wotd.json not found, using default word list.")
|
||||
self.word_list = default_word_list
|
||||
|
||||
# Load bingo card words from JSON if available
|
||||
default_bingo_card = [
|
||||
"dog", "cat", "fish", "bird", "hamster", "rabbit", "turtle", "lizard", "snake", "frog",
|
||||
"horse", "cow", "pig", "sheep", "goat", "chicken", "duck", "turkey", "peacock", "parrot",
|
||||
"elephant", "lion", "tiger", "bear", "wolf", "fox", "deer", "moose", "zebra", "giraffe",
|
||||
"monkey", "ape", "chimpanzee", "gorilla", "orangutan", "kangaroo", "koala", "panda",
|
||||
"whale", "dolphin", "shark", "octopus", "crab", "lobster", "jellyfish", "seahorse",
|
||||
"ant", "bee", "butterfly", "dragonfly", "spider", "ladybug"
|
||||
]
|
||||
bingo_json_path = os.path.join('data', 'bingo.json')
|
||||
if os.path.exists(bingo_json_path):
|
||||
try:
|
||||
with open(bingo_json_path, 'r') as f:
|
||||
bingoCard = json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
logger.debug("System: WoTd: Failed to load data/bingo.json, using default bingo card. example format: [\"word1\", \"word2\", ...]")
|
||||
bingoCard = default_bingo_card
|
||||
else:
|
||||
logger.debug("System: WoTd: data/bingo.json not found, using default bingo card.")
|
||||
bingoCard = default_bingo_card
|
||||
|
||||
# Create a set for faster lookup
|
||||
self.bingoCardSet = set(bingoCard)
|
||||
|
||||
self.leet_dict = {
|
||||
'a': ['4', '@'],
|
||||
'b': ['8'],
|
||||
'e': ['3'],
|
||||
'i': ['1', '!', '|'],
|
||||
'l': ['1', '|', '7'],
|
||||
'o': ['0'],
|
||||
's': ['5', '$'],
|
||||
't': ['7', '+'],
|
||||
'g': ['9', '6'],
|
||||
}
|
||||
# Initialize the word of the day
|
||||
self.word_of_the_day_entry = random.choice(self.word_list)
|
||||
logger.debug(f"System: WoTd: Initialized with word of the day '{self.word_of_the_day_entry['word']}'.")
|
||||
# Initialize bingo card
|
||||
self.generate_bingo_card(self.bingo_board_size)
|
||||
logger.debug("System: BINGO: " + ". ".join(" | ".join(row) for row in self.bingo_card))
|
||||
|
||||
def get_emoji_type(self, emoji, randomReturn=False):
|
||||
smileys = "😀😁😂🤣😃😄😅😆😉😊😋😎😍😘🥰😗😙😚🙂🤗🤩🤔🤨😐😑😶🙄😏😣😥😮🤐😯😪😫😴😌😛😜😝🤤😒😓😔😕🙃🤑😲☹️🙁😖😞😟😤😢😭😦😧😨😩🤯😬😰😱🥵🥶😳🤪😵😡😠🤬😷🤒🤕🤢🤮🤧😇🥳🥺🤠"
|
||||
animals = "🐶🐱🐭🐹🐰🦊🐻🐼🐨🐯🦁🐮🐷🐽🐸🐵🙈🙉🙊🐒🐔🐧🐦🐤🐣🐥🦆🦅🦉🦇🐺🐗🐴🦄🐝🐛🦋🐌🐞🐜🦟🦗🕷️🕸️🐢🐍🦎🦂🦀🦞🦐🦑🐙🦑🐠🐟🐡🐬🦈🐳🐋🐊🐅🐆🦓🦍🦧🐘🦛🦏🐪🐫🦒🦘🐃🐂🐄🐎🐖🐏🐑🦙🐐🦌🐕🐩🦮🐕🦺🐈🐓🦃🦚🦜🦢🦩🕊️🐇🦝🦨🦡🦦🦥🐁🐀🐿️🦔"
|
||||
fruit = "🍎🍊🍌🍉🍇🍓🍒🍑🥭🍍🥥🥝🍅🥑🍆🥔🥕🌽🌶️🥒🥬🥦🧄🧅🍄🥜🌰"
|
||||
categories = {'smileys': smileys, 'animals': animals, 'fruit': fruit}
|
||||
if randomReturn:
|
||||
cat = random.choice(list(categories))
|
||||
return random.choice(categories[cat])
|
||||
for cat, chars in categories.items():
|
||||
if emoji in chars:
|
||||
return cat
|
||||
return False
|
||||
|
||||
def reset_word_of_the_day(self):
|
||||
logger.debug("System: WoTd: Resetting Word of the Day.")
|
||||
self.word_of_the_day_entry = random.choice(self.word_list)
|
||||
|
||||
def generate_leet_variants(self, word):
|
||||
chars = []
|
||||
for c in word.lower():
|
||||
if c in self.leet_dict:
|
||||
chars.append([c] + self.leet_dict[c])
|
||||
else:
|
||||
chars.append([c])
|
||||
variants = set()
|
||||
for combo in product(*chars):
|
||||
variant = ''.join(combo)
|
||||
variants.add(variant)
|
||||
if len(variants) > 128:
|
||||
break
|
||||
return variants
|
||||
|
||||
def did_it_happen(self, string_of_text=''):
|
||||
"""
|
||||
Check if the current word of the day (or its leet variants) appears in the text.
|
||||
Also check for a bingo win.
|
||||
Returns:
|
||||
(wotd_found, old_entry, new_entry, bingo_win, bingo_message)
|
||||
"""
|
||||
text = string_of_text.lower()
|
||||
words_in_text = set(text.split())
|
||||
word = self.word_of_the_day_entry['word'].lower()
|
||||
variants = self.generate_leet_variants(word)
|
||||
wotd_found = False
|
||||
old_entry = None
|
||||
new_entry = None
|
||||
|
||||
for variant in variants:
|
||||
if variant in words_in_text:
|
||||
old_entry = self.word_of_the_day_entry
|
||||
self.reset_word_of_the_day()
|
||||
new_entry = self.word_of_the_day_entry
|
||||
wotd_found = True
|
||||
break
|
||||
|
||||
bingo_win, bingo_message = self.b_i_n_g_o(string_of_text)
|
||||
return wotd_found, old_entry, new_entry, bingo_win, bingo_message
|
||||
|
||||
def generate_bingo_card(self, size=None):
|
||||
"""
|
||||
Generate a random bingo card of given size (size x size) from the bingoCardSet.
|
||||
Returns a 2D list representing the bingo card.
|
||||
"""
|
||||
if size is None:
|
||||
size = self.bingo_board_size
|
||||
words = random.sample(list(self.bingoCardSet), size * size)
|
||||
card = [words[i*size:(i+1)*size] for i in range(size)]
|
||||
self.bingo_card = card
|
||||
self.bingo_card_matches = [[False]*size for _ in range(size)]
|
||||
return card
|
||||
|
||||
def b_i_n_g_o(self, string_of_text=''):
|
||||
"""
|
||||
Check if any words in the text match the bingo card.
|
||||
If a row, column, or diagonal is fully matched, return True and the winning line.
|
||||
Otherwise, return False and None.
|
||||
"""
|
||||
if not hasattr(self, 'bingo_card'):
|
||||
logger.debug("System: WoTd: Generating new bingo card.")
|
||||
self.generate_bingo_card(self.bingo_board_size)
|
||||
|
||||
words_in_text = set(string_of_text.lower().split())
|
||||
size = len(self.bingo_card)
|
||||
# Mark matches
|
||||
for i in range(size):
|
||||
for j in range(size):
|
||||
if self.bingo_card[i][j].lower() in words_in_text:
|
||||
self.bingo_card_matches[i][j] = True
|
||||
|
||||
# Check rows
|
||||
for i in range(size):
|
||||
if all(self.bingo_card_matches[i]):
|
||||
winning_row = self.bingo_card[i]
|
||||
logger.debug("System: BINGO achieved, generating new bingo card.")
|
||||
self.generate_bingo_card(size) # Reset board after win
|
||||
return True, f"BINGO! Row {i+1}: {winning_row}"
|
||||
|
||||
# Check columns
|
||||
for j in range(size):
|
||||
if all(self.bingo_card_matches[i][j] for i in range(size)):
|
||||
col = [self.bingo_card[i][j] for i in range(size)]
|
||||
logger.debug("System: BINGO achieved, generating new bingo card.")
|
||||
self.generate_bingo_card(size) # Reset board after win
|
||||
return True, f"BINGO! Column {j+1}: {col}"
|
||||
|
||||
# Check diagonals
|
||||
if all(self.bingo_card_matches[i][i] for i in range(size)):
|
||||
diag = [self.bingo_card[i][i] for i in range(size)]
|
||||
logger.debug("System: BINGO achieved, generating new bingo card.")
|
||||
self.generate_bingo_card(size) # Reset board after win
|
||||
return True, f"BINGO! Diagonal: {diag}"
|
||||
if all(self.bingo_card_matches[i][size-1-i] for i in range(size)):
|
||||
diag = [self.bingo_card[i][size-1-i] for i in range(size)]
|
||||
logger.debug("System: BINGO achieved, generating new bingo card.")
|
||||
self.generate_bingo_card(size) # Reset board after win
|
||||
return True, f"BINGO! Diagonal: {diag}"
|
||||
|
||||
return False, None
|
||||
|
||||
def extract_emojis(self, text):
|
||||
emojis = []
|
||||
for char in text:
|
||||
cp = ord(char)
|
||||
# Common emoji Unicode ranges
|
||||
if (
|
||||
0x1F600 <= cp <= 0x1F64F or # Emoticons
|
||||
0x1F300 <= cp <= 0x1F5FF or # Symbols & pictographs
|
||||
0x1F680 <= cp <= 0x1F6FF or # Transport & map symbols
|
||||
0x1F1E6 <= cp <= 0x1F1FF or # Regional indicator symbols
|
||||
0x2600 <= cp <= 0x26FF or # Misc symbols
|
||||
0x2700 <= cp <= 0x27BF or # Dingbats
|
||||
0x1F900 <= cp <= 0x1F9FF or # Supplemental symbols & pictographs
|
||||
0x1FA70 <= cp <= 0x1FAFF or # Symbols & pictographs extended-A
|
||||
0x2B50 == cp or # Star
|
||||
0x2B55 == cp # Heavy large circle
|
||||
):
|
||||
emojis.append(char)
|
||||
return emojis
|
||||
|
||||
def emojiMiniGame(self, string_of_text='', nodeID=0, nodeInt=1, emojiSeen=False):
|
||||
from modules.system import meshLeaderboard
|
||||
"""
|
||||
Track emoji usage, Returns a string if the mini-game is won, else None.
|
||||
If emojiSeen is False, only update mostMessages leaderboard and skip emoji logic.
|
||||
"""
|
||||
|
||||
# Only increment for text/chat messages
|
||||
meshLeaderboard['nodeMessageCounts'][nodeID] = meshLeaderboard['nodeMessageCounts'].get(nodeID, 0) + 1
|
||||
|
||||
# Update mostMessages leaderboard
|
||||
if meshLeaderboard['nodeMessageCounts']:
|
||||
max_node = max(meshLeaderboard['nodeMessageCounts'], key=meshLeaderboard['nodeMessageCounts'].get)
|
||||
meshLeaderboard['mostMessages'] = {
|
||||
'nodeID': max_node,
|
||||
'value': meshLeaderboard['nodeMessageCounts'][max_node],
|
||||
'timestamp': time.time()
|
||||
}
|
||||
|
||||
|
||||
emoji = None # Placeholder: extract emoji from string_of_text if needed
|
||||
emojis = self.extract_emojis(string_of_text)
|
||||
if not emojiSeen and not emojis:
|
||||
return None
|
||||
logger.debug(f"System: WoTd: Emoji mini-game processing for nodeID {nodeID} with emojis: {emojis}")
|
||||
# --- 1. Update meshLeaderboard for emoji usage ---
|
||||
if 'emojiCounts' not in meshLeaderboard:
|
||||
meshLeaderboard['emojiCounts'] = {}
|
||||
if 'emojiTypeCounts' not in meshLeaderboard:
|
||||
meshLeaderboard['emojiTypeCounts'] = {}
|
||||
|
||||
meshLeaderboard['emojiCounts'].setdefault(nodeID, {})
|
||||
for emoji in emojis:
|
||||
meshLeaderboard['emojiCounts'][nodeID][emoji] = meshLeaderboard['emojiCounts'][nodeID].get(emoji, 0) + 1
|
||||
|
||||
# --- Update the leaderboard record for most emojis ---
|
||||
# Flatten per-node emoji counts to total per node
|
||||
emoji_totals = {nid: sum(emojicounts.values()) for nid, emojicounts in meshLeaderboard['emojiCounts'].items() if isinstance(emojicounts, dict)}
|
||||
if emoji_totals:
|
||||
max_node = max(emoji_totals, key=emoji_totals.get)
|
||||
meshLeaderboard['mostEmojis'] = {
|
||||
'nodeID': max_node,
|
||||
'value': emoji_totals[max_node],
|
||||
'timestamp': time.time()
|
||||
}
|
||||
|
||||
# --- 2. Track most used of a type (e.g., smileys, animals, etc.) ---
|
||||
emoji_type = self.get_emoji_type(emoji)
|
||||
meshLeaderboard['emojiTypeCounts'].setdefault(emoji_type, {})
|
||||
meshLeaderboard['emojiTypeCounts'][emoji_type][emoji] = meshLeaderboard['emojiTypeCounts'][emoji_type].get(emoji, 0) + 1
|
||||
|
||||
# --- 3. Slot machine mini-game ---
|
||||
if 'emojiSlotWindow' not in meshLeaderboard:
|
||||
meshLeaderboard['emojiSlotWindow'] = []
|
||||
meshLeaderboard['emojiSlotWindow'].append(emoji)
|
||||
# Randomize jackpot length after each win
|
||||
if not hasattr(self, 'slot_jackpot_length'):
|
||||
self.slot_jackpot_length = random.choice([3,4,5]) # JackPot length can be 3, 4, or 5
|
||||
if len(meshLeaderboard['emojiSlotWindow']) > self.slot_jackpot_length:
|
||||
meshLeaderboard['emojiSlotWindow'].pop(0)
|
||||
|
||||
# --- 3a. Detect spam of 3 identical emojis in a row ---
|
||||
if len(meshLeaderboard['emojiSlotWindow']) >= 5:
|
||||
last_three = meshLeaderboard['emojiSlotWindow'][-3:]
|
||||
if len(set(last_three)) == 1:
|
||||
# Option: Randomly add an emoji to break the spam
|
||||
random_emoji = self.get_emoji_type('', randomReturn=True)
|
||||
meshLeaderboard['emojiSlotWindow'].append(random_emoji)
|
||||
logger.debug(f"System: WoTd: Detected emoji spam, added random emoji '{random_emoji}' to slot window.")
|
||||
# Optionally, you can still scramble or pop as well if you want
|
||||
random.shuffle(meshLeaderboard['emojiSlotWindow'])
|
||||
meshLeaderboard['emojiSlotWindow'].pop()
|
||||
|
||||
# # Debug: Show slot window status before jackpot check
|
||||
# logger.debug(
|
||||
# f"Emoji Slot Window: {meshLeaderboard['emojiSlotWindow']} | "
|
||||
# f"Jackpot Length: {self.slot_jackpot_length} | "
|
||||
# f"Unique: {set(meshLeaderboard['emojiSlotWindow'])} | "
|
||||
# f"Needed: {self.slot_jackpot_length - len(meshLeaderboard['emojiSlotWindow'])}"
|
||||
# )
|
||||
|
||||
# Jackpot: all emojis in window are the same
|
||||
if (
|
||||
len(meshLeaderboard['emojiSlotWindow']) == self.slot_jackpot_length and
|
||||
len(set(meshLeaderboard['emojiSlotWindow'])) == 1
|
||||
):
|
||||
winner_msg = f"🎰 JACKPOT! {emoji * self.slot_jackpot_length}"
|
||||
meshLeaderboard['emojiSlotWindow'] = []
|
||||
self.slot_jackpot_length = random.choice([3, 4, 5]) # Randomize jackpot length after win
|
||||
return winner_msg
|
||||
|
||||
return None
|
||||
|
||||
# Example usage:
|
||||
# theWordOfTheDay = WordOfTheDayGame()
|
||||
# happened, entry = theWordOfTheDay.did_it_happen("I love serendipity!")
|
||||
# if happened:
|
||||
# print(f"Found the word of the day: {entry['word']} - {entry['meta']}")
|
||||
183
modules/llm.py
183
modules/llm.py
@@ -8,6 +8,7 @@ from modules.log import *
|
||||
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
if not rawLLMQuery:
|
||||
# this may be removed in the future
|
||||
@@ -48,7 +49,7 @@ meshBotAI = """
|
||||
PROMPT
|
||||
{input}
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
if llmContext_fromGoogle:
|
||||
meshBotAI = meshBotAI + """
|
||||
@@ -76,6 +77,142 @@ if llmEnableHistory:
|
||||
|
||||
"""
|
||||
|
||||
# Tooling Functions Defined Here
|
||||
# Example: current_time function
|
||||
def llmTool_current_time():
|
||||
"""
|
||||
Example tool function to get the current time.
|
||||
:return: Current time string.
|
||||
"""
|
||||
return datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')
|
||||
|
||||
def llmTool_math_calculator(expression):
|
||||
"""
|
||||
Example tool function to perform basic math calculations.
|
||||
:param expression: A string containing a math expression (e.g., "2 + 2").
|
||||
:return: The result of the calculation as a string.
|
||||
"""
|
||||
try:
|
||||
# WARNING: Using eval can be dangerous if not controlled properly.
|
||||
# This is a simple example; in production, consider using a safe math parser.
|
||||
result = eval(expression, {"__builtins__": None}, {})
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
return f"Error in calculation: {e}"
|
||||
|
||||
def llmTool_get_google(query, num_results=3):
|
||||
"""
|
||||
Example tool function to perform a Google search and return results.
|
||||
:param query: The search query string.
|
||||
:param num_results: Number of search results to return.
|
||||
:return: A list of search result titles and descriptions.
|
||||
"""
|
||||
results = []
|
||||
try:
|
||||
googleSearch = search(query, advanced=True, num_results=num_results)
|
||||
for result in googleSearch:
|
||||
results.append(f"{result.title}: {result.description}")
|
||||
return results
|
||||
except Exception as e:
|
||||
return [f"Error in Google search: {e}"]
|
||||
|
||||
llmFunctions = [
|
||||
|
||||
{
|
||||
"name": "llmTool_current_time",
|
||||
"description": "Get the current time.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "llmTool_math_calculator",
|
||||
"description": "Perform basic math calculations.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "A math expression to evaluate, e.g., '2 + 2'."
|
||||
}
|
||||
},
|
||||
"required": ["expression"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "llmTool_get_google",
|
||||
"description": "Perform a Google search and return results.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query string."
|
||||
},
|
||||
"num_results": {
|
||||
"type": "integer",
|
||||
"description": "Number of search results to return.",
|
||||
"default": 3
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def get_google_context(input, num_results):
|
||||
# Get context from Google search results
|
||||
googleResults = []
|
||||
try:
|
||||
googleSearch = search(input, advanced=True, num_results=num_results)
|
||||
if googleSearch:
|
||||
for result in googleSearch:
|
||||
googleResults.append(f"{result.title} {result.description}")
|
||||
else:
|
||||
googleResults = ['no other context provided']
|
||||
except Exception as e:
|
||||
logger.debug(f"System: LLM Query: context gathering failed, likely due to network issues")
|
||||
googleResults = ['no other context provided']
|
||||
return googleResults
|
||||
|
||||
def send_ollama_query(llmQuery):
|
||||
# Send the query to the Ollama API and return the response
|
||||
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
|
||||
if result.status_code == 200:
|
||||
result_json = result.json()
|
||||
result = result_json.get("response", "")
|
||||
# deepseek has added <think> </think> tags to the response
|
||||
if "<think>" in result:
|
||||
result = result.split("</think>")[1]
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code}")
|
||||
return result
|
||||
|
||||
def send_ollama_tooling_query(prompt, functions, model=None, max_tokens=450):
|
||||
"""
|
||||
Send a prompt and function/tool definitions to Ollama API for function calling.
|
||||
:param prompt: The user prompt string.
|
||||
:param functions: List of function/tool definitions (see Ollama API docs).
|
||||
:param model: Model name (optional, defaults to llmModel).
|
||||
:param max_tokens: Max tokens for response.
|
||||
:return: Ollama API response JSON.
|
||||
"""
|
||||
if model is None:
|
||||
model = llmModel
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"functions": functions,
|
||||
"stream": False,
|
||||
"max_tokens": max_tokens
|
||||
}
|
||||
result = requests.post(ollamaAPI, data=json.dumps(payload))
|
||||
if result.status_code == 200:
|
||||
return result.json()
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code} - {result.text}")
|
||||
|
||||
def llm_query(input, nodeID=0, location_name=None):
|
||||
global antiFloodLLM, llmChat_history
|
||||
googleResults = []
|
||||
@@ -85,6 +222,10 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
if input == " " and rawLLMQuery:
|
||||
logger.warning("System: These LLM models lack a traditional system prompt, they can be verbose and not very helpful be advised.")
|
||||
input = meshbotAIinit
|
||||
else:
|
||||
input = input.strip()
|
||||
# classic model for gemma2, deepseek-r1, etc
|
||||
logger.debug(f"System: Using classic LLM model framework, ideally for gemma2, deepseek-r1, etc")
|
||||
|
||||
if not location_name:
|
||||
location_name = "no location provided "
|
||||
@@ -105,23 +246,7 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
antiFloodLLM.append(nodeID)
|
||||
|
||||
if llmContext_fromGoogle and not rawLLMQuery:
|
||||
# grab some context from the internet using google search hits (if available)
|
||||
# localization details at https://pypi.org/project/googlesearch-python/
|
||||
|
||||
# remove common words from the search query
|
||||
# commonWordsList = ["is", "for", "the", "of", "and", "in", "on", "at", "to", "with", "by", "from", "as", "a", "an", "that", "this", "these", "those", "there", "here", "where", "when", "why", "how", "what", "which", "who", "whom", "whose", "whom"]
|
||||
# sanitizedSearch = ' '.join([word for word in input.split() if word.lower() not in commonWordsList])
|
||||
try:
|
||||
googleSearch = search(input, advanced=True, num_results=googleSearchResults)
|
||||
if googleSearch:
|
||||
for result in googleSearch:
|
||||
# SearchResult object has url= title= description= just grab title and description
|
||||
googleResults.append(f"{result.title} {result.description}")
|
||||
else:
|
||||
googleResults = ['no other context provided']
|
||||
except Exception as e:
|
||||
logger.debug(f"System: LLM Query: context gathering failed, likely due to network issues")
|
||||
googleResults = ['no other context provided']
|
||||
googleResults = get_google_context(input, googleSearchResults)
|
||||
|
||||
history = llmChat_history.get(nodeID, ["", ""])
|
||||
|
||||
@@ -147,20 +272,11 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
|
||||
llmQuery = {"model": llmModel, "prompt": modelPrompt, "stream": False, "max_tokens": tokens}
|
||||
# Query the model via Ollama web API
|
||||
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
|
||||
# Condense the result to just needed
|
||||
if result.status_code == 200:
|
||||
result_json = result.json()
|
||||
result = result_json.get("response", "")
|
||||
|
||||
# deepseek-r1 has added <think> </think> tags to the response
|
||||
if "<think>" in result:
|
||||
result = result.split("</think>")[1]
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code}")
|
||||
result = send_ollama_query(llmQuery)
|
||||
|
||||
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
|
||||
except Exception as e:
|
||||
antiFloodLLM.remove(nodeID) # Ensure removal on error
|
||||
logger.warning(f"System: LLM failure: {e}")
|
||||
return "⛔️I am having trouble processing your request, please try again later."
|
||||
|
||||
@@ -171,15 +287,8 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
#retryy loop to truncate the response
|
||||
logger.warning(f"System: LLM Query: Response exceeded {tokens} characters, requesting truncation")
|
||||
truncateQuery = {"model": llmModel, "prompt": truncatePrompt + response, "stream": False, "max_tokens": tokens}
|
||||
truncateResult = requests.post(ollamaAPI, data=json.dumps(truncateQuery))
|
||||
if truncateResult.status_code == 200:
|
||||
truncate_json = truncateResult.json()
|
||||
result = truncate_json.get("response", "")
|
||||
truncateResult = send_ollama_query(truncateQuery)
|
||||
|
||||
else:
|
||||
#use the original result if truncation fails
|
||||
logger.warning("System: LLM Query: Truncation failed, using original response")
|
||||
|
||||
# cleanup for message output
|
||||
response = result.strip().replace('\n', ' ')
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import maidenhead as mh # pip install maidenhead
|
||||
import requests # pip install requests
|
||||
import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from datetime import datetime
|
||||
from modules.log import *
|
||||
import math
|
||||
|
||||
@@ -16,6 +17,7 @@ trap_list_location = ("whereami", "wx", "wxa", "wxalert", "rlist", "ea", "ealert
|
||||
def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
whereIam = ""
|
||||
grid = mh.to_maiden(float(lat), float(lon))
|
||||
location = lat, lon
|
||||
|
||||
if int(float(lat)) == 0 and int(float(lon)) == 0:
|
||||
logger.error("Location: No GPS data, try sending location")
|
||||
@@ -171,9 +173,10 @@ def getArtSciRepeaters(lat=0, lon=0):
|
||||
|
||||
def get_NOAAtide(lat=0, lon=0):
|
||||
station_id = ""
|
||||
location = lat,lon
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.error("Location:No GPS data, try sending location for tide")
|
||||
return NO_DATA_NOGPS
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
station_lookup_url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/tidepredstations.json?lat=" + str(lat) + "&lon=" + str(lon) + "&radius=50"
|
||||
try:
|
||||
station_data = requests.get(station_lookup_url, timeout=urlTimeoutSeconds)
|
||||
@@ -235,8 +238,10 @@ def get_NOAAtide(lat=0, lon=0):
|
||||
def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
# get weather report from NOAA for forecast detailed
|
||||
weather = ""
|
||||
location = lat,lon
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
return NO_DATA_NOGPS
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
|
||||
# get weather data from NOAA units for metric unit = 1 is metric
|
||||
if use_metric:
|
||||
@@ -292,9 +297,37 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
|
||||
return weather
|
||||
|
||||
def abbreviate_noaa(row):
|
||||
# replace long strings with shorter ones for display
|
||||
replacements = {
|
||||
def case_insensitive_replace(text, old, new):
|
||||
"""Replace all occurrences of old (any case) in text with new."""
|
||||
idx = 0
|
||||
old_lower = old.lower()
|
||||
text_lower = text.lower()
|
||||
while True:
|
||||
idx = text_lower.find(old_lower, idx)
|
||||
if idx == -1:
|
||||
break
|
||||
text = text[:idx] + new + text[idx+len(old):]
|
||||
text_lower = text.lower()
|
||||
idx += len(new)
|
||||
return text
|
||||
|
||||
def abbreviate_noaa(data=""):
|
||||
# Long phrases (with spaces)
|
||||
phrase_replacements = {
|
||||
"less than a tenth of an inch possible": "< 0.1in",
|
||||
"between a tenth and quarter of an inch possible": "0.1-0.25in",
|
||||
"between a quarter and half an inch possible": "0.25-0.5in",
|
||||
"between a half and three quarters of an inch possible": "0.5-0.75in",
|
||||
"between one and two inches possible": "1-2in",
|
||||
"between two and three inches possible": "2-3in",
|
||||
"between three and four inches possible": "3-4in",
|
||||
"between four and five inches possible": "4-5in",
|
||||
"between five and six inches possible": "5-6in",
|
||||
"between six and eight inches possible": "6-8in",
|
||||
"gusts as high as": "gusts to",
|
||||
}
|
||||
# Single words (no spaces)
|
||||
word_replacements = {
|
||||
"monday": "Mon",
|
||||
"tuesday": "Tue",
|
||||
"wednesday": "Wed",
|
||||
@@ -310,6 +343,8 @@ def abbreviate_noaa(row):
|
||||
"south": "S",
|
||||
"east": "E",
|
||||
"west": "W",
|
||||
"accumulation": "accum",
|
||||
"visibility": "vis",
|
||||
"precipitation": "precip",
|
||||
"showers": "shwrs",
|
||||
"thunderstorms": "t-storms",
|
||||
@@ -331,27 +366,37 @@ def abbreviate_noaa(row):
|
||||
"degrees": "°",
|
||||
"percent": "%",
|
||||
"department": "Dept.",
|
||||
"amounts less than a tenth of an inch possible.": "< 0.1in",
|
||||
"temperatures": "temps.",
|
||||
"temperature": "temp.",
|
||||
"temperatures": "temps:",
|
||||
"temperature": "temp:",
|
||||
"amounts": "amts:",
|
||||
"afternoon": "Aftn",
|
||||
"around": "~",
|
||||
"evening": "Eve",
|
||||
}
|
||||
|
||||
line = row
|
||||
for key, value in replacements.items():
|
||||
# case insensitive replace
|
||||
line = line.replace(key, value).replace(key.capitalize(), value).replace(key.upper(), value)
|
||||
|
||||
return line
|
||||
text = data
|
||||
|
||||
# Replace long phrases (case-insensitive)
|
||||
for key in sorted(phrase_replacements, key=len, reverse=True):
|
||||
value = phrase_replacements[key]
|
||||
text = case_insensitive_replace(text, key, value)
|
||||
|
||||
# Replace single words (case-insensitive)
|
||||
for key in word_replacements:
|
||||
value = word_replacements[key]
|
||||
text = case_insensitive_replace(text, key, value)
|
||||
|
||||
return text
|
||||
|
||||
def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
|
||||
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
|
||||
alerts = ""
|
||||
location = lat,lon
|
||||
if useDefaultLatLon:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
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"
|
||||
@@ -422,9 +467,10 @@ def alertBrodcastNOAA():
|
||||
def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
|
||||
# get the latest details of weather alerts from NOAA
|
||||
alerts = ""
|
||||
location = lat,lon
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.warning("Location:No GPS data, try sending location for weather alerts")
|
||||
return NO_DATA_NOGPS
|
||||
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"
|
||||
@@ -484,10 +530,10 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
try:
|
||||
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
|
||||
if not alert_data.ok:
|
||||
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
|
||||
logger.warning(f"System: iPAWS fetching IPAWS alerts from FEMA (HTTP {alert_data.status_code})")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
|
||||
except Exception as e:
|
||||
logger.warning(f"System: iPAWS fetching IPAWS alerts from FEMA failed: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# main feed bulletins
|
||||
@@ -569,14 +615,13 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
|
||||
# check if the alert is for the SAME location, if wanted keep alert
|
||||
if (sameVal in mySAMEList) or (geocode_value in mySAMEList) or mySAMEList == ['']:
|
||||
# ignore the FEMA test alerts
|
||||
ignore_alert = False
|
||||
if ignoreFEMAenable:
|
||||
ignore_alert = False
|
||||
for word in ignoreFEMAwords:
|
||||
if word.lower() in headline.lower():
|
||||
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing {word} at {areaDesc}")
|
||||
ignore_alert = True
|
||||
break
|
||||
ignore_alert = any(
|
||||
word.lower() in headline.lower()
|
||||
for word in ignoreFEMAwords)
|
||||
if ignore_alert:
|
||||
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing one of {ignoreFEMAwords} at {areaDesc}")
|
||||
if ignore_alert:
|
||||
continue
|
||||
|
||||
@@ -701,7 +746,7 @@ def get_volcano_usgs(lat=0, lon=0):
|
||||
return alerts
|
||||
|
||||
def get_nws_marine(zone, days=3):
|
||||
# forcast from NWS coastal products
|
||||
# forecast from NWS coastal products
|
||||
try:
|
||||
marine_pz_data = requests.get(zone, timeout=urlTimeoutSeconds)
|
||||
if not marine_pz_data.ok:
|
||||
@@ -710,18 +755,21 @@ def get_nws_marine(zone, days=3):
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching NWS Marine PZ data")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
|
||||
marine_pz_data = marine_pz_data.text
|
||||
#validate data
|
||||
todayDate = today.strftime("%Y%m%d")
|
||||
if marine_pz_data.startswith("Expires:"):
|
||||
expires = marine_pz_data.split(";;")[0].split(":")[1]
|
||||
expires_date = expires[:8]
|
||||
if expires_date < todayDate:
|
||||
logger.debug("Location: NWS Marine PZ data expired")
|
||||
todayDate = datetime.now().strftime("%Y%m%d")
|
||||
if marine_pz_data and marine_pz_data.startswith("Expires:"):
|
||||
try:
|
||||
expires = marine_pz_data.split(";;")[0].split(":")[1]
|
||||
expires_date = expires[:8]
|
||||
if expires_date < todayDate:
|
||||
logger.debug("Location: NWS Marine PZ data expired")
|
||||
return ERROR_FETCHING_DATA
|
||||
except Exception as e:
|
||||
logger.debug(f"Location: NWS Marine PZ data parse error: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
else:
|
||||
logger.debug("Location: NWS Marine PZ data not valid")
|
||||
logger.debug("Location: NWS Marine PZ data not valid or empty")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# process the marine forecast data
|
||||
@@ -813,6 +861,7 @@ def distance(lat=0,lon=0,nodeID=0, reset=False):
|
||||
# part of the howfar function, calculates the distance between two lat/lon points
|
||||
msg = ""
|
||||
dupe = False
|
||||
location = lat,lon
|
||||
r = 6371 # Radius of earth in kilometers # haversine formula
|
||||
|
||||
if lat == 0 and lon == 0:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from modules.settings import *
|
||||
# if LOGGING_LEVEL is not set in settings.py, default to DEBUG
|
||||
if not LOGGING_LEVEL:
|
||||
@@ -38,11 +36,17 @@ class CustomFormatter(logging.Formatter):
|
||||
return formatter.format(record)
|
||||
|
||||
class plainFormatter(logging.Formatter):
|
||||
ansi_escape = re.compile(r'\x1b\[([0-9]+)(;[0-9]+)*m')
|
||||
ansi_codes = [
|
||||
'\x1b[38;21m', '\x1b[38;5;231m', '\x1b[38;5;39m', '\x1b[38;5;226m',
|
||||
'\x1b[38;5;196m', '\x1b[38;5;46m', '\x1b[38;5;129m', '\x1b[31;1m',
|
||||
'\x1b[37;1m', '\x1b[0m'
|
||||
]
|
||||
|
||||
def format(self, record):
|
||||
message = super().format(record)
|
||||
return self.ansi_escape.sub('', message)
|
||||
for code in self.ansi_codes:
|
||||
message = message.replace(code, '')
|
||||
return message
|
||||
|
||||
# Create logger
|
||||
logger = logging.getLogger("MeshBot System Logger")
|
||||
@@ -56,7 +60,6 @@ msgLogger.propagate = False
|
||||
# Define format for logs
|
||||
logFormat = '%(asctime)s | %(levelname)8s | %(message)s'
|
||||
msgLogFormat = '%(asctime)s | %(message)s'
|
||||
today = datetime.now()
|
||||
|
||||
# Create stdout handler for logging to the console
|
||||
stdout_handler = logging.StreamHandler()
|
||||
|
||||
260
modules/radio.py
260
modules/radio.py
@@ -2,23 +2,46 @@
|
||||
# detect signal strength and frequency of active channel if appears to be in use send to mesh network
|
||||
# depends on rigctld running externally as a network service
|
||||
# also can use VOX detection with a microphone and vosk speech to text to send voice messages to mesh network
|
||||
# requires vosk and sounddevice python modules
|
||||
# requires vosk and sounddevice python modules. will auto download needed. more from https://alphacephei.com/vosk/models and unpack
|
||||
# 2024 Kelly Keeton K7MHI
|
||||
|
||||
previousVoxState = False
|
||||
from modules.log import *
|
||||
import asyncio
|
||||
|
||||
# verbose debug logging for trap words function
|
||||
debugVoxTmsg = False
|
||||
|
||||
|
||||
if radio_detection_enabled:
|
||||
# used by hamlib detection
|
||||
import socket
|
||||
|
||||
|
||||
if voxDetectionEnabled:
|
||||
# methods available for trap word processing, these can be called by VOX detection when trap words are detected
|
||||
from mesh_bot import tell_joke, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
|
||||
botMethods = {
|
||||
"joke": tell_joke,
|
||||
"weather": handle_wxc,
|
||||
"moon": handle_moon,
|
||||
"daylight": handle_sun,
|
||||
"river": handle_riverFlow,
|
||||
"tide": handle_tide,
|
||||
"satellite": handle_satpass}
|
||||
# module global variables
|
||||
previousVoxState = False
|
||||
voxHoldTime = signalHoldTime
|
||||
|
||||
try:
|
||||
import sounddevice as sd # pip install sounddevice sudo apt install portaudio19-dev
|
||||
from vosk import Model, KaldiRecognizer # pip install vosk
|
||||
import json
|
||||
q = asyncio.Queue()
|
||||
q = asyncio.Queue(maxsize=32) # queue for audio data
|
||||
|
||||
if useLocalVoxModel:
|
||||
voxModel = Model(lang=localVoxModelPath) # use built in model for specified language
|
||||
else:
|
||||
voxModel = Model(lang=voxLanguage) # use built in model for specified language
|
||||
|
||||
except Exception as e:
|
||||
print(f"RadioMon: Error importing VOX dependencies: {e}")
|
||||
print(f"To use VOX detection please install the vosk and sounddevice python modules")
|
||||
@@ -27,8 +50,56 @@ if voxDetectionEnabled:
|
||||
voxDetectionEnabled = False
|
||||
logger.error(f"RadioMon: VOX detection disabled due to import error")
|
||||
|
||||
|
||||
FREQ_NAME_MAP = {
|
||||
462562500: "GRMS CH1",
|
||||
462587500: "GRMS CH2",
|
||||
462612500: "GRMS CH3",
|
||||
462637500: "GRMS CH4",
|
||||
462662500: "GRMS CH5",
|
||||
462687500: "GRMS CH6",
|
||||
462712500: "GRMS CH7",
|
||||
467562500: "GRMS CH8",
|
||||
467587500: "GRMS CH9",
|
||||
467612500: "GRMS CH10",
|
||||
467637500: "GRMS CH11",
|
||||
467662500: "GRMS CH12",
|
||||
467687500: "GRMS CH13",
|
||||
467712500: "GRMS CH14",
|
||||
467737500: "GRMS CH15",
|
||||
462550000: "GRMS CH16",
|
||||
462575000: "GMRS CH17",
|
||||
462600000: "GMRS CH18",
|
||||
462625000: "GMRS CH19",
|
||||
462675000: "GMRS CH20",
|
||||
462670000: "GMRS CH21",
|
||||
462725000: "GMRS CH22",
|
||||
462725500: "GMRS CH23",
|
||||
467575000: "GMRS CH24",
|
||||
467600000: "GMRS CH25",
|
||||
467625000: "GMRS CH26",
|
||||
467650000: "GMRS CH27",
|
||||
467675000: "GMRS CH28",
|
||||
467700000: "FRS CH1",
|
||||
462650000: "FRS CH5",
|
||||
462700000: "FRS CH7",
|
||||
462737500: "FRS CH16",
|
||||
146520000: "2M Simplex Calling",
|
||||
446000000: "70cm Simplex Calling",
|
||||
156800000: "Marine CH16",
|
||||
# Add more as needed
|
||||
}
|
||||
|
||||
def get_freq_common_name(freq):
|
||||
freq = int(freq)
|
||||
name = FREQ_NAME_MAP.get(freq)
|
||||
if name:
|
||||
return name
|
||||
else:
|
||||
# Return MHz if not found
|
||||
return f"{freq/1000000} Mhz"
|
||||
|
||||
def get_hamlib(msg="f"):
|
||||
# get data from rigctld server
|
||||
try:
|
||||
rigControlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
rigControlSocket.settimeout(2)
|
||||
@@ -50,115 +121,46 @@ def get_hamlib(msg="f"):
|
||||
except Exception as e:
|
||||
logger.error(f"RadioMon: Error fetching data from rigctld: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
def get_freq_common_name(freq):
|
||||
freq = int(freq)
|
||||
if freq == 462562500:
|
||||
return "GRMS CH1"
|
||||
elif freq == 462587500:
|
||||
return "GRMS CH2"
|
||||
elif freq == 462612500:
|
||||
return "GRMS CH3"
|
||||
elif freq == 462637500:
|
||||
return "GRMS CH4"
|
||||
elif freq == 462662500:
|
||||
return "GRMS CH5"
|
||||
elif freq == 462687500:
|
||||
return "GRMS CH6"
|
||||
elif freq == 462712500:
|
||||
return "GRMS CH7"
|
||||
elif freq == 467562500:
|
||||
return "GRMS CH8"
|
||||
elif freq == 467587500:
|
||||
return "GRMS CH9"
|
||||
elif freq == 467612500:
|
||||
return "GRMS CH10"
|
||||
elif freq == 467637500:
|
||||
return "GRMS CH11"
|
||||
elif freq == 467662500:
|
||||
return "GRMS CH12"
|
||||
elif freq == 467687500:
|
||||
return "GRMS CH13"
|
||||
elif freq == 467712500:
|
||||
return "GRMS CH14"
|
||||
elif freq == 467737500:
|
||||
return "GRMS CH15"
|
||||
elif freq == 462550000:
|
||||
return "GRMS CH16"
|
||||
elif freq == 462575000:
|
||||
return "GMRS CH17"
|
||||
elif freq == 462600000:
|
||||
return "GMRS CH18"
|
||||
elif freq == 462625000:
|
||||
return "GMRS CH19"
|
||||
elif freq == 462675000:
|
||||
return "GMRS CH20"
|
||||
elif freq == 462670000:
|
||||
return "GMRS CH21"
|
||||
elif freq == 462725000:
|
||||
return "GMRS CH22"
|
||||
elif freq == 462725500:
|
||||
return "GMRS CH23"
|
||||
elif freq == 467575000:
|
||||
return "GMRS CH24"
|
||||
elif freq == 467600000:
|
||||
return "GMRS CH25"
|
||||
elif freq == 467625000:
|
||||
return "GMRS CH26"
|
||||
elif freq == 467650000:
|
||||
return "GMRS CH27"
|
||||
elif freq == 467675000:
|
||||
return "GMRS CH28"
|
||||
elif freq == 467700000:
|
||||
return "FRS CH1"
|
||||
elif freq == 462575000:
|
||||
return "FRS CH2"
|
||||
elif freq == 462600000:
|
||||
return "FRS CH3"
|
||||
elif freq == 462650000:
|
||||
return "FRS CH5"
|
||||
elif freq == 462675000:
|
||||
return "FRS CH6"
|
||||
elif freq == 462700000:
|
||||
return "FRS CH7"
|
||||
elif freq == 462725000:
|
||||
return "FRS CH8"
|
||||
elif freq == 462562500:
|
||||
return "FRS CH9"
|
||||
elif freq == 462587500:
|
||||
return "FRS CH10"
|
||||
elif freq == 462612500:
|
||||
return "FRS CH11"
|
||||
elif freq == 462637500:
|
||||
return "FRS CH12"
|
||||
elif freq == 462662500:
|
||||
return "FRS CH13"
|
||||
elif freq == 462687500:
|
||||
return "FRS CH14"
|
||||
elif freq == 462712500:
|
||||
return "FRS CH15"
|
||||
elif freq == 462737500:
|
||||
return "FRS CH16"
|
||||
elif freq == 146520000:
|
||||
return "2M Simplex Calling"
|
||||
elif freq == 446000000:
|
||||
return "70cm Simplex Calling"
|
||||
elif freq == 156800000:
|
||||
return "Marine CH16"
|
||||
else:
|
||||
#return Mhz
|
||||
freq = freq/1000000
|
||||
return f"{freq} Mhz"
|
||||
|
||||
def get_sig_strength():
|
||||
strength = get_hamlib('l STRENGTH')
|
||||
return strength
|
||||
|
||||
def vox_callback(indata, frames, time, status):
|
||||
if status:
|
||||
logger.warning(f"RadioMon: VOX input status: {status}")
|
||||
q.put(bytes(indata))
|
||||
|
||||
def checkVoxTrapWords(text):
|
||||
try:
|
||||
if not voxOnTrapList:
|
||||
logger.debug(f"RadioMon: VOX detected: {text}")
|
||||
return text
|
||||
if text:
|
||||
traps = [voxTrapList] if isinstance(voxTrapList, str) else voxTrapList
|
||||
text_lower = text.lower()
|
||||
for trap in traps:
|
||||
trap_clean = trap.strip()
|
||||
trap_lower = trap_clean.lower()
|
||||
idx = text_lower.find(trap_lower)
|
||||
if debugVoxTmsg:
|
||||
logger.debug(f"RadioMon: VOX checking for trap word '{trap_lower}' in: '{text}' (index: {idx})")
|
||||
if idx != -1:
|
||||
new_text = text[idx + len(trap_clean):].strip()
|
||||
if debugVoxTmsg:
|
||||
logger.debug(f"RadioMon: VOX detected trap word '{trap_lower}' in: '{text}' (remaining: '{new_text}')")
|
||||
new_words = new_text.split()
|
||||
if voxEnableCmd:
|
||||
for word in new_words:
|
||||
if word in botMethods:
|
||||
logger.info(f"RadioMon: VOX action '{word}' with '{new_text}'")
|
||||
if word == "joke":
|
||||
return botMethods[word](vox=True)
|
||||
else:
|
||||
return botMethods[word](None, None, None, vox=True)
|
||||
logger.debug(f"RadioMon: VOX returning text after trap word '{trap_lower}': '{new_text}'")
|
||||
return new_text
|
||||
if debugVoxTmsg:
|
||||
logger.debug(f"RadioMon: VOX no trap word found in: '{text}'")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.debug(f"RadioMon: Error in checkVoxTrapWords: {e}")
|
||||
return None
|
||||
|
||||
async def signalWatcher():
|
||||
global previousStrength
|
||||
@@ -184,31 +186,42 @@ async def signalWatcher():
|
||||
signalCycle = 0
|
||||
previousStrength = -40
|
||||
|
||||
def make_vox_callback(loop, q):
|
||||
async def make_vox_callback(loop, q):
|
||||
def vox_callback(indata, frames, time, status):
|
||||
if status:
|
||||
logger.warning(f"RadioMon: VOX input status: {status}")
|
||||
try:
|
||||
loop.call_soon_threadsafe(q.put_nowait, bytes(indata))
|
||||
except asyncio.QueueFull:
|
||||
# Drop the oldest item and add the new one
|
||||
try:
|
||||
q.get_nowait() # Remove oldest
|
||||
except asyncio.QueueEmpty:
|
||||
pass
|
||||
try:
|
||||
loop.call_soon_threadsafe(q.put_nowait, bytes(indata))
|
||||
except asyncio.QueueFull:
|
||||
# If still full, just drop this frame
|
||||
logger.debug("RadioMon: VOX queue full, dropping audio frame")
|
||||
except RuntimeError:
|
||||
# Loop may be closed
|
||||
pass
|
||||
return vox_callback
|
||||
|
||||
voxInputDevice = None
|
||||
|
||||
async def voxMonitor():
|
||||
global previousVoxState, voxMsgQueue
|
||||
try:
|
||||
model = Model(lang="en-us")
|
||||
model = voxModel
|
||||
device_info = sd.query_devices(voxInputDevice, 'input')
|
||||
samplerate = 16000
|
||||
logger.debug(f"RadioMon: VOX monitor started on device {device_info['name']} with samplerate {samplerate}")
|
||||
logger.debug(f"RadioMon: VOX monitor started on device {device_info['name']} with samplerate {samplerate} using trap words: {voxTrapList if voxOnTrapList else 'none'}")
|
||||
rec = KaldiRecognizer(model, samplerate)
|
||||
loop = asyncio.get_running_loop()
|
||||
callback = make_vox_callback(loop, q)
|
||||
callback = await make_vox_callback(loop, q)
|
||||
with sd.RawInputStream(
|
||||
device=voxInputDevice,
|
||||
samplerate=samplerate,
|
||||
blocksize=8000,
|
||||
blocksize=4000,
|
||||
dtype='int16',
|
||||
channels=1,
|
||||
callback=callback
|
||||
@@ -218,11 +231,16 @@ async def voxMonitor():
|
||||
if rec.AcceptWaveform(data):
|
||||
result = rec.Result()
|
||||
text = json.loads(result).get("text", "")
|
||||
if text and text != "huh":
|
||||
logger.info(f"🎙️Detected {voxDescription}: {text}")
|
||||
voxMsgQueue.append(f"🎙️Detected {voxDescription}: {text}")
|
||||
await asyncio.sleep(0.5)
|
||||
# process text
|
||||
if text and text != 'huh':
|
||||
result = checkVoxTrapWords(text)
|
||||
if result:
|
||||
# If result is a function return, handle it (send to mesh, log, etc.)
|
||||
# If it's just text, handle as a normal message
|
||||
voxMsgQueue.append(result)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"RadioMon: Error in VOX monitor: {e}")
|
||||
|
||||
# end of file
|
||||
# end of file
|
||||
|
||||
79
modules/scheduler.py
Normal file
79
modules/scheduler.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# modules/scheduler.py 2025 meshing-around
|
||||
# Scheduler setup for Mesh Bot
|
||||
import asyncio
|
||||
import schedule
|
||||
from modules.log import logger
|
||||
from modules.system import send_message
|
||||
|
||||
async def setup_scheduler(
|
||||
schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
|
||||
schedulerValue, schedulerTime, schedulerInterval, logger, BroadcastScheduler):
|
||||
|
||||
# methods available for custom scheduler messages
|
||||
from mesh_bot import tell_joke, welcome_message, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
|
||||
schedulerValue = schedulerValue.lower().strip()
|
||||
schedulerTime = schedulerTime.strip()
|
||||
schedulerInterval = schedulerInterval.strip()
|
||||
schedulerChannel = int(schedulerChannel)
|
||||
schedulerInterface = int(schedulerInterface)
|
||||
# Setup the scheduler based on configuration
|
||||
try:
|
||||
if schedulerMotd:
|
||||
scheduler_message = MOTD
|
||||
else:
|
||||
scheduler_message = schedulerMessage
|
||||
|
||||
# Basic Scheduler Options
|
||||
basicOptions = ['day', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'hour', 'min']
|
||||
if any(option.lower() in schedulerValue.lower() for option in basicOptions):
|
||||
# Basic scheduler job to run the schedule see examples below for custom schedules
|
||||
if schedulerValue.lower() == 'day':
|
||||
if schedulerTime != '':
|
||||
schedule.every().day.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
else:
|
||||
schedule.every(int(schedulerInterval)).days.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'mon' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().monday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'tue' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().tuesday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'wed' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().wednesday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'thu' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().thursday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'fri' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().friday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sat' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().saturday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sun' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().sunday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'hour' in schedulerValue.lower():
|
||||
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'min' in schedulerValue.lower():
|
||||
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {schedulerInterval} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'joke' in schedulerValue.lower():
|
||||
# Schedule to send a joke every specified interval
|
||||
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the joke scheduler to send a joke every {schedulerInterval} minutes on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'weather' in schedulerValue.lower():
|
||||
# Schedule to send weather updates every specified interval
|
||||
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(handle_wxc(0, schedulerInterface, 'wx'), schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the weather scheduler to send weather updates every {schedulerInterval} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'custom' in schedulerValue.lower():
|
||||
# Import and setup custom schedules from custom_scheduler.py
|
||||
try:
|
||||
# This file is located in etc/custom_scheduler.py and copied to modules/custom_scheduler.py at install
|
||||
from modules.custom_scheduler import setup_custom_schedules # type: ignore # pylance
|
||||
setup_custom_schedules(
|
||||
send_message, tell_joke, welcome_message, handle_wxc, MOTD,
|
||||
schedulerChannel, schedulerInterface)
|
||||
logger.debug("System: Custom scheduler file imported and custom schedules set up.")
|
||||
except Exception as e:
|
||||
logger.debug(f"System: Failed to import custom scheduler. {e}")
|
||||
logger.warning("Custom scheduler file not found or failed to import. cp etc/custom_scheduler.py modules/custom_scheduler.py")
|
||||
|
||||
# Start the Broadcast Scheduler
|
||||
await BroadcastScheduler()
|
||||
except Exception as e:
|
||||
logger.error(f"System: Scheduler Error {e}")
|
||||
|
||||
@@ -28,11 +28,23 @@ wiki_return_limit = 3 # limit the number of sentences returned off the first par
|
||||
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
|
||||
cmdHistory = [] # list to hold the last commands
|
||||
seenNodes = [] # list to hold the last seen nodes
|
||||
surveyTracker, tictactoeTracker, hamtestTracker, hangmanTracker, golfTracker, mastermindTracker, vpTracker, blackjackTracker, lemonadeTracker, dwPlayerTracker, jackTracker = [], [], [], [], [], [], [], [], [], [], [] # game trackers
|
||||
cmdHistory = [] # list to hold the command history for lheard and history commands
|
||||
msg_history = [] # list to hold the message history for the messages command
|
||||
max_bytes = 200 # Meshtastic has ~237 byte limit, use conservative 200 bytes for message content
|
||||
voxMsgQueue = [] # queue for VOX detected messages
|
||||
# Game trackers
|
||||
surveyTracker = [] # Survey game tracker
|
||||
tictactoeTracker = [] # TicTacToe game tracker
|
||||
hamtestTracker = [] # Ham radio test tracker
|
||||
hangmanTracker = [] # Hangman game tracker
|
||||
golfTracker = [] # GolfSim game tracker
|
||||
mastermindTracker = [] # Mastermind game tracker
|
||||
vpTracker = [] # Video Poker game tracker
|
||||
jackTracker = [] # Blackjack game tracker
|
||||
lemonadeTracker = [] # Lemonade Stand game tracker
|
||||
dwPlayerTracker = [] # DopeWars player tracker
|
||||
jackTracker = [] # Jack game tracker
|
||||
mindTracker = [] # Mastermind (mmind) game tracker
|
||||
|
||||
# Read the config file, if it does not exist, create basic config file
|
||||
config = configparser.ConfigParser()
|
||||
@@ -259,6 +271,7 @@ try:
|
||||
secure_interface = config['sentry'].getint('SentryInterface', 1) # default 1
|
||||
sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9
|
||||
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
|
||||
sentryWatchList = config['sentry'].get('sentryWatchList', '').split(',')
|
||||
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
|
||||
email_sentry_alerts = config['sentry'].getboolean('emailSentryAlerts', False) # default False
|
||||
highfly_enabled = config['sentry'].getboolean('highFlyingAlert', True) # default True
|
||||
@@ -268,11 +281,17 @@ try:
|
||||
highfly_ignoreList = config['sentry'].get('highFlyingIgnoreList', '').split(',') # default empty
|
||||
highfly_check_openskynetwork = config['sentry'].getboolean('highflyOpenskynetwork', True) # default True check with OpenSkyNetwork if highfly detected
|
||||
detctionSensorAlert = config['sentry'].getboolean('detectionSensorAlert', False) # default False
|
||||
reqLocationEnabled = config['sentry'].getboolean('reqLocationEnabled', False) # default False
|
||||
cmdShellSentryAlerts = config['sentry'].getboolean('cmdShellSentryAlerts', False) # default False
|
||||
sentryAlertNear = config['sentry'].get('sentryAlertNear', 'sentry_alert_near.sh') # default sentry_alert_near.sh
|
||||
sentryAlertFar = config['sentry'].get('sentryAlertFar', 'sentry_alert_far.sh') # default sentry_alert_far.sh
|
||||
|
||||
# location
|
||||
location_enabled = config['location'].getboolean('enabled', True)
|
||||
latitudeValue = config['location'].getfloat('lat', 48.50)
|
||||
longitudeValue = config['location'].getfloat('lon', -123.0)
|
||||
fuzz_config_location = config['location'].getboolean('fuzzConfigLocation', True) # default True
|
||||
fuzzItAll = config['location'].getboolean('fuzzAllLocations', False) # default False, only fuzz config location
|
||||
use_meteo_wxApi = config['location'].getboolean('UseMeteoWxAPI', False) # default False use NOAA
|
||||
use_metric = config['location'].getboolean('useMetric', False) # default Imperial units
|
||||
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
|
||||
@@ -368,6 +387,13 @@ try:
|
||||
signalCycleLimit = config['radioMon'].getint('signalCycleLimit', 5) # default 5 cycles, used with SIGNAL_COOLDOWN
|
||||
voxDetectionEnabled = config['radioMon'].getboolean('voxDetectionEnabled', False) # default VOX detection disabled
|
||||
voxDescription = config['radioMon'].get('voxDescription', 'VOX') # default VOX detected audio message
|
||||
useLocalVoxModel = config['radioMon'].getboolean('useLocalVoxModel', False) # default False
|
||||
localVoxModelPath = config['radioMon'].get('localVoxModelPath', 'no') # default models/vox.tflite
|
||||
voxLanguage = config['radioMon'].get('voxLanguage', 'en-US') # default en-US
|
||||
voxInputDevice = config['radioMon'].get('voxInputDevice', 'default') # default default
|
||||
voxOnTrapList = config['radioMon'].getboolean('voxOnTrapList', False) # default False
|
||||
voxTrapList = config['radioMon'].get('voxTrapList', 'chirpy').split(',') # default chirpy
|
||||
voxEnableCmd = config['radioMon'].getboolean('voxEnableCmd', True) # default True
|
||||
|
||||
# file monitor
|
||||
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
|
||||
@@ -378,7 +404,7 @@ try:
|
||||
news_random_line_only = config['fileMon'].getboolean('news_random_line', False) # default False
|
||||
enable_runShellCmd = config['fileMon'].getboolean('enable_runShellCmd', False) # default False
|
||||
allowXcmd = config['fileMon'].getboolean('allowXcmd', False) # default False
|
||||
xCmd2factorEnabled = config['fileMon'].getboolean('2factor_enabled', False) # default False
|
||||
xCmd2factorEnabled = config['fileMon'].getboolean('2factor_enabled', True) # default True
|
||||
xCmd2factor_timeout = config['fileMon'].getint('2factor_timeout', 100) # default 100 seconds
|
||||
|
||||
# games
|
||||
@@ -395,15 +421,17 @@ try:
|
||||
tictactoe_enabled = config['games'].getboolean('tictactoe', True)
|
||||
quiz_enabled = config['games'].getboolean('quiz', False)
|
||||
survey_enabled = config['games'].getboolean('survey', False)
|
||||
default_survey = config['games'].get('defaultSurvey', 'example') # default example
|
||||
surveyRecordID = config['games'].getboolean('surveyRecordID', True)
|
||||
surveyRecordLocation = config['games'].getboolean('surveyRecordLocation', True)
|
||||
wordOfTheDay = config['games'].getboolean('wordOfTheDay', 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 chars
|
||||
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
|
||||
maxBuffer = config['messagingSettings'].getint('maxBuffer', 200) # default 200
|
||||
maxBuffer = config['messagingSettings'].getint('maxBuffer', 200) # default 200 bytes
|
||||
enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False
|
||||
debugMetadata = config['messagingSettings'].getboolean('debugMetadata', False) # default False
|
||||
metadataFilter = config['messagingSettings'].get('metadataFilter', '').split(',') # default empty
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
import json
|
||||
import os # For file operations
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from collections import Counter
|
||||
from modules.log import *
|
||||
|
||||
@@ -49,20 +51,25 @@ class SurveyModule:
|
||||
logger.error(f"Survey: Error loading surveys: {e}")
|
||||
|
||||
def start_survey(self, user_id, survey_name='example', location=None):
|
||||
"""Begin a new survey session for a user."""
|
||||
if not survey_name:
|
||||
survey_name = 'example'
|
||||
if survey_name not in allowedSurveys:
|
||||
return f"error: survey '{survey_name}' is not allowed."
|
||||
self.responses[user_id] = {
|
||||
'survey_name': survey_name,
|
||||
'current_question': 0,
|
||||
'answers': [],
|
||||
'location': location if surveyRecordLocation and location is not None else 'N/A'
|
||||
}
|
||||
msg = f"'{survey_name}'📝survey\nSend answer' or 'end'\n"
|
||||
msg += self.show_question(user_id)
|
||||
return msg
|
||||
try:
|
||||
"""Begin a new survey session for a user."""
|
||||
if not survey_name:
|
||||
survey_name = default_survey
|
||||
if survey_name not in allowedSurveys:
|
||||
return f"error: survey '{survey_name}' is not allowed."
|
||||
self.responses[user_id] = {
|
||||
'survey_name': survey_name,
|
||||
'current_question': 0,
|
||||
'answers': [],
|
||||
'location': location if surveyRecordLocation and location is not None else 'N/A'
|
||||
}
|
||||
msg = f"'{survey_name}'📝survey\n"
|
||||
msg += self.show_question(user_id)
|
||||
msg += f"\nSend answer' or 'end'"
|
||||
return msg
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting survey for user {user_id}: {e}")
|
||||
return "An error occurred while starting the survey. Please try again later."
|
||||
|
||||
def show_question(self, user_id):
|
||||
"""Show the current question for the user, or end the survey."""
|
||||
@@ -92,17 +99,92 @@ class SurveyModule:
|
||||
filename = os.path.join(self.response_dir, f'{survey_name}_responses.csv')
|
||||
try:
|
||||
with open(filename, 'a', encoding='utf-8') as f:
|
||||
row = list(map(str, self.responses[user_id]['answers']))
|
||||
if surveyRecordID:
|
||||
row.insert(0, str(user_id))
|
||||
if surveyRecordLocation:
|
||||
location = self.responses[user_id].get('location')
|
||||
row.insert(1 if surveyRecordID else 0, str(location) if location is not None else "N/A")
|
||||
# Always write: timestamp, userID, position, answers...
|
||||
timestamp = datetime.now().strftime('%d%m%Y%H%M%S')
|
||||
user_id_str = str(user_id)
|
||||
location = self.responses[user_id].get('location', "N/A")
|
||||
answers = list(map(str, self.responses[user_id]['answers']))
|
||||
row = [timestamp, user_id_str, str(location)] + answers
|
||||
f.write(','.join(row) + '\n')
|
||||
logger.info(f"Survey: Responses for user {user_id} saved for survey '{survey_name}' to {filename}.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving responses to {filename}: {e}")
|
||||
|
||||
def format_survey_results(self, results):
|
||||
if isinstance(results, dict) and "error" in results:
|
||||
return results["error"]
|
||||
if not results:
|
||||
return "No results found."
|
||||
msg = "📊Survey Results:\n"
|
||||
for idx, q in enumerate(results):
|
||||
msg += f"\nQ{idx+1}: {q['question']}\n"
|
||||
if q['type'] == 'multiple_choice':
|
||||
for opt, count in q['summary'].items():
|
||||
msg += f" {opt}: {count}\n"
|
||||
elif q['type'] == 'integer':
|
||||
s = q['summary']
|
||||
msg += f" Count: {s['count']}, Avg: {s['average']:.2f}, Min: {s['min']}, Max: {s['max']}\n"
|
||||
elif q['type'] == 'text':
|
||||
msg += f" Responses: {q['summary']['responses_count']}\n"
|
||||
return msg
|
||||
|
||||
def get_survey_results(self, survey_name='example'):
|
||||
if survey_name not in self.surveys:
|
||||
return {"error": f"Survey '{survey_name}' not found."}
|
||||
filename = os.path.join(self.response_dir, f'{survey_name}_responses.csv')
|
||||
questions = self.surveys[survey_name]
|
||||
results = []
|
||||
try:
|
||||
with open(filename, encoding='utf-8') as f:
|
||||
reader = csv.reader(f)
|
||||
lines = []
|
||||
for row in reader:
|
||||
if not row or len(row) < 4:
|
||||
continue
|
||||
# If location field is split due to comma, join columns 2 and 3
|
||||
if row[2].startswith('[') and not row[2].endswith(']') and len(row) > 4:
|
||||
location = row[2] + ',' + row[3]
|
||||
answers = row[4:]
|
||||
else:
|
||||
location = row[2]
|
||||
answers = row[3:]
|
||||
lines.append(answers)
|
||||
|
||||
for q_idx, question in enumerate(questions):
|
||||
qtype = question.get('type', 'multiple_choice')
|
||||
answers = [row[q_idx] for row in lines if len(row) > q_idx]
|
||||
|
||||
summary = {}
|
||||
if qtype == 'multiple_choice':
|
||||
counts = Counter(answers)
|
||||
summary = {chr(65+i): counts.get(chr(65+i), 0) for i in range(len(question.get('options', [])))}
|
||||
|
||||
elif qtype == 'integer':
|
||||
ints = [int(a) for a in answers if a.isdigit()]
|
||||
summary = {
|
||||
"count": len(ints),
|
||||
"average": sum(ints)/len(ints) if ints else 0,
|
||||
"min": min(ints) if ints else None,
|
||||
"max": max(ints) if ints else None
|
||||
}
|
||||
|
||||
elif qtype == 'text':
|
||||
summary = {"responses_count": len([a for a in answers if a.strip()])}
|
||||
|
||||
|
||||
results.append({
|
||||
"question": question['question'],
|
||||
"type": qtype,
|
||||
"summary": summary
|
||||
})
|
||||
|
||||
return results
|
||||
except FileNotFoundError:
|
||||
return {"error": f"No responses recorded yet for '{survey_name}'."}
|
||||
except Exception as e:
|
||||
logger.error(f"Error summarizing survey results: {e}")
|
||||
return NO_ALERTS
|
||||
|
||||
def answer(self, user_id, answer, location=None):
|
||||
try:
|
||||
"""Record an answer and return the next question or end message."""
|
||||
@@ -121,7 +203,8 @@ class SurveyModule:
|
||||
return "Please answer with a letter (A, B, C, ...)."
|
||||
option_index = ord(answer_char) - 65
|
||||
if 0 <= option_index < len(question['options']):
|
||||
self.responses[user_id]['answers'].append(str(option_index))
|
||||
# Valid answer record letter, not index
|
||||
self.responses[user_id]['answers'].append(answer_char)
|
||||
self.responses[user_id]['current_question'] += 1
|
||||
return f"Recorded..\n" + self.show_question(user_id)
|
||||
else:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
126
modules/udp.py
Normal file
126
modules/udp.py
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# UDP Interface Listener
|
||||
# credit to pdxlocations for all of this core work https://github.com/pdxlocations/
|
||||
# depends on: pip install meshtastic protobuf zeroconf pubsub
|
||||
# 2025 Kelly Keeton K7MHI
|
||||
from pubsub import pub
|
||||
from meshtastic.protobuf import mesh_pb2, portnums_pb2
|
||||
from mudp import UDPPacketStream, node, conn, send_text_message, send_nodeinfo, send_device_telemetry, send_position, send_environment_metrics, send_power_metrics, send_waypoint, send_data
|
||||
from mudp.encryption import generate_hash
|
||||
import time
|
||||
from zeroconf import Zeroconf, ServiceBrowser
|
||||
import socket
|
||||
|
||||
MCAST_GRP, MCAST_PORT, CHANNEL_ID, KEY = "224.0.0.69", 4403, "LongFast", "1PG7OiApB1nwvP+rz05pAQ=="
|
||||
PUBLIC_CHANNEL_IDS = ["LongFast", "ShortSlow", "MediumFast", "MediumSlow", "ShortFast", "ShortTurbo"]
|
||||
mudpEnabled, mudpInterface = True, None
|
||||
messages = []
|
||||
|
||||
class ZeroconfListner:
|
||||
def add_service(self, zeroconf, type, name):
|
||||
info = zeroconf.get_service_info(type, name)
|
||||
if info:
|
||||
txt = info.properties
|
||||
ip = None
|
||||
if info.addresses:
|
||||
ip = socket.inet_ntoa(info.addresses[0])
|
||||
print(f"Found Meshtastic node: id={txt.get(b'id', b'').decode()} shortname={txt.get(b'shortname', b'').decode()} longname={txt.get(b'longname', b'').decode()} ip={ip}")
|
||||
|
||||
def update_service(self, zeroconf, type, name):
|
||||
# This method is required by zeroconf, but you can leave it empty if you don't need updates.
|
||||
pass
|
||||
|
||||
def initalize_mudp():
|
||||
global mudpInterface
|
||||
if mudpEnabled and mudpInterface is None:
|
||||
mudpInterface = UDPPacketStream(MCAST_GRP, MCAST_PORT, key=KEY)
|
||||
print(f"MUDP Interface initialized with multicast group", MCAST_GRP, "port", MCAST_PORT)
|
||||
node.node_id, node.long_name, node.short_name = "!deadbeef", "UDP Test", "UDP"
|
||||
node.channel, node.key = "LongFast", KEY
|
||||
conn.setup_multicast(MCAST_GRP, MCAST_PORT)
|
||||
|
||||
def on_recieve(packet: mesh_pb2.MeshPacket, addr=None):
|
||||
print(f"\n[RECV] Packet received from {addr}")
|
||||
print("from:", getattr(packet, "from", None))
|
||||
print("to:", packet.to)
|
||||
|
||||
# Check against all public channels
|
||||
matched_channel = None
|
||||
for channel_name in PUBLIC_CHANNEL_IDS:
|
||||
channel_hash = generate_hash(channel_name, KEY)
|
||||
if packet.channel == channel_hash:
|
||||
matched_channel = channel_name
|
||||
break
|
||||
|
||||
if matched_channel:
|
||||
channel_status = f"Match ({matched_channel})"
|
||||
else:
|
||||
channel_status = f"Hash: {packet.channel}"
|
||||
|
||||
print("channel:", channel_status)
|
||||
|
||||
if packet.HasField("decoded"):
|
||||
port_name = portnums_pb2.PortNum.Name(packet.decoded.portnum) if packet.decoded.portnum else "N/A"
|
||||
try:
|
||||
payload_decoded = True
|
||||
packet_payload = packet.decoded.payload.decode("utf-8", "ignore")
|
||||
except Exception:
|
||||
print(" payload (raw bytes):", packet.decoded.payload)
|
||||
else:
|
||||
print(f"encrypted: { {packet.encrypted} }")
|
||||
|
||||
|
||||
print("id:", packet.id or None)
|
||||
print("rx_time:", packet.rx_time or None)
|
||||
print("rx_snr:", packet.rx_snr or None)
|
||||
print("hop_limit:", packet.hop_limit or None)
|
||||
priority_name = mesh_pb2.MeshPacket.Priority.Name(packet.priority) if packet.priority else "N/A"
|
||||
print("priority:", priority_name or None)
|
||||
print("rx_rssi:", packet.rx_rssi or None)
|
||||
print("hop_start:", packet.hop_start or None)
|
||||
print("next_hop:", packet.next_hop or None)
|
||||
print("relay_node:", packet.relay_node or None)
|
||||
|
||||
print(f"decoded {{portnum: {port_name}, payload: {packet_payload if payload_decoded else 'N/A'}, bitfield: {packet.decoded.bitfield or None}}}" if packet.HasField("decoded") else "No decoded field")
|
||||
|
||||
pub.subscribe(on_recieve, "mesh.rx.packet")
|
||||
# pub.subscribe(on_text_message, "mesh.rx.port.1")
|
||||
# pub.subscribe(on_nodeinfo, "mesh.rx.port.4") # NODEINFO_APP
|
||||
|
||||
zeroconf = Zeroconf()
|
||||
listener = ZeroconfListner()
|
||||
browser = ServiceBrowser(zeroconf, "_meshtastic._tcp.local.", listener)
|
||||
|
||||
def main():
|
||||
initalize_mudp()
|
||||
mudpInterface.start()
|
||||
try:
|
||||
while True: time.sleep(0.05)
|
||||
except KeyboardInterrupt: pass
|
||||
finally: mudpInterface.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
# Meshtastic Port Numbers Reference:
|
||||
# | Port Number | Name | Purpose |
|
||||
# |-------------|------------------------|--------------------------------|
|
||||
# | 1 | TEXT_MESSAGE_APP | Text messages |
|
||||
# | 2 | POSITION_APP | Position updates (GPS) |
|
||||
# | 3 | ROUTING_APP | Routing info |
|
||||
# | 4 | NODEINFO_APP | Node info (name, id, etc) |
|
||||
# | 5 | TELEMETRY_APP | Telemetry (battery, sensors) |
|
||||
# | 6 | SERIAL_APP | Serial data |
|
||||
# | 7 | ENVIRONMENTAL_APP | Environmental sensors |
|
||||
# | 8 | REMOTE_HARDWARE_APP | Remote hardware control |
|
||||
# | 9 | STORE_FORWARD_APP | Store and forward |
|
||||
# | 10 | RANGE_TEST_APP | Range test |
|
||||
# | 11 | ADMIN_APP | Admin/config |
|
||||
# | 12 | WAYPOINT_APP | Waypoints |
|
||||
# | 13 | CHANNEL_NODEINFO_APP | Channel node info |
|
||||
# | 256 | PRIVATE_APP | Private app (custom use) |
|
||||
# See: https://github.com/meshtastic/protobufs/blob/main/meshtastic/protobuf/portnums.proto
|
||||
|
||||
|
||||
202
pong_bot.py
202
pong_bot.py
@@ -10,6 +10,7 @@ except ImportError:
|
||||
|
||||
import asyncio
|
||||
import time # for sleep, get some when you can :)
|
||||
from datetime import datetime
|
||||
import random
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
@@ -72,13 +73,13 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
type = ''
|
||||
|
||||
if "ping" in message.lower():
|
||||
msg = "🏓PONG\n"
|
||||
msg = "🏓PONG"
|
||||
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",\
|
||||
"🎙Ah-wun, ah-two...\n", "🎙Is this thing on?\n",\
|
||||
"🎙Roger that!\n",])
|
||||
msg = random.choice(["🎙Testing 1,2,3", "🎙Testing",\
|
||||
"🎙Testing, testing",\
|
||||
"🎙Ah-wun, ah-two...", "🎙Is this thing on?",\
|
||||
"🎙Roger that!",])
|
||||
type = "🎙TEST"
|
||||
elif "ack" in message.lower():
|
||||
msg = random.choice(["✋ACK-ACK!\n", "✋Ack to you!\n"])
|
||||
@@ -92,10 +93,21 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
else:
|
||||
msg = "🔊 Can you hear me now?"
|
||||
|
||||
if hop == "Direct":
|
||||
msg = msg + f"SNR:{snr} RSSI:{rssi}"
|
||||
# append SNR/RSSI or hop info
|
||||
if hop.startswith("Gateway") or hop.startswith("MQTT"):
|
||||
msg += f" [GW]"
|
||||
elif hop.startswith("Direct"):
|
||||
msg += f" [RF]"
|
||||
else:
|
||||
msg = msg + hop
|
||||
#flood
|
||||
msg += f" [F]"
|
||||
|
||||
if (float(snr) != 0 or float(rssi) != 0) and "Hops" not in hop:
|
||||
msg += f"\nSNR:{snr} RSSI:{rssi}"
|
||||
elif "Hops" in hop:
|
||||
msg += f"\n{hop}🐇 "
|
||||
else:
|
||||
msg += "\nflood route"
|
||||
|
||||
if "@" in message:
|
||||
msg = msg + " @" + message.split("@")[1]
|
||||
@@ -150,7 +162,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
def handle_motd(message, message_from_id, isDM):
|
||||
global MOTD
|
||||
isAdmin = False
|
||||
msg = ""
|
||||
msg = MOTD
|
||||
# check if the message_from_id is in the bbs_admin_list
|
||||
if bbs_admin_list != ['']:
|
||||
for admin in bbs_admin_list:
|
||||
@@ -216,11 +228,16 @@ def onReceive(packet, interface):
|
||||
rxType = type(interface).__name__
|
||||
|
||||
# Valies assinged to the packet
|
||||
rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0
|
||||
rxNode = message_from_id = snr = rssi = hop = hop_away = channel_number = hop_start = hop_count = hop_limit = 0
|
||||
pkiStatus = (False, 'ABC')
|
||||
replyIDset = False
|
||||
rxNodeHostName = None
|
||||
emojiSeen = False
|
||||
simulator_flag = False
|
||||
isDM = False
|
||||
channel_name = "unknown"
|
||||
session_passkey = None
|
||||
playingGame = False
|
||||
|
||||
if DEBUGpacket:
|
||||
# Debug print the interface object
|
||||
@@ -229,45 +246,60 @@ def onReceive(packet, interface):
|
||||
# Debug print the packet for debugging
|
||||
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
|
||||
|
||||
# set the value for the incomming interface
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
if port1 in rxInterface: rxNode = 1
|
||||
elif multiple_interface and port2 in rxInterface: rxNode = 2
|
||||
elif multiple_interface and port3 in rxInterface: rxNode = 3
|
||||
elif multiple_interface and port4 in rxInterface: rxNode = 4
|
||||
elif multiple_interface and port5 in rxInterface: rxNode = 5
|
||||
elif multiple_interface and port6 in rxInterface: rxNode = 6
|
||||
elif multiple_interface and port7 in rxInterface: rxNode = 7
|
||||
elif multiple_interface and port8 in rxInterface: rxNode = 8
|
||||
elif multiple_interface and port9 in rxInterface: rxNode = 9
|
||||
|
||||
# determine the rxNode based on the interface type
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
if rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
rxNodeHostName = interface.__dict__.get('ip', None)
|
||||
rxNode = next(
|
||||
(i for i in range(1, 10)
|
||||
if multiple_interface and rxHost and
|
||||
globals().get(f'hostname{i}', '').split(':', 1)[0] in rxHost and
|
||||
globals().get(f'interface{i}_type', '') == 'tcp'),None)
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble': rxNode = 1
|
||||
elif multiple_interface and interface2_type == 'ble': rxNode = 2
|
||||
elif multiple_interface and interface3_type == 'ble': rxNode = 3
|
||||
elif multiple_interface and interface4_type == 'ble': rxNode = 4
|
||||
elif multiple_interface and interface5_type == 'ble': rxNode = 5
|
||||
elif multiple_interface and interface6_type == 'ble': rxNode = 6
|
||||
elif multiple_interface and interface7_type == 'ble': rxNode = 7
|
||||
elif multiple_interface and interface8_type == 'ble': rxNode = 8
|
||||
elif multiple_interface and interface9_type == 'ble': rxNode = 9
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
rxNode = next(
|
||||
(i for i in range(1, 10)
|
||||
if globals().get(f'port{i}', '') in rxInterface),None)
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if rxType == 'BLEInterface':
|
||||
rxNode = next(
|
||||
(i for i in range(1, 10)
|
||||
if globals().get(f'interface{i}_type', '') == 'ble'),0)
|
||||
|
||||
if rxNode is None:
|
||||
# default to interface 1 ## FIXME needs better like a default interface setting or hash lookup
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] in ['ADMIN_APP', 'SIMULATOR_APP']:
|
||||
session_passkey = packet.get('decoded', {}).get('admin', {}).get('sessionPasskey', None)
|
||||
rxNode = 1
|
||||
|
||||
# check if the packet has a channel flag use it ## FIXME needs to be channel hash lookup
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
channel_number = packet.get('channel')
|
||||
# get channel name from channel number from connected devices
|
||||
for device in channel_list:
|
||||
if device["interface_id"] == rxNode:
|
||||
device_channels = device['channels']
|
||||
for chan_name, info in device_channels.items():
|
||||
if info['number'] == channel_number:
|
||||
channel_name = chan_name
|
||||
break
|
||||
|
||||
# get channel hashes for the interface
|
||||
device = next((d for d in channel_list if d["interface_id"] == rxNode), None)
|
||||
if device:
|
||||
# Find the channel name whose hash matches channel_number
|
||||
for chan_name, info in device['channels'].items():
|
||||
if info['hash'] == channel_number:
|
||||
print(f"Matched channel hash {info['hash']} to channel name {chan_name}")
|
||||
channel_name = chan_name
|
||||
break
|
||||
|
||||
# check if the packet has a simulator flag
|
||||
simulator_flag = packet.get('decoded', {}).get('simulator', False)
|
||||
if isinstance(simulator_flag, dict):
|
||||
# assume Software Simulator
|
||||
simulator_flag = True
|
||||
|
||||
# set the message_from_id
|
||||
message_from_id = packet['from']
|
||||
@@ -282,6 +314,7 @@ def onReceive(packet, interface):
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
via_mqtt = packet['decoded'].get('viaMqtt', False)
|
||||
transport_mechanism = packet['decoded'].get('transport_mechanism', 'unknown')
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
|
||||
@@ -294,46 +327,62 @@ def onReceive(packet, interface):
|
||||
|
||||
# check if the packet has a publicKey flag use it
|
||||
if packet.get('publicKey'):
|
||||
pkiStatus = (packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC'))
|
||||
pkiStatus = packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC')
|
||||
|
||||
# check if the packet has replyId flag // currently unused in the code
|
||||
if packet.get('replyId'):
|
||||
replyIDset = packet.get('replyId', False)
|
||||
|
||||
# check if the packet has emoji flag set it // currently unused in the code
|
||||
if packet.get('emoji'):
|
||||
emojiSeen = packet.get('emoji', False)
|
||||
|
||||
# check if the packet has a hop count flag use it
|
||||
if packet.get('hopsAway'):
|
||||
hop_away = packet.get('hopsAway', 0)
|
||||
|
||||
if packet.get('hopStart'):
|
||||
hop_start = packet.get('hopStart', 0)
|
||||
|
||||
if packet.get('hopLimit'):
|
||||
hop_limit = packet.get('hopLimit', 0)
|
||||
|
||||
# calculate hop count
|
||||
hop = ""
|
||||
if hop_limit > 0 and hop_start >= hop_limit:
|
||||
hop_count = hop_away + (hop_start - hop_limit)
|
||||
elif hop_limit > 0 and hop_start < hop_limit:
|
||||
hop_count = hop_away + (hop_limit - hop_start)
|
||||
else:
|
||||
# if the packet does not have a hop count try other methods
|
||||
if packet.get('hopLimit'):
|
||||
hop_limit = packet.get('hopLimit', 0)
|
||||
else:
|
||||
hop_limit = 0
|
||||
|
||||
if packet.get('hopStart'):
|
||||
hop_start = packet.get('hopStart', 0)
|
||||
else:
|
||||
hop_start = 0
|
||||
hop_count = hop_away
|
||||
|
||||
if hop == "" and hop_count > 0:
|
||||
# set hop string from calculated hop count
|
||||
hop = f"{hop_count} Hop" if hop_count == 1 else f"{hop_count} Hops"
|
||||
|
||||
if hop_start == hop_limit and "lora" in str(transport_mechanism).lower() and (snr != 0 or rssi != 0):
|
||||
# 2.7+ firmware direct hop over LoRa
|
||||
hop = "Direct"
|
||||
|
||||
if ((hop_start == 0 and hop_limit >= 0) or via_mqtt or ("mqtt" in str(transport_mechanism).lower())):
|
||||
hop = "MQTT"
|
||||
elif hop == "" and hop_count == 0 and (snr != 0 or rssi != 0):
|
||||
# this came from a UDP but we had signal info so gateway is used
|
||||
hop = "Gateway"
|
||||
elif "unknown" in str(transport_mechanism).lower() and (snr == 0 and rssi == 0):
|
||||
# we for sure detected this sourced from a UDP like host
|
||||
hop = "Gateway"
|
||||
|
||||
if hop in ("MQTT", "Gateway") and hop_count > 0:
|
||||
hop = f"{hop_count} Hops"
|
||||
|
||||
if enableHopLogs:
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start}")
|
||||
|
||||
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
|
||||
hop = "Last Hop"
|
||||
hop_count = 0
|
||||
|
||||
if hop_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
elif hop_start == 0 and hop_limit > 0 or via_mqtt:
|
||||
hop = "MQTT"
|
||||
hop_count = 0
|
||||
else:
|
||||
# set hop to Direct if the message was sent directly otherwise set the hop count
|
||||
if hop_away > 0:
|
||||
hop_count = hop_away
|
||||
else:
|
||||
hop_count = hop_start - hop_limit
|
||||
#print (f"calculated hop count: {hop_start} - {hop_limit} = {hop_count}")
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism} Hostname:{rxNodeHostName}")
|
||||
|
||||
# check with stringSafeChecker if the message is safe
|
||||
if stringSafeCheck(message_string) is False:
|
||||
logger.warning(f"System: Possibly Unsafe Message from {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
|
||||
hop = f"{hop_count} hops"
|
||||
|
||||
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)}")
|
||||
@@ -353,7 +402,6 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
send_message(welcome_message, channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
|
||||
# log the message to the message log
|
||||
if log_messages_to_file:
|
||||
|
||||
60
script/README.md
Normal file
60
script/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
## script/runShell.sh
|
||||
|
||||
**Purpose:**
|
||||
`runShell.sh` is a simple demo shell script for the Mesh Bot project. It demonstrates how to execute shell commands within the project’s scripting environment.
|
||||
|
||||
**Usage:**
|
||||
Run this script from the terminal to see a basic example of shell scripting in the project context.
|
||||
|
||||
```sh
|
||||
bash script/runShell.sh
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Changes the working directory to the script’s location.
|
||||
- Prints the current directory path and a message indicating the script is running.
|
||||
- Serves as a template for creating additional shell scripts or automating tasks related to the project.
|
||||
|
||||
**Note:**
|
||||
You can modify this script to add more shell commands or automation steps as needed for your workflow.
|
||||
|
||||
## script/runShell.sh
|
||||
|
||||
**Purpose:**
|
||||
`runShell.sh` is a demonstration shell script for the Mesh Bot project, showing how to execute shell commands in the project environment.
|
||||
|
||||
**Usage:**
|
||||
Run this script from the terminal to see a basic shell scripting example:
|
||||
|
||||
```sh
|
||||
bash script/runShell.sh
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Changes to the script’s directory.
|
||||
- Prints the current directory path.
|
||||
- Displays a message indicating the script is running.
|
||||
|
||||
**Note:**
|
||||
You can use this as a template for your own shell scripts or to automate project-related tasks.
|
||||
|
||||
## script/sysEnv.sh
|
||||
|
||||
**Purpose:**
|
||||
`sysEnv.sh` is a shell script that collects and displays system telemetry and environment information, especially useful for monitoring a Raspberry Pi or similar device running the Mesh Bot.
|
||||
|
||||
**Usage:**
|
||||
Run this script from the terminal to view system stats and network info:
|
||||
|
||||
```sh
|
||||
bash script/sysEnv.sh
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
- Reports disk space, RAM usage, CPU usage, and CPU temperature (in °C and °F).
|
||||
- Checks for available Git updates if the project is a Git repository.
|
||||
- Displays the device’s public and local IP addresses.
|
||||
- Designed to work on Linux systems, with special handling for Raspberry Pi hardware.
|
||||
|
||||
**Note:**
|
||||
You can expand or modify this script to include additional telemetry or environment checks as needed for your deployment.
|
||||
@@ -28,6 +28,7 @@ if __name__ == "__main__":
|
||||
|
||||
# welcome header
|
||||
print("meshing-around: addFav - Auto-Add favorite nodes to all interfaces from config.ini data")
|
||||
print("This script may need API improvments still in progress")
|
||||
print("---------------------------------------------------------------")
|
||||
|
||||
try:
|
||||
@@ -93,7 +94,7 @@ try:
|
||||
count_devices = set([fav['deviceID'] for fav in favList])
|
||||
count_nodes = set([fav['nodeID'] for fav in favList])
|
||||
for fav in favList:
|
||||
print(f"Device: {fav.get('deviceID', 'N/A')} Node: {fav.get('nodeID', 'N/A')} Interface: {fav.get('interface', 'N/A')}")
|
||||
print(f"addFav: adding nodeID {fav['nodeID']} meshtastic --set-favorite-node {fav['nodeID']}")
|
||||
confirm = input(f"Are you sure you want to add these {len(count_nodes)} favorite nodes to {len(count_devices)} device(s)? (y/n): ").strip().lower()
|
||||
if confirm != 'y':
|
||||
print("Operation cancelled by user.")
|
||||
@@ -109,8 +110,9 @@ if favList:
|
||||
# for each node,interface tuple add the favorite node
|
||||
for fav in favList:
|
||||
try:
|
||||
handleFavoritNode(fav['deviceID'], fav['nodeID'], True)
|
||||
time.sleep(1)
|
||||
handleFavoriteNode(fav['deviceID'], fav['nodeID'], True)
|
||||
logger.info(f"addFav: waiting 15 seconds to avoid API rate limits")
|
||||
time.sleep(15) # wait to avoid API rate limits
|
||||
except Exception as e:
|
||||
logger.error(f"addFav: Error adding favorite node {fav['nodeID']} to device {fav['deviceID']}: {e}")
|
||||
else:
|
||||
|
||||
104
script/configMerge.py
Normal file
104
script/configMerge.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Configuration Merge Script
|
||||
# Merges user configuration with default settings
|
||||
# 2025 Kelly Keeton K7MHI mesh-around and its meshtastic
|
||||
import shutil
|
||||
import configparser
|
||||
import os
|
||||
|
||||
|
||||
def merge_configs(default_config_path, user_config_path, output_config_path):
|
||||
# Load default configuration (INI)
|
||||
default_config = configparser.ConfigParser()
|
||||
default_config.read(default_config_path)
|
||||
|
||||
# Load user configuration (INI)
|
||||
user_config = configparser.ConfigParser()
|
||||
user_config.read(user_config_path)
|
||||
|
||||
# Merge configurations
|
||||
for section in user_config.sections():
|
||||
if not default_config.has_section(section):
|
||||
default_config.add_section(section)
|
||||
for key, value in user_config.items(section):
|
||||
default_config.set(section, key, value)
|
||||
|
||||
# Save merged configuration as INI
|
||||
with open(output_config_path, 'w', encoding='utf-8') as f:
|
||||
default_config.write(f)
|
||||
|
||||
def backup_config(config_path, backup_path):
|
||||
shutil.copyfile(config_path, backup_path)
|
||||
|
||||
def show_config_changes(user_config_path, merged_config_path):
|
||||
if not os.path.exists(merged_config_path) or os.path.getsize(merged_config_path) == 0:
|
||||
print(f"Error: {merged_config_path} is empty or missing!")
|
||||
return
|
||||
|
||||
# Load user config (as dict)
|
||||
user_config = configparser.ConfigParser()
|
||||
user_config.read(user_config_path)
|
||||
user_dict = {s: dict(user_config.items(s)) for s in user_config.sections()}
|
||||
|
||||
# Load merged config (as dict)
|
||||
merged_config = configparser.ConfigParser()
|
||||
merged_config.read(merged_config_path)
|
||||
merged_dict = {s: dict(merged_config.items(s)) for s in merged_config.sections()}
|
||||
|
||||
print("\n--- Changes in merged configuration ---")
|
||||
for section in merged_dict:
|
||||
if section not in user_dict:
|
||||
print(f"[{section}] (new section)")
|
||||
for k, v in merged_dict[section].items():
|
||||
print(f" {k} = {v} (added)")
|
||||
else:
|
||||
for k, v in merged_dict[section].items():
|
||||
if k not in user_dict[section]:
|
||||
print(f"[{section}] {k} = {v} (added)")
|
||||
elif user_dict[section][k] != v:
|
||||
print(f"[{section}] {k}: {user_dict[section][k]} -> {v} (changed)")
|
||||
print("--- End of changes ---\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("MESHING-AROUND: Configuration Merge Script for config.ini checking updates from config.template")
|
||||
print("---------------------------------------------------------------")
|
||||
master_config_path = 'config.template'
|
||||
user_config_path = 'config.ini'
|
||||
output_config = 'config_new.ini'
|
||||
backup_config_path = 'config.bak'
|
||||
|
||||
# Step 1: Check master config
|
||||
try:
|
||||
if not os.path.exists(master_config_path) or os.path.getsize(master_config_path) == 0:
|
||||
raise FileNotFoundError(f"Master configuration file {master_config_path} is missing or empty.")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
print("Run the tool from the meshing-around/script/ directory where the config.template is located.")
|
||||
print(" python3 script/configMerge.py")
|
||||
exit(1)
|
||||
|
||||
# Step 2: Backup user config
|
||||
try:
|
||||
backup_config(user_config_path, backup_config_path)
|
||||
print(f"Backup of user config created at {backup_config_path}")
|
||||
except Exception as e:
|
||||
print(f"Error backing up user config: {e}")
|
||||
exit(1)
|
||||
|
||||
# Step 3: Merge configs
|
||||
try:
|
||||
merge_configs(master_config_path, user_config_path, output_config)
|
||||
print(f"Merged configuration saved to {output_config}")
|
||||
except Exception as e:
|
||||
print(f"Error merging configuration: {e}")
|
||||
exit(1)
|
||||
|
||||
# Step 4: Show changes
|
||||
try:
|
||||
show_config_changes(user_config_path, output_config)
|
||||
print("Please review the new configuration and replace your existing config.ini if needed.")
|
||||
print(" cp config_new.ini config.ini")
|
||||
except Exception as e:
|
||||
print(f"Error showing configuration changes: {e}")
|
||||
exit(1)
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# Usage: python3 script/injectDM.py -s NODEID -d NODEID -m "message"
|
||||
# meshing-around - helper script
|
||||
# meshing-around - helper script - enable the bbsAPI in config.ini first
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
|
||||
@@ -42,3 +42,15 @@ then
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get public and local IP addresses
|
||||
public_ip=$(curl -s https://ifconfig.me 2>/dev/null)
|
||||
public_ip=${public_ip:-""}
|
||||
local_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
local_ip=${local_ip:-""}
|
||||
if [ -n "$public_ip" ]; then
|
||||
echo "Public IP: $public_ip"
|
||||
fi
|
||||
if [ -n "$local_ip" ]; then
|
||||
echo "Local IP: $local_ip"
|
||||
fi
|
||||
53
update.sh
53
update.sh
@@ -24,6 +24,13 @@ if systemctl is-active --quiet mesh_bot_w3.service; then
|
||||
service_stopped=true
|
||||
fi
|
||||
|
||||
# Fetch latest changes from GitHub
|
||||
echo "Fetching latest changes from GitHub..."
|
||||
if ! git fetch origin; then
|
||||
echo "Error: Failed to fetch from GitHub, check your network connection."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# git pull with rebase to avoid unnecessary merge commits
|
||||
echo "Pulling latest changes from GitHub..."
|
||||
if ! git pull origin main --rebase; then
|
||||
@@ -37,22 +44,42 @@ if ! git pull origin main --rebase; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install or update dependencies
|
||||
echo "Installing or updating dependencies..."
|
||||
if pip install -r requirements.txt --upgrade 2>&1 | grep -q "externally-managed-environment"; then
|
||||
# if venv is found ask to run with launch.sh
|
||||
if [ -d "venv" ]; then
|
||||
echo "A virtual environment (venv) was found. run from inside venv"
|
||||
# copy modules/custom_scheduler.py template if it does not exist
|
||||
if [[ ! -f modules/custom_scheduler.py ]]; then
|
||||
cp etc/custom_scheduler.py modules/custom_scheduler.py
|
||||
printf "\nCustom scheduler template copied to modules/custom_scheduler.py\n"
|
||||
fi
|
||||
|
||||
# Backup the data/ directory
|
||||
echo "Backing up data/ directory..."
|
||||
#backup_file="backup_$(date +%Y%m%d_%H%M%S).tar.gz"
|
||||
backup_file="data_backup.tar.gz"
|
||||
path2backup="data/"
|
||||
#copy custom_scheduler.py if it exists
|
||||
if [ -f "modules/custom_scheduler.py" ]; then
|
||||
echo "Including custom_scheduler.py in backup..."
|
||||
cp modules/custom_scheduler.py data/
|
||||
fi
|
||||
tar -czf "$backup_file" "$path2backup"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Backup failed."
|
||||
else
|
||||
echo "Backup of ${path2backup} completed: ${backup_file}"
|
||||
fi
|
||||
|
||||
|
||||
# Build a config_new.ini file merging user config with new defaults
|
||||
echo "Merging configuration files..."
|
||||
python3 script/configMerge.py > ini_merge_log.txt 2>&1
|
||||
|
||||
if [ -f ini_merge_log.txt ]; then
|
||||
if grep -q "Error during configuration merge" ini_merge_log.txt; then
|
||||
echo "Configuration merge encountered errors. Please check ini_merge_log.txt for details."
|
||||
else
|
||||
read -p "Warning: You are in an externally managed environment. Do you want to continue with --break-system-packages? (y/n): " choice
|
||||
if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
|
||||
pip install --break-system-packages -r requirements.txt --upgrade
|
||||
else
|
||||
echo "Update aborted due to dependency installation issue."
|
||||
fi
|
||||
echo "Configuration merge completed. Please review config_new.ini and ini_merge_log.txt."
|
||||
fi
|
||||
else
|
||||
echo "Dependencies installed or updated."
|
||||
echo "Configuration merge log (ini_merge_log.txt) not found. check out the script/configMerge.py tool!"
|
||||
fi
|
||||
|
||||
# if service was stopped earlier, restart it
|
||||
|
||||
Reference in New Issue
Block a user