mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
177 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fa5928537 | ||
|
|
f12198b140 | ||
|
|
0d44ffb635 | ||
|
|
c11ebf1443 | ||
|
|
b94a5ebd8d | ||
|
|
3392d2d5a8 | ||
|
|
1df3a7aaa2 | ||
|
|
9a11214208 | ||
|
|
0a4f101370 | ||
|
|
5f3c32dc00 | ||
|
|
74cb135c6c | ||
|
|
a20e520501 | ||
|
|
23e0e4c6a0 | ||
|
|
10918546d6 | ||
|
|
cf16cc6606 | ||
|
|
3b73b665d6 | ||
|
|
993fd760af | ||
|
|
a029334576 | ||
|
|
eb8143f298 | ||
|
|
c756b447ac | ||
|
|
cef05e061c | ||
|
|
c85d517b91 | ||
|
|
170d1a6a45 | ||
|
|
8d2313cfb1 | ||
|
|
ed8636f5a5 | ||
|
|
b95d94f06f | ||
|
|
f7cdf446bf | ||
|
|
28e8e2705a | ||
|
|
9bc6f6f661 | ||
|
|
2630310210 | ||
|
|
3fae42305c | ||
|
|
9cc8dd7143 | ||
|
|
7ffa9d5309 | ||
|
|
30d2b996c0 | ||
|
|
49c098ef0b | ||
|
|
afa41c6ecd | ||
|
|
8861179cb2 | ||
|
|
f32ceb0383 | ||
|
|
9a380964aa | ||
|
|
180a8261ca | ||
|
|
0536657c8e | ||
|
|
c5a2330dd1 | ||
|
|
dc0b5be387 | ||
|
|
a1f43a5e94 | ||
|
|
b05a817769 | ||
|
|
f7187fdf27 | ||
|
|
cca51d68dd | ||
|
|
21804cc975 | ||
|
|
7a9ee27336 | ||
|
|
0c637226b2 | ||
|
|
555b14ddc0 | ||
|
|
656c23c631 | ||
|
|
bb591257c9 | ||
|
|
364a5c5c67 | ||
|
|
8cb05d38db | ||
|
|
f9fe13f322 | ||
|
|
b8d33cc270 | ||
|
|
a6ce9e9211 | ||
|
|
60bdabdd1b | ||
|
|
9c5c2080cf | ||
|
|
8f758229cb | ||
|
|
8ac9c53f1a | ||
|
|
98cbf5528c | ||
|
|
6296150677 | ||
|
|
13cb1e8df9 | ||
|
|
e26e876ccf | ||
|
|
550b50f74e | ||
|
|
ac5aa1a201 | ||
|
|
19700f54c5 | ||
|
|
7e5626cd30 | ||
|
|
c27b6ed8a1 | ||
|
|
717181bcd0 | ||
|
|
4d5916df29 | ||
|
|
93b7a1d613 | ||
|
|
35cc029984 | ||
|
|
589d44c152 | ||
|
|
06a14d875f | ||
|
|
454f823ad7 | ||
|
|
6974c4ef66 | ||
|
|
bd956dfebc | ||
|
|
4aaac5ba49 | ||
|
|
2ae792dd8d | ||
|
|
ca033f024e | ||
|
|
ad11f787de | ||
|
|
e3d1607c86 | ||
|
|
b68461cbc8 | ||
|
|
ddad35aa1e | ||
|
|
35f4aad6f8 | ||
|
|
f08f98e040 | ||
|
|
467376d9c7 | ||
|
|
1cbdc93632 | ||
|
|
2323015617 | ||
|
|
51de0dee8a | ||
|
|
b74c0ebd36 | ||
|
|
0a4c54a5a2 | ||
|
|
481809493c | ||
|
|
c3914e0423 | ||
|
|
ac40254bc4 | ||
|
|
b6540a1d20 | ||
|
|
87d29d123f | ||
|
|
0aa6f8cc07 | ||
|
|
e2bb480f5f | ||
|
|
920f951e47 | ||
|
|
215fe76f2a | ||
|
|
1740bbf666 | ||
|
|
f9370d47b4 | ||
|
|
91072cb47d | ||
|
|
c30be37f02 | ||
|
|
d51dadba04 | ||
|
|
99c404f479 | ||
|
|
659ee2959c | ||
|
|
1ac9f3b0d6 | ||
|
|
d0dc737863 | ||
|
|
e438c82a11 | ||
|
|
9d7d4601dc | ||
|
|
fdd741446c | ||
|
|
fdbab1685f | ||
|
|
ed0940b126 | ||
|
|
a087c7bb3a | ||
|
|
0439db2ec0 | ||
|
|
c1a5d4d336 | ||
|
|
eeffc6361a | ||
|
|
e2be3c20b7 | ||
|
|
b43c21fc98 | ||
|
|
e115f33d47 | ||
|
|
b8016aafc9 | ||
|
|
743b0ab10b | ||
|
|
e06b2a3581 | ||
|
|
582e00402a | ||
|
|
82551e0b4a | ||
|
|
a9c2660ec1 | ||
|
|
fa802ba313 | ||
|
|
874d56045e | ||
|
|
8204cbe60f | ||
|
|
a50c06206c | ||
|
|
895e5a2b07 | ||
|
|
2012986aff | ||
|
|
63d1f84887 | ||
|
|
d8233bc9e2 | ||
|
|
bdea3d6036 | ||
|
|
2fe2009b97 | ||
|
|
dcad12935f | ||
|
|
0e2f6343a2 | ||
|
|
56bd6f9ea7 | ||
|
|
5718a43d20 | ||
|
|
f759e2e7e5 | ||
|
|
1e97554cbf | ||
|
|
04d4a2f5a7 | ||
|
|
fb47756deb | ||
|
|
a33fed711d | ||
|
|
bcb741102d | ||
|
|
8b2d933fd1 | ||
|
|
f8d6419551 | ||
|
|
cf518aeff5 | ||
|
|
95eebcde2b | ||
|
|
5cd7dca9b0 | ||
|
|
eb87cf1bc8 | ||
|
|
8a510a7b11 | ||
|
|
e2631407e8 | ||
|
|
eb86fa911c | ||
|
|
448ad65c67 | ||
|
|
bb8d2167ce | ||
|
|
a2bf33d71d | ||
|
|
e287bdeaef | ||
|
|
16e5acbd27 | ||
|
|
1ea6961393 | ||
|
|
bd2bce0029 | ||
|
|
33c8d4c0ad | ||
|
|
d453c3cac1 | ||
|
|
187fc7c2e4 | ||
|
|
33154626e5 | ||
|
|
cfdbf1836f | ||
|
|
054692adf0 | ||
|
|
ce33421b16 | ||
|
|
d2cde424fc | ||
|
|
517ae5d4b4 | ||
|
|
e69ee5c1a8 |
@@ -1,4 +1,4 @@
|
||||
FROM python:3.10-slim
|
||||
FROM python:3.13-slim
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
RUN apt-get update && apt-get install -y gettext tzdata locales && rm -rf /var/lib/apt/lists/*
|
||||
@@ -7,7 +7,7 @@ RUN apt-get update && apt-get install -y gettext tzdata locales && rm -rf /var/l
|
||||
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
|
||||
dpkg-reconfigure --frontend=noninteractive locales && \
|
||||
update-locale LANG=en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANG=en_US.UTF-8
|
||||
ENV TZ="America/Los_Angeles"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
98
README.md
98
README.md
@@ -5,6 +5,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||

|
||||
|
||||
## Key Features
|
||||

|
||||
|
||||
### Intelligent Keyword Responder
|
||||
- **Automated Responses**: The bot detects keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
|
||||
@@ -28,9 +29,10 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS expanding visability.
|
||||
|
||||
### Interactive AI and Data Lookup
|
||||
- **NOAA location Data**: Get localized weather(alerts) and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
|
||||
- **NOAA location Data**: Get localized weather(alerts), 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.
|
||||
- **Satalite Pass Info**: Get passes for satalite at your location.
|
||||
|
||||
### Proximity Alerts
|
||||
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
|
||||
@@ -47,6 +49,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **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.
|
||||
- **EAS Alerts over the air**: Utilizing external tools to report EAS alerts offline over mesh.
|
||||
- **UK.GOV Alerts**: Pulling data form the UK.GOV alert page
|
||||
|
||||
### File Monitor Alerts
|
||||
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
|
||||
@@ -59,11 +62,12 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Message Chunking**: Automatically chunk messages over 160 characters to ensure higher delivery success across 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, or [femtofox](https://github.com/noon92/femtofox) project for embedding, possibly see the [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic). 🥔 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, [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), there is also [femtofox](https://github.com/noon92/femtofox). 🥔 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.
|
||||
|
||||
### Installation
|
||||
|
||||
#### Clone the Repository
|
||||
If you dont have git you will need it `sudo apt-get install git`
|
||||
```sh
|
||||
git clone https://github.com/spudgunman/meshing-around
|
||||
```
|
||||
@@ -76,7 +80,7 @@ The code is under active development, so make sure to pull the latest changes re
|
||||
#### Docker Installation
|
||||
If you prefer to use Docker, follow these steps:
|
||||
|
||||
1. Ensure your serial port is properly shared and the GPU is configured if using LLM in docker with [NVIDIA](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/docker-specialized.html).
|
||||
1. Ensure your serial port is properly shared.
|
||||
2. Build the Docker image:
|
||||
```sh
|
||||
cd meshing-around
|
||||
@@ -135,7 +139,7 @@ ignoreDefaultChannel = False # ignoreDefaultChannel, the bot will ignore the def
|
||||
```
|
||||
|
||||
### 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. It is also the default (or value when none found for user) for Sentry, all NOAA, repeater lookup, etc.
|
||||
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]
|
||||
@@ -143,10 +147,11 @@ enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
UseMeteoWxAPI = True
|
||||
riverListDefault = # NOAA Hydrology data, unique identifiers, LID or USGS ID
|
||||
```
|
||||
|
||||
### Module Settings
|
||||
Modules can be enabled or disabled as needed.
|
||||
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]
|
||||
@@ -179,7 +184,7 @@ sentryIgnoreList = # list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
```
|
||||
|
||||
### E-Mail / SMS Settings
|
||||
To enable connectivity with SMTP/IMAP.
|
||||
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]
|
||||
@@ -198,31 +203,25 @@ Traps the following ("emergency", "911", "112", "999", "police", "fire", "ambula
|
||||
|
||||
```ini
|
||||
[emergencyHandler]
|
||||
# enable or disable the emergency response handler
|
||||
enabled = True
|
||||
# channel to send a message to when the emergency handler is triggered
|
||||
alert_channel = 2
|
||||
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
|
||||
UNDER DEV..
|
||||
|
||||
This uses the SAME code to locate the bot and alerts.
|
||||
#### FEMA iPAWS/EAS and UK.gov
|
||||
This uses USA: SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages. UK.gov for England
|
||||
|
||||
```ini
|
||||
# FEMA IPAWS/CAP Alert Broadcast
|
||||
femaAlertBroadcastEnabled = True
|
||||
# FEMA IPAWS/CAP Alert Broadcast Channels
|
||||
femaAlertBroadcastCh = 2,4
|
||||
# Ignore any headline that includes the word Test
|
||||
ignoreFEMAtest = True
|
||||
# comma separated list of codes trigger local alert. (e.g., SAME, FIPS, ZIP)
|
||||
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
|
||||
eAlertBroadcastCh = 2,3 # Goverment Emergency IPAWS/CAP Alert Broadcast Channels
|
||||
ignoreFEMAtest = True # Ignore any headline that includes the word Test
|
||||
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
|
||||
# find your SAME https://www.weather.gov/nwr/counties
|
||||
mySAME = 053029,053073
|
||||
enableGBalerts = False # use UK.gov for alert source
|
||||
```
|
||||
|
||||
#### NOAA EAS
|
||||
@@ -262,6 +261,7 @@ llmEnableHistory = True # enable history for the LLM model to use in responses a
|
||||
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.
|
||||
|
||||
### 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.**
|
||||
@@ -287,6 +287,8 @@ file_path = alert.txt
|
||||
broadcastCh = 2,4
|
||||
enable_read_news = False
|
||||
news_file_path = news.txt
|
||||
news_random_line = False # only return a single random line from the news file
|
||||
enable_runShellCmd = False # enables running of bash commands runShell.sh demo for sysinfo
|
||||
```
|
||||
|
||||
#### Offline EAS
|
||||
@@ -314,6 +316,8 @@ rtl_fm -f 162425000 -s 22050 | multimon-ng -t raw -a EAS /dev/stdin | python eas
|
||||
#### Newspaper on mesh
|
||||
a newspaper could be built by external scripts. could use Ollama to compile text via news web pages and write news.txt
|
||||
|
||||
you can also enable the line by line (hint just search for the commented lines with a 🐝) to return a string from the [bee movie](https://courses.cs.washington.edu/courses/cse163/20wi/files/lectures/L04/bee-movie.txt) for example adding it alongside news.txt as bee.txt
|
||||
|
||||
### Scheduler
|
||||
In the config.ini enable the module
|
||||
```ini
|
||||
@@ -332,7 +336,7 @@ schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now
|
||||
```
|
||||
|
||||
#### BBS Link
|
||||
The scheduler also handles the BBL Link Brodcast message, this would be an esxample of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initator, one direction pull.
|
||||
The scheduler also handles the BBS Link Brodcast message, this would be an esxample of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initator, one direction pull.
|
||||
```python
|
||||
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 8 on device 1
|
||||
schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 8, 0, 1))
|
||||
@@ -343,7 +347,9 @@ bbslink_whitelist = # list of whitelisted nodes numbers ex: 2813308004,425867530
|
||||
```
|
||||
|
||||
### MQTT Notes
|
||||
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. There also seems to be a quicker way to enable MQTT by having your bot node with the enabled [serial](https://meshtastic.org/docs/configuration/module/serial/) module with echo enabled and MQTT uplink and downlink. These two methods have been mentioned as allowing MQTT routing for the project.
|
||||
There is no direct support for MQTT in the code, however, reports from Discord are that using [meshtasticd](https://meshtastic.org/docs/hardware/devices/linux-native-hardware/) with no radio and attaching the bot to the software node, which is MQTT-linked, allows routing. Tested working fully Firmware:2.5.15.79da236 with [mosquitto](https://meshtastic.org/docs/software/integrations/mqtt/mosquitto/).
|
||||
|
||||
~~There also seems to be a quicker way to enable MQTT by having your bot node with the enabled [serial](https://meshtastic.org/docs/configuration/module/serial/) module with echo enabled and MQTT uplink and downlink. These two~~
|
||||
|
||||
## Full list of commands for the bot
|
||||
|
||||
@@ -351,25 +357,28 @@ There is no direct support for MQTT in the code, however, reports from Discord a
|
||||
| Command | Description | ✅ Works Off-Grid |
|
||||
|---------|-------------|-
|
||||
| `ping`, `ack` | Return data for signal. Example: `ping 15 #DrivingI5` (activates auto-ping every 20 seconds for count 15) | ✅ |
|
||||
| `test` | Returns like ping but also can be used to test the limits of data buffers `test 4` sends data to the maxBuffer limit (default 220) | ✅ |
|
||||
| `cmd` | Returns the list of commands (the help message) | ✅ |
|
||||
| `history` | Returns the last commands run by user(s) | ✅ |
|
||||
| `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 220) | ✅ |
|
||||
| `whereami` | Returns the address of the sender's location if known |
|
||||
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
|
||||
| `motd` | Displays the message of the day or sets it. Example: `motd $New Message Of the day` | ✅ |
|
||||
| `lheard` | Returns the last 5 heard nodes with SNR. Can also use `sitrep` | ✅ |
|
||||
| `history` | Returns the last commands run by user(s) | ✅ |
|
||||
| `cmd` | Returns the list of commands (the help message) | ✅ |
|
||||
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
|
||||
|
||||
### Radio Propagation & Weather Forcasting
|
||||
| Command | Description | |
|
||||
|---------|-------------|-------------------
|
||||
| `sun` and `moon` | Return info on rise and set local time | ✅ |
|
||||
| `solar` | Gives an idea of the x-ray flux | |
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or UK. Headline or expanded details for USA | |
|
||||
| `hfcond` | Returns a table of HF solar conditions | |
|
||||
| `tide` | Returns the local tides (NOAA data source) |
|
||||
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
|
||||
| `riverflow` | Return information from NOAA for river flow info. Example: `riverflow modules/settings.py`| |
|
||||
| `solar` | Gives an idea of the x-ray flux | |
|
||||
| `sun` and `moon` | Return info on rise and set local time | ✅ |
|
||||
| `tide` | Returns the local tides (NOAA data source) |
|
||||
| `wx` and `wxc` | Return local weather forecast (wxc is metric value), NOAA or Open Meteo for weather forecasting | |
|
||||
| `wxa` and `wxalert` | Return NOAA alerts. Short title or expanded details | |
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts. Headline or expanded details | |
|
||||
|
||||
### Bulletin Board & Mail
|
||||
| Command | Description | |
|
||||
@@ -390,21 +399,22 @@ There is no direct support for MQTT in the code, however, reports from Discord a
|
||||
### Data Lookup
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
|
||||
| `askai` and `ask:` | Ask Ollama LLM AI for a response. Example: `askai what temp do I cook chicken` | ✅ |
|
||||
| `messages` | Replays the last messages heard, like Store and Forward | ✅ |
|
||||
| `readnews` | returns the contents of a file (news.txt, by default) via the chunker on air | ✅ |
|
||||
| `satpass` | returns the pass info from API for defined NORAD ID in config or Example: `satpass 25544,33591`| |
|
||||
| `wiki:` | Searches Wikipedia and returns the first few sentences of the first result if a match. Example: `wiki: lora radio` |
|
||||
|
||||
### Games (via DM)
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
|
||||
| `dopewars` | Plays the classic drug trader game | ✅ |
|
||||
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
|
||||
| `joke` | Tells a joke | ✅ |
|
||||
| `lemonstand` | Plays the classic Lemonade Stand finance game | ✅ |
|
||||
| `dopewars` | Plays the classic drug trader game | ✅ |
|
||||
| `blackjack` | Plays Blackjack (Casino 21) | ✅ |
|
||||
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
|
||||
| `mastermind` | Plays the classic code-breaking game | ✅ |
|
||||
| `golfsim` | Plays a 9-hole Golf Simulator | ✅ |
|
||||
| `videopoker` | Plays basic 5-card hold Video Poker | ✅ |
|
||||
|
||||
# Recognition
|
||||
|
||||
@@ -430,6 +440,8 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
- **[https://github.com/A-c0rN](A-c0rN)**: Assistance with iPAWS and EAS
|
||||
- **Mike O'Connell/skrrt**: For [eas_alert_parser](etc/eas_alert_parser.py) enhanced by **sheer.cold**
|
||||
- **PiDiBi**: For looking at test functions and other suggestions like wxc, CPU use, and alerting ideas.
|
||||
- **WH6GXZ nurse dude**: For bashing on installer
|
||||
- **Josh**: For more bashing on installer!
|
||||
- **Cisien, bitflip, **Woof**, **propstg**, **Josh** and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
|
||||
|
||||
@@ -437,7 +449,7 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
- **Node Backup Management**: [Node Slurper](https://github.com/SpudGunMan/node-slurper)
|
||||
|
||||
### Requirements
|
||||
Python 3.8? or later is needed (dev on latest). The following can be installed with `pip install -r requirements.txt` or using the [install.sh](install.sh) script for venv and automation:
|
||||
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
|
||||
@@ -457,14 +469,6 @@ pip install schedule
|
||||
pip install wikipedia
|
||||
```
|
||||
|
||||
For open-meteo use:
|
||||
|
||||
```sh
|
||||
pip install openmeteo_requests
|
||||
pip install retry_requests
|
||||
pip install numpy
|
||||
```
|
||||
|
||||
For the Ollama LLM:
|
||||
|
||||
```sh
|
||||
@@ -476,3 +480,5 @@ 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.
|
||||
|
||||
@@ -9,7 +9,7 @@ type = serial
|
||||
port = /dev/ttyACM0
|
||||
# port = /dev/ttyUSB0
|
||||
# port = COM1
|
||||
# hostname = 192.168.0.1
|
||||
# hostname = localhost
|
||||
# mac = 00:11:22:33:44:55
|
||||
|
||||
# Additional interface for dual radio support
|
||||
@@ -25,6 +25,8 @@ port = /dev/ttyUSB0
|
||||
[general]
|
||||
# if False will respond on all channels but the default channel
|
||||
respond_by_dm_only = True
|
||||
# Allows auto-ping feature in a channel, False forces to 1 ping only
|
||||
autoPingInChannel = False
|
||||
# defaultChannel is the meshtastic default public channel, e.g. LongFast (if none use -1)
|
||||
defaultChannel = 0
|
||||
# ignoreDefaultChannel, the bot will ignore the default channel set above
|
||||
@@ -106,61 +108,50 @@ bbslink_enabled = False
|
||||
# list of whitelisted nodes numbers ex: 2813308004,4258675309 empty list allows all
|
||||
bbslink_whitelist =
|
||||
|
||||
[smtp]
|
||||
# enable or disable the SMTP module
|
||||
enableSMTP = False
|
||||
# enable or disable the IMAP module for inbound email
|
||||
enableImap = False
|
||||
# list of Sysop Emails seperate with commas
|
||||
sysopEmails =
|
||||
SMTP_SERVER = smtp.gmail.com
|
||||
# 587 SMTP over TLS/STARTTLS, 25 legacy SMTP, 465 SMTP over SSL
|
||||
SMTP_PORT = 587
|
||||
# Sender email: be mindful of public access, don't use your personal email
|
||||
FROM_EMAIL = none@gmail.com
|
||||
SMTP_AUTH = True
|
||||
SMTP_USERNAME = none@gmail.com
|
||||
SMTP_PASSWORD = none
|
||||
EMAIL_SUBJECT = Meshtastic✉️
|
||||
|
||||
# IMAP not implimented yet
|
||||
IMAP_SERVER = imap.gmail.com
|
||||
# 993 IMAP over TLS/SSL, 143 legacy IMAP
|
||||
IMAP_PORT = 993
|
||||
# IMAP login usually same as SMTP
|
||||
IMAP_USERNAME = none@gmail.com
|
||||
IMAP_PASSWORD = none
|
||||
IMAP_FOLDER = inbox
|
||||
|
||||
|
||||
# location module
|
||||
[location]
|
||||
enabled = True
|
||||
lat = 48.50
|
||||
lon = -123.0
|
||||
|
||||
# Default to metric units rather than imperial
|
||||
useMetric = False
|
||||
|
||||
# repeaterList lookup location (rbook / artsci)
|
||||
repeaterLookup = rbook
|
||||
|
||||
# NOAA weather forecast days, the first two rows are today and tonight
|
||||
NOAAforecastDuration = 4
|
||||
# number of weather alerts to display
|
||||
NOAAalertCount = 2
|
||||
|
||||
# use Open-Meteo API for weather data not NOAA useful for non US locations
|
||||
UseMeteoWxAPI = False
|
||||
# Default to metric units rather than imperial
|
||||
useMetric = False
|
||||
# repeaterList lookup location (rbook / artsci)
|
||||
repeaterLookup = rbook
|
||||
|
||||
# NOAA Hydrology unique identifiers, LID or USGS ID
|
||||
riverListDefault =
|
||||
|
||||
# EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2
|
||||
# FEMA IPAWS/CAP Alert Broadcast
|
||||
femaAlertBroadcastEnabled = False
|
||||
# FEMA IPAWS/CAP Alert Broadcast Channels
|
||||
femaAlertBroadcastCh = 2
|
||||
|
||||
# Goverment IPAWS/CAP Alert Broadcast
|
||||
eAlertBroadcastEnabled = False
|
||||
# Goverment Emergency IPAWS/CAP Alert Broadcast Channels
|
||||
eAlertBroadcastCh = 2
|
||||
# Ignore any headline that includes the word Test
|
||||
ignoreFEMAtest = True
|
||||
# comma separated list of codes (e.g., SAME,FIPS,ZIP) trigger local alert.
|
||||
# find your SAME https://www.weather.gov/nwr/counties
|
||||
mySAME = 053029,053073
|
||||
enableGBalerts = False
|
||||
|
||||
# Satalite Pass Prediction
|
||||
# Register for free API https://www.n2yo.com/login/
|
||||
n2yoAPIKey =
|
||||
# NORAD list https://www.n2yo.com/satellites/
|
||||
satList = 25544,7530
|
||||
|
||||
# repeater module
|
||||
[repeater]
|
||||
@@ -195,6 +186,36 @@ file_path = alert.txt
|
||||
broadcastCh = 2
|
||||
enable_read_news = False
|
||||
news_file_path = news.txt
|
||||
# only return a single random line from the news file
|
||||
news_random_line = False
|
||||
# enable the use of exernal shell commands
|
||||
enable_runShellCmd = False
|
||||
|
||||
[smtp]
|
||||
# enable or disable the SMTP module
|
||||
enableSMTP = False
|
||||
# enable or disable the IMAP module for inbound email
|
||||
enableImap = False
|
||||
# list of Sysop Emails seperate with commas
|
||||
sysopEmails =
|
||||
SMTP_SERVER = smtp.gmail.com
|
||||
# 587 SMTP over TLS/STARTTLS, 25 legacy SMTP, 465 SMTP over SSL
|
||||
SMTP_PORT = 587
|
||||
# Sender email: be mindful of public access, don't use your personal email
|
||||
FROM_EMAIL = none@gmail.com
|
||||
SMTP_AUTH = True
|
||||
SMTP_USERNAME = none@gmail.com
|
||||
SMTP_PASSWORD = none
|
||||
EMAIL_SUBJECT = Meshtastic✉️
|
||||
|
||||
# IMAP not implimented yet
|
||||
IMAP_SERVER = imap.gmail.com
|
||||
# 993 IMAP over TLS/SSL, 143 legacy IMAP
|
||||
IMAP_PORT = 993
|
||||
# IMAP login usually same as SMTP
|
||||
IMAP_USERNAME = none@gmail.com
|
||||
IMAP_PASSWORD = none
|
||||
IMAP_FOLDER = inbox
|
||||
|
||||
[games]
|
||||
# if hop limit for the user exceeds this value, the message will be dropped
|
||||
@@ -216,7 +237,7 @@ splitDelay = 0.0
|
||||
MESSAGE_CHUNK_SIZE = 160
|
||||
# Request Acknowledgement of message OTA
|
||||
wantAck = False
|
||||
# Max lilmit Buffer for radio testing
|
||||
# Max limit buffer for radio testing. 233 is hard limit 2.5+ firmware
|
||||
maxBuffer = 220
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
# instruction set the meshing-around docker container
|
||||
|
||||
# Substitute environment variables in the config file
|
||||
envsubst < /app/config.ini > /app/config.tmp && mv /app/config.tmp /app/config.ini
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# # Simulate meshing-around de K7MHI 2024
|
||||
from modules.log import * # Import the logger
|
||||
from modules.log import * # Import the logger; ### --> If you are reading this put the script in the project root <-- ###
|
||||
import time
|
||||
import random
|
||||
|
||||
|
||||
313
install.sh
313
install.sh
@@ -1,90 +1,157 @@
|
||||
#!/bin/bash
|
||||
# meshing-around install helper script
|
||||
|
||||
# install.sh
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
cp etc/pong_bot.tmp etc/pong_bot.service
|
||||
cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
|
||||
|
||||
printf "\n########################"
|
||||
printf "\nMeshing Around Installer\n"
|
||||
printf "\nThis script will install the Meshing Around bot and its dependencies works best in debian/ubuntu\n"
|
||||
printf "\nChecking for dependencies\n"
|
||||
printf "########################\n"
|
||||
printf "\nThis script will try and install the Meshing Around Bot and its dependencies."
|
||||
printf "Installer works best in raspian/debian/ubuntu, if there is a problem, try running the installer again.\n"
|
||||
printf "\nChecking for dependencies...\n"
|
||||
|
||||
# check if we are in /opt/meshing-around
|
||||
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 -iq "^y") ]]; then
|
||||
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
|
||||
fi
|
||||
fi
|
||||
|
||||
# check write access to program path
|
||||
if [[ ! -w ${program_path} ]]; then
|
||||
printf "\nInstall path not writable, try running the installer with sudo\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# if hostname = femtofox, then we are on embedded
|
||||
if [[ $(hostname) == "femtofox" ]]; then
|
||||
printf "\nDetected femtofox embedded system\n"
|
||||
embedded="y"
|
||||
else
|
||||
# check if running on embedded
|
||||
printf "\nAre You installing into an embedded system like a luckfox? (y/n)"
|
||||
read embedded
|
||||
fi
|
||||
|
||||
if [[ $(echo "${embedded}" | grep -i "^y") ]]; then
|
||||
printf "\nDetected embedded skipping dependency installation\n"
|
||||
else
|
||||
# Check and install dependencies
|
||||
if ! command -v python3 &> /dev/null
|
||||
then
|
||||
printf "python3 not found, trying 'apt-get install python3 python3-pip'\n"
|
||||
sudo apt-get install python3 python3-pip
|
||||
fi
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, trying 'apt-get install python3-pip'\n"
|
||||
sudo apt-get install python3-pip
|
||||
fi
|
||||
|
||||
# double check for python3 and pip
|
||||
if ! command -v python3 &> /dev/null
|
||||
then
|
||||
printf "python3 not found, please install python3 with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, please install pip with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
printf "\nDependencies installed\n"
|
||||
fi
|
||||
|
||||
# add user to groups for serial access
|
||||
printf "\nAdding user to dialout and tty groups for serial access\n"
|
||||
printf "\nAdding user to dialout, bluetooth, and tty groups for serial access\n"
|
||||
sudo usermod -a -G dialout $USER
|
||||
sudo usermod -a -G tty $USER
|
||||
sudo usermod -a -G bluetooth $USER
|
||||
|
||||
# check for pip
|
||||
if ! command -v pip &> /dev/null
|
||||
then
|
||||
printf "pip not found, please install pip with your OS\n"
|
||||
sudo apt-get install python3-pip
|
||||
else
|
||||
printf "python pip found\n"
|
||||
fi
|
||||
# copy service files
|
||||
cp etc/pong_bot.tmp etc/pong_bot.service
|
||||
cp etc/mesh_bot.tmp etc/mesh_bot.service
|
||||
cp etc/mesh_bot_reporting.tmp etc/mesh_bot_reporting.service
|
||||
|
||||
# generate config file, check if it exists
|
||||
if [ -f config.ini ]; then
|
||||
if [[ -f config.ini ]]; then
|
||||
printf "\nConfig file already exists, moving to backup config.old\n"
|
||||
mv config.ini config.old
|
||||
fi
|
||||
|
||||
cp config.template config.ini
|
||||
printf "\nConfig file generated\n"
|
||||
printf "\nConfig files generated!\n"
|
||||
|
||||
|
||||
# set virtual environment and install dependencies
|
||||
printf "\nMeshing Around Installer\n"
|
||||
|
||||
echo "Do you want to install the bot in a virtual environment? (y/n)"
|
||||
read venv
|
||||
|
||||
if [ $venv == "y" ]; then
|
||||
# set virtual environment
|
||||
if ! python3 -m venv --help &> /dev/null; then
|
||||
printf "Python3 venv module not found, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
else
|
||||
echo "Creating virtual environment..."
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
#check if python3 has venv module
|
||||
if [ -f venv/bin/activate ]; then
|
||||
printf "\nFpund virtual environment for python\n"
|
||||
else
|
||||
sudo apt-get install python3-venv
|
||||
printf "\nPython3 venv module not found, please install python3-venv with your OS if not already done. re-run the script\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# config service files for virtual environment
|
||||
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
|
||||
# install dependencies
|
||||
pip install -U -r requirements.txt
|
||||
fi
|
||||
# check if running on embedded
|
||||
if [[ $(echo "${embedded}" | grep -iq "^y") ]]; then
|
||||
printf "\nDetected embedded skipping venv\n"
|
||||
else
|
||||
printf "\nSkipping virtual environment...\n"
|
||||
# install dependencies
|
||||
printf "Are you on Raspberry Pi(debian/ubuntu)?\nshould we add --break-system-packages to the pip install command? (y/n)"
|
||||
read rpi
|
||||
if [ $rpi == "y" ]; then
|
||||
pip install -U -r requirements.txt --break-system-packages
|
||||
printf "\nRecomended install is in a python virtual environment, do you want to use venv? (y/n)"
|
||||
read venv
|
||||
|
||||
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
|
||||
# set virtual environment
|
||||
if ! python3 -m venv --help &> /dev/null; then
|
||||
printf "Python3/venv error, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
else
|
||||
echo "The Following could be messy, or take some time on slower devices."
|
||||
echo "Creating virtual environment..."
|
||||
#check if python3 has venv module
|
||||
if [[ -f venv/bin/activate ]]; then
|
||||
printf "\nFound virtual environment for python\n"
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
else
|
||||
printf "\nVirtual environment not found, trying `sudo apt-get install python3-venv`\n"
|
||||
sudo apt-get install python3-venv
|
||||
fi
|
||||
# create virtual environment
|
||||
python3 -m venv venv
|
||||
|
||||
# double check for python3-venv
|
||||
if [[ -f venv/bin/activate ]]; then
|
||||
printf "\nFound virtual environment for python\n"
|
||||
source venv/bin/activate
|
||||
else
|
||||
printf "\nPython3 venv module not found, please install python3-venv with your OS\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "\nVirtual environment created\n"
|
||||
|
||||
# config service files for virtual environment
|
||||
replace="s|python3 mesh_bot.py|/usr/bin/bash launch.sh mesh|g"
|
||||
sed -i "$replace" etc/mesh_bot.service
|
||||
replace="s|python3 pong_bot.py|/usr/bin/bash launch.sh pong|g"
|
||||
sed -i "$replace" etc/pong_bot.service
|
||||
|
||||
# install dependencies to venv
|
||||
pip install -U -r requirements.txt
|
||||
fi
|
||||
else
|
||||
pip install -U -r requirements.txt
|
||||
printf "\nSkipping virtual environment...\n"
|
||||
# install dependencies to system
|
||||
printf "Are you on Raspberry Pi(debian/ubuntu)?\nshould we add --break-system-packages to the pip install command? (y/n)"
|
||||
read rpi
|
||||
if [[ $(echo "${rpi}" | grep -i "^y") ]]; then
|
||||
pip install -U -r requirements.txt --break-system-packages
|
||||
else
|
||||
pip install -U -r requirements.txt
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\n\n"
|
||||
echo "Which bot do you want to install as a service? Pong Mesh or None? (pong/mesh/n)"
|
||||
echo "Pong bot is a simple bot for network testing, Mesh bot is a more complex bot more suited for meshing around"
|
||||
read bot
|
||||
|
||||
# set the correct path in the service file
|
||||
@@ -93,7 +160,32 @@ sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
# set the correct user in the service file?
|
||||
whoami=$(whoami)
|
||||
|
||||
#ask if we should add a user for the bot
|
||||
if [[ $(echo "${embedded}" | grep -i "^y") ]]; then
|
||||
printf "\nDo you want to add a local user (meshbot) no login, for the bot? (y/n)"
|
||||
read meshbotservice
|
||||
else
|
||||
meshbotservice="n"
|
||||
fi
|
||||
|
||||
if [[ $(echo "${meshbotservice}" | grep -i "^y") ]] || [[ $(echo "${embedded}" | grep -i "^y") ]]; then
|
||||
sudo useradd -M meshbot
|
||||
sudo usermod -L meshbot
|
||||
whoami="meshbot"
|
||||
echo "Added user meshbot with no home directory"
|
||||
sudo usermod -a -G dialout $whoami
|
||||
sudo usermod -a -G tty $whoami
|
||||
sudo usermod -a -G bluetooth $whoami
|
||||
echo "Added meshbot to dialout, tty, and bluetooth groups"
|
||||
|
||||
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"
|
||||
else
|
||||
whoami=$(whoami)
|
||||
fi
|
||||
# set the correct user in the service file
|
||||
replace="s|User=pi|User=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
@@ -102,58 +194,87 @@ replace="s|Group=pi|Group=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
sudo systemctl daemon-reload
|
||||
printf "\n service files updated\n"
|
||||
|
||||
# ask if emoji font should be installed for linux
|
||||
echo "Do you want to install the emoji font for debian/ubuntu linux? (y/n)"
|
||||
read emoji
|
||||
if [ $emoji == "y" ]; then
|
||||
sudo apt-get install -y fonts-noto-color-emoji
|
||||
echo "Emoji font installed!, reboot to load the font"
|
||||
fi
|
||||
|
||||
if [ $bot == "pong" ]; then
|
||||
if [[ $(echo "${bot}" | grep -i "^p") ]]; then
|
||||
# install service for pong bot
|
||||
sudo cp etc/pong_bot.service /etc/systemd/system/
|
||||
sudo systemctl enable pong_bot.service
|
||||
sudo systemctl daemon-reload
|
||||
echo "to start pong bot service: systemctl start pong_bot"
|
||||
fi
|
||||
|
||||
if [ $bot == "mesh" ]; then
|
||||
if [[ $(echo "${bot}" | grep -i "^m") ]]; then
|
||||
# install service for mesh bot
|
||||
sudo cp etc/mesh_bot.service /etc/systemd/system/
|
||||
sudo systemctl enable mesh_bot.service
|
||||
sudo systemctl daemon-reload
|
||||
echo "to start mesh bot service: systemctl start mesh_bot"
|
||||
fi
|
||||
|
||||
if [ $bot == "n" ]; then
|
||||
if [ -f launch.sh ]; then
|
||||
printf "\nTo run the bot, use the command: ./launch.sh\n"
|
||||
./launch.sh
|
||||
# check if running on embedded for final steps
|
||||
if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
# ask if emoji font should be installed for linux
|
||||
printf "\nDo you want to install the emoji font for debian/ubuntu linux? (y/n)"
|
||||
read emoji
|
||||
if [[ $(echo "${emoji}" | grep -i "^y") ]]; then
|
||||
sudo apt-get install -y fonts-noto-color-emoji
|
||||
echo "Emoji font installed!, reboot to load the font"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf "\nOptionally if you want to install the LLM Ollama compnents we will execute the following commands\n"
|
||||
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
|
||||
printf "\nOptionally if you want to install the multi gig LLM Ollama compnents we will execute the following commands\n"
|
||||
printf "\ncurl -fsSL https://ollama.com/install.sh | sh\n"
|
||||
printf "ollama pull gemma2:2b\n"
|
||||
printf "Total download is multi GB, recomend pi5/8GB or better for this\n"
|
||||
# ask if the user wants to install the LLM Ollama components
|
||||
printf "\nDo you want to install the LLM Ollama components? (y/n)"
|
||||
read ollama
|
||||
if [[ $(echo "${ollama}" | grep -i "^y") ]]; then
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# ask if the user wants to install the LLM Ollama components
|
||||
echo "Do you want to install the LLM Ollama components? (y/n)"
|
||||
read ollama
|
||||
if [ $ollama == "y" ]; then
|
||||
curl -fsSL https://ollama.com/install.sh | sh
|
||||
|
||||
# ask if want to install gemma2:2b
|
||||
printf "\n Ollama install done now we can install the Gemma2:2b components, multi GB download\n"
|
||||
echo "Do you want to install the Gemma2:2b components? (y/n)"
|
||||
read gemma
|
||||
if [ $gemma == "y" ]; then
|
||||
ollama pull gemma2:2b
|
||||
# ask if want to install gemma2:2b
|
||||
printf "\n Ollama install done now we can install the Gemma2:2b components\n"
|
||||
echo "Do you want to install the Gemma2:2b components? (y/n)"
|
||||
read gemma
|
||||
if [[ $(echo "${gemma}" | grep -i "^y") ]]; then
|
||||
ollama pull gemma2:2b
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
|
||||
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n"
|
||||
fi
|
||||
|
||||
printf "\nGood time to reboot? (y/n)"
|
||||
read reboot
|
||||
if [[ $(echo "${reboot}" | grep -i "^y") ]]; then
|
||||
sudo reboot
|
||||
fi
|
||||
else
|
||||
# we are on embedded
|
||||
# replace "type = serial" with "type = tcp" in config.ini
|
||||
replace="s|type = serial|type = tcp|g"
|
||||
sed -i "$replace" config.ini
|
||||
# replace "# hostname = 192.168.0.1" with "hostname = localhost" in config.ini
|
||||
replace="s|# hostname = localhost|hostname = localhost|g"
|
||||
sed -i "$replace" config.ini
|
||||
printf "\nConfig file updated for embedded\n"
|
||||
|
||||
# Set up the meshing around service
|
||||
printf "To install the meshing around service and keep notes, copy and paste the following commands:\n\n"
|
||||
printf "sudo cp /opt/meshing-around/meshing-around.service /etc/systemd/system/meshing-around.service\n"
|
||||
printf "sudo systemctl daemon-reload\n"
|
||||
printf "sudo systemctl enable meshing-around.service\n"
|
||||
printf "sudo systemctl start meshing-around.service\n"
|
||||
printf "sudo systemctl status meshing-around.service\n\n"
|
||||
printf "To see logs and stop the service:\n"
|
||||
printf "sudo journalctl -u meshing-around.service\n"
|
||||
printf "sudo systemctl stop meshing-around.service\n"
|
||||
printf "sudo systemctl disable meshing-around.service\n"
|
||||
fi
|
||||
|
||||
echo "Good time to reboot? (y/n)"
|
||||
read reboot
|
||||
if [ $reboot == "y" ]; then
|
||||
sudo reboot
|
||||
fi
|
||||
|
||||
printf "\nInstallation complete!\n"
|
||||
exit 0
|
||||
# after install shenannigans
|
||||
# add 'bee = True' to config.ini General section. You will likley want to clean the txt up a bit
|
||||
# wget https://courses.cs.washington.edu/courses/cse163/20wi/files/lectures/L04/bee-movie.txt -O bee.txt
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
# This script launches the meshing-around bot or the report generator in python virtual environment
|
||||
|
||||
# launch.sh
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
180
mesh_bot.py
180
mesh_bot.py
@@ -5,7 +5,7 @@
|
||||
import asyncio
|
||||
import time # for sleep, get some when you can :)
|
||||
import random
|
||||
from pubsub import pub # pip install pubsub
|
||||
from pubsub import pub # pip install pubsub, use launch.sh for venv
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
@@ -14,8 +14,6 @@ restrictedCommands = ["blackjack", "videopoker", "dopewars", "lemonstand", "golf
|
||||
restrictedResponse = "🤖only available in a Direct Message📵" # "" for none
|
||||
|
||||
# Global Variables
|
||||
cmdHistory = [] # list to hold the last commands
|
||||
seenNodes = [] # list to hold the last seen nodes
|
||||
DEBUGpacket = False # Debug print the packet rx
|
||||
DEBUGhops = False # Debug print hop info and bad hop count packets
|
||||
|
||||
@@ -25,7 +23,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
message_lower = message.lower()
|
||||
bot_response = "🤖I'm sorry, I'm afraid I can't do that."
|
||||
|
||||
# Command List
|
||||
# Command List processes system.trap_list. system.messageTrap() sends any commands to here
|
||||
default_commands = {
|
||||
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"ask:": lambda: handle_llm(message_from_id, channel_number, deviceID, message, publicChannel),
|
||||
@@ -45,8 +43,8 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"dopewars": lambda: handleDopeWars(message, message_from_id, deviceID),
|
||||
"ea": lambda: handle_fema_alerts(message, message_from_id, deviceID),
|
||||
"ealert": lambda: handle_fema_alerts(message, message_from_id, deviceID),
|
||||
"ea": lambda: handle_emergency_alerts(message, message_from_id, deviceID),
|
||||
"ealert": lambda: handle_emergency_alerts(message, message_from_id, deviceID),
|
||||
"email:": lambda: handle_email(message_from_id, message),
|
||||
"games": lambda: gamesCmdList,
|
||||
"globalthermonuclearwar": lambda: handle_gTnW(),
|
||||
@@ -64,19 +62,23 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"pinging": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"pong": lambda: "🏓PING!!🛜",
|
||||
"readnews": lambda: read_news(),
|
||||
"riverflow": lambda: handle_riverFlow(message, message_from_id, deviceID),
|
||||
"rlist": lambda: handle_repeaterQuery(message_from_id, deviceID, channel_number),
|
||||
"satpass": lambda: handle_satpass(message_from_id, deviceID, channel_number, message),
|
||||
"setemail": lambda: handle_email(message_from_id, message),
|
||||
"setsms": lambda: handle_sms( message_from_id, message),
|
||||
"sitrep": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
|
||||
"sms:": lambda: handle_sms(message_from_id, message),
|
||||
"solar": lambda: drap_xray_conditions() + "\n" + solar_conditions(),
|
||||
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
|
||||
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID),
|
||||
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"tide": lambda: handle_tide(message_from_id, deviceID, channel_number),
|
||||
"videopoker": lambda: handleVideoPoker(message, message_from_id, deviceID),
|
||||
"whereami": lambda: handle_whereami(message_from_id, deviceID, channel_number),
|
||||
"whoami": lambda: handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus),
|
||||
"whois": lambda: handle_whois(message, deviceID, channel_number, message_from_id),
|
||||
"wiki:": lambda: handle_wiki(message, isDM),
|
||||
"wiki?": lambda: handle_wiki(message, isDM),
|
||||
"wx": lambda: handle_wxc(message_from_id, deviceID, 'wx'),
|
||||
@@ -85,6 +87,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"wxc": lambda: handle_wxc(message_from_id, deviceID, 'wxc'),
|
||||
"📍": lambda: handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus),
|
||||
"🔔": lambda: handle_alertBell(message_from_id, deviceID, message),
|
||||
"🐝": lambda: read_file("bee.txt", True),
|
||||
# any value from system.py:trap_list_emergency will trigger the emergency function
|
||||
"112": lambda: handle_emergency(message_from_id, deviceID, message),
|
||||
"911": lambda: handle_emergency(message_from_id, deviceID, message),
|
||||
@@ -170,6 +173,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
msg = msg + " #" + message.split("#")[1]
|
||||
type = type + " #" + message.split("#")[1]
|
||||
|
||||
|
||||
# check for multi ping request
|
||||
if " " in message:
|
||||
# if stop multi ping
|
||||
@@ -179,6 +183,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
multiPingList.pop(i)
|
||||
msg = "🛑 auto-ping"
|
||||
|
||||
|
||||
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
|
||||
if len(multiPingList) > 2:
|
||||
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
|
||||
@@ -189,6 +194,10 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
pingCount = int(message.split(" ")[1])
|
||||
if pingCount == 123 or pingCount == 1234:
|
||||
pingCount = 1
|
||||
elif not autoPingInChannel and not isDM:
|
||||
# no autoping in channels
|
||||
pingCount = 1
|
||||
|
||||
if pingCount > 51:
|
||||
pingCount = 50
|
||||
except:
|
||||
@@ -200,6 +209,10 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
|
||||
else:
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
|
||||
# if not a DM add the username to the beginning of msg
|
||||
if not useDMForResponse and not isDM:
|
||||
msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg
|
||||
|
||||
return msg
|
||||
|
||||
@@ -208,6 +221,11 @@ def handle_alertBell(message_from_id, deviceID, message):
|
||||
return random.choice(msg)
|
||||
|
||||
def handle_emergency(message_from_id, deviceID, message):
|
||||
# if user in bbs_ban_list return
|
||||
if str(message_from_id) in bbs_ban_list:
|
||||
# silent discard
|
||||
logger.warning(f"System: {message_from_id} on spam list, no emergency responder alert sent")
|
||||
return ''
|
||||
# trgger alert to emergency_responder_alert_channel
|
||||
if message_from_id != 0:
|
||||
if deviceID == 1: rxNode = myNodeNum1
|
||||
@@ -265,9 +283,9 @@ def handle_wxalert(message_from_id, deviceID, message):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if "wxalert" in message:
|
||||
# Detailed weather alert
|
||||
weatherAlert = getActiveWeatherAlertsDetail(str(location[0]), str(location[1]))
|
||||
weatherAlert = getActiveWeatherAlertsDetailNOAA(str(location[0]), str(location[1]))
|
||||
else:
|
||||
weatherAlert = getWeatherAlerts(str(location[0]), str(location[1]))
|
||||
weatherAlert = getWeatherAlertsNOAA(str(location[0]), str(location[1]))
|
||||
|
||||
if NO_ALERTS not in weatherAlert:
|
||||
weatherAlert = weatherAlert[0]
|
||||
@@ -289,6 +307,34 @@ llmRunCounter = 0
|
||||
llmTotalRuntime = []
|
||||
llmLocationTable = [{'nodeID': 1234567890, 'location': 'No Location'},]
|
||||
|
||||
def handle_satpass(message_from_id, deviceID, channel_number, message):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
passes = ''
|
||||
satList = satListConfig
|
||||
message = message.lower()
|
||||
|
||||
# if user has a NORAD ID in the message
|
||||
if "satpass " in message:
|
||||
try:
|
||||
userList = message.split("satpass ")[1].split(" ")[0]
|
||||
#split userList and make into satList overrided the config.ini satList
|
||||
satList = userList.split(",")
|
||||
except:
|
||||
return "example use:🛰️satpass 25544,33591"
|
||||
|
||||
# Detailed satellite pass
|
||||
for bird in satList:
|
||||
satPass = getNextSatellitePass(bird, str(location[0]), str(location[1]))
|
||||
if satPass:
|
||||
# append to passes
|
||||
passes = passes + satPass + "\n"
|
||||
# remove the last newline
|
||||
passes = passes[:-1]
|
||||
|
||||
if passes == '':
|
||||
passes = "No 🛰️ anytime soon"
|
||||
return passes
|
||||
|
||||
def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel):
|
||||
global llmRunCounter, llmLocationTable, llmTotalRuntime, cmdHistory
|
||||
location_name = 'no location provided'
|
||||
@@ -601,29 +647,56 @@ def handleGolf(message, nodeID, deviceID):
|
||||
time.sleep(responseDelay + 1)
|
||||
return msg
|
||||
|
||||
def handle_riverFlow(message, message_from_id, deviceID):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
userRiver = message.lower()
|
||||
|
||||
if "riverflow " in userRiver:
|
||||
userRiver = userRiver.split("riverflow ")[1] if "riverflow " in userRiver else riverListDefault
|
||||
else:
|
||||
userRiver = userRiver.split(",") if "," in userRiver else riverListDefault
|
||||
|
||||
# return river flow data
|
||||
if use_meteo_wxApi:
|
||||
return get_flood_openmeteo(location[0], location[1])
|
||||
else:
|
||||
# if userRiver a list
|
||||
if type(userRiver) == list:
|
||||
msg = ""
|
||||
for river in userRiver:
|
||||
msg += get_flood_noaa(location[0], location[1], river)
|
||||
return msg
|
||||
# if single river
|
||||
msg = get_flood_noaa(location[0], location[1], userRiver)
|
||||
return msg
|
||||
|
||||
|
||||
def handle_wxc(message_from_id, deviceID, cmd):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if use_meteo_wxApi and not "wxc" in cmd and not use_metric:
|
||||
logger.debug("System: Bot Returning Open-Meteo API for weather imperial")
|
||||
#logger.debug("System: Bot Returning Open-Meteo API for weather imperial")
|
||||
weather = get_wx_meteo(str(location[0]), str(location[1]))
|
||||
elif use_meteo_wxApi:
|
||||
logger.debug("System: Bot Returning Open-Meteo API for weather metric")
|
||||
#logger.debug("System: Bot Returning Open-Meteo API for weather metric")
|
||||
weather = get_wx_meteo(str(location[0]), str(location[1]), 1)
|
||||
elif not use_meteo_wxApi and "wxc" in cmd or use_metric:
|
||||
logger.debug("System: Bot Returning NOAA API for weather metric")
|
||||
weather = get_weather(str(location[0]), str(location[1]), 1)
|
||||
#logger.debug("System: Bot Returning NOAA API for weather metric")
|
||||
weather = get_NOAAweather(str(location[0]), str(location[1]), 1)
|
||||
else:
|
||||
logger.debug("System: Bot Returning NOAA API for weather imperial")
|
||||
weather = get_weather(str(location[0]), str(location[1]))
|
||||
#logger.debug("System: Bot Returning NOAA API for weather imperial")
|
||||
weather = get_NOAAweather(str(location[0]), str(location[1]))
|
||||
return weather
|
||||
|
||||
def handle_fema_alerts(message, message_from_id, deviceID):
|
||||
def handle_emergency_alerts(message, message_from_id, deviceID):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if enableGBalerts:
|
||||
# UK Alerts
|
||||
return get_govUK_alerts(str(location[0]), str(location[1]))
|
||||
if message.lower().startswith("ealert"):
|
||||
# Detailed alert
|
||||
# Detailed alert FEMA
|
||||
return getIpawsAlert(str(location[0]), str(location[1]))
|
||||
else:
|
||||
# Headlines only
|
||||
# Headlines only FEMA
|
||||
return getIpawsAlert(str(location[0]), str(location[1]), shortAlerts=True)
|
||||
|
||||
def handle_bbspost(message, message_from_id, deviceID):
|
||||
@@ -692,6 +765,16 @@ def handle_sun(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_sun(str(location[0]), str(location[1]))
|
||||
|
||||
def sysinfo(message, message_from_id, deviceID):
|
||||
if "?" in message:
|
||||
return "sysinfo command returns system information."
|
||||
else:
|
||||
if enable_runShellCmd and file_monitor_enabled:
|
||||
shellData = call_external_script(None, "sysEnv.sh").rstrip()
|
||||
return get_sysinfo(message_from_id, deviceID) + "\n" + shellData
|
||||
else:
|
||||
return get_sysinfo(message_from_id, deviceID)
|
||||
|
||||
def handle_lheard(message, nodeid, deviceID, isDM):
|
||||
if "?" in message and isDM:
|
||||
return message.split("?")[0].title() + " command returns a list of the nodes that have been heard recently"
|
||||
@@ -782,7 +865,7 @@ def handle_repeaterQuery(message_from_id, deviceID, channel_number):
|
||||
|
||||
def handle_tide(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_tide(str(location[0]), str(location[1]))
|
||||
return get_NOAAtide(str(location[0]), str(location[1]))
|
||||
|
||||
def handle_moon(message_from_id, deviceID, channel_number):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
@@ -813,6 +896,46 @@ def handle_whoami(message_from_id, deviceID, hop, snr, rssi, pkiStatus):
|
||||
msg = "Error in whoami"
|
||||
return msg
|
||||
|
||||
def handle_whois(message, deviceID, channel_number, message_from_id):
|
||||
#return data on a node name or number
|
||||
if "?" in message:
|
||||
return message.split("?")[0].title() + " command returns information on a node."
|
||||
else:
|
||||
# get the nodeID from the message
|
||||
msg = ''
|
||||
node = ''
|
||||
# find the requested node in db
|
||||
if " " in message:
|
||||
node = message.split(" ")[1]
|
||||
if node.startswith("!") and len(node) == 9:
|
||||
# mesh !hex
|
||||
try:
|
||||
node = int(node.strip("!"),16)
|
||||
except ValueError as e:
|
||||
node = 0
|
||||
elif node.isalpha() or not node.isnumeric():
|
||||
# try short name
|
||||
node = get_num_from_short_name(node, deviceID)
|
||||
|
||||
# get details on the node
|
||||
for i in range(len(seenNodes)):
|
||||
if seenNodes[i]['nodeID'] == int(node):
|
||||
msg = f"Node: {seenNodes[i]['nodeID']} is {get_name_from_number(seenNodes[i]['nodeID'], 'long', deviceID)}\n"
|
||||
msg += f"Last 👀: {time.ctime(seenNodes[i]['lastSeen'])} "
|
||||
break
|
||||
|
||||
if msg == '':
|
||||
msg = "Provide a valid node number or short name"
|
||||
else:
|
||||
# if the user is an admin show the channel and interface and location
|
||||
if str(message_from_id) in bbs_admin_list:
|
||||
location = get_node_location(seenNodes[i]['nodeID'], deviceID, channel_number)
|
||||
msg += f"Ch: {seenNodes[i]['channel']}, Int: {seenNodes[i]['rxInterface']}"
|
||||
msg += f"Lat: {location[0]}, Lon: {location[1]}\n"
|
||||
if location != [latitudeValue, longitudeValue]:
|
||||
msg += f"Loc: {where_am_i(str(location[0]), str(location[1]))}"
|
||||
return msg
|
||||
|
||||
def check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func):
|
||||
global llm_enabled
|
||||
|
||||
@@ -894,7 +1017,7 @@ def onReceive(packet, interface):
|
||||
rxNode = 1
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
rxNode = 2
|
||||
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
@@ -924,6 +1047,10 @@ def onReceive(packet, interface):
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
|
||||
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
|
||||
|
||||
# get the signal strength and snr if available
|
||||
if packet.get('rxSnr') or packet.get('rxRssi'):
|
||||
snr = packet.get('rxSnr', 0)
|
||||
@@ -964,6 +1091,9 @@ def onReceive(packet, interface):
|
||||
if hop_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
elif hop_start == 0 and hop_limit > 0:
|
||||
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:
|
||||
@@ -1092,7 +1222,7 @@ def onReceive(packet, interface):
|
||||
send_message(rMsg, channel_number, 0, 1)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode)
|
||||
consumeMetadata(packet, rxNode)
|
||||
except KeyError as e:
|
||||
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
|
||||
logger.debug(f"System: Error Packet = {packet}")
|
||||
@@ -1149,10 +1279,16 @@ async def start_rx():
|
||||
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
|
||||
if file_monitor_enabled:
|
||||
logger.debug(f"System: File Monitor Enabled for {file_monitor_file_path}, broadcasting to channels: {file_monitor_broadcastCh}")
|
||||
if read_news_enabled:
|
||||
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
|
||||
if enable_runShellCmd:
|
||||
logger.debug(f"System: Shell Command monitor enabled")
|
||||
if read_news_enabled:
|
||||
logger.debug(f"System: File Monitor News Reader Enabled for {news_file_path}")
|
||||
if bee_enabled:
|
||||
logger.debug(f"System: File Monitor Bee Monitor Enabled for bee.txt")
|
||||
if wxAlertBroadcastEnabled:
|
||||
logger.debug(f"System: Weather Alert Broadcast Enabled on channels {wxAlertBroadcastChannel}")
|
||||
if emergencyAlertBrodcastEnabled:
|
||||
logger.debug(f"System: Emergency Alert Broadcast Enabled on channels {emergencyAlertBroadcastCh}")
|
||||
if emergency_responder_enabled:
|
||||
logger.debug(f"System: Emergency Responder Enabled on channels {emergency_responder_alert_channel} for interface {emergency_responder_alert_interface}")
|
||||
if enableSMTP:
|
||||
|
||||
@@ -3,22 +3,36 @@
|
||||
|
||||
from modules.log import *
|
||||
import asyncio
|
||||
import random
|
||||
import os
|
||||
|
||||
trap_list_filemon = ("readnews",)
|
||||
|
||||
def read_file(file_monitor_file_path):
|
||||
def read_file(file_monitor_file_path, random_line_only=False):
|
||||
|
||||
try:
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
content = f.read()
|
||||
return content
|
||||
if not os.path.exists(file_monitor_file_path):
|
||||
logger.warning(f"FileMon: File not found: {file_monitor_file_path}")
|
||||
if file_monitor_file_path == "bee.txt":
|
||||
return "🐝buzz 💐buzz buzz🍯"
|
||||
if random_line_only:
|
||||
# read a random line from the file
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
return random.choice(lines)
|
||||
else:
|
||||
# read the whole file
|
||||
with open(file_monitor_file_path, 'r') as f:
|
||||
content = f.read()
|
||||
return content
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error reading file: {file_monitor_file_path}")
|
||||
return None
|
||||
|
||||
def read_news():
|
||||
# read the news file on demand
|
||||
return read_file(news_file_path)
|
||||
return read_file(news_file_path, read_news_enabled)
|
||||
|
||||
|
||||
def write_news(content, append=False):
|
||||
# write the news file on demand
|
||||
@@ -47,4 +61,24 @@ async def watch_file():
|
||||
content = content.replace('\n', ' ').replace('\r', '').strip()
|
||||
if content:
|
||||
return content
|
||||
await asyncio.sleep(1) # Check every
|
||||
await asyncio.sleep(1) # Check every
|
||||
|
||||
def call_external_script(message, script="runShell.sh"):
|
||||
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
|
||||
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()
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error calling external script: {e}")
|
||||
return None
|
||||
|
||||
60
modules/globalalert.py
Normal file
60
modules/globalalert.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# helper functions to use location data for data outside US/north america
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import json # pip install json
|
||||
from geopy.geocoders import Nominatim # pip install geopy
|
||||
import maidenhead as mh # pip install maidenhead
|
||||
import requests # pip install requests
|
||||
import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from modules.log import *
|
||||
|
||||
trap_list_location_eu = ("ukalert", "ukwx", "ukflood")
|
||||
|
||||
def get_govUK_alerts(shortAlerts=False):
|
||||
try:
|
||||
# get UK.gov alerts
|
||||
url = 'https://www.gov.uk/alerts'
|
||||
response = requests.get(url)
|
||||
soup = bs.BeautifulSoup(response.text, 'html.parser')
|
||||
# the alerts are in <h2 class="govuk-heading-m" id="alert-status">
|
||||
alert = soup.find('h2', class_='govuk-heading-m', id='alert-status')
|
||||
except Exception as e:
|
||||
logger.warning("Error getting UK alerts: " + str(e))
|
||||
return NO_ALERTS
|
||||
|
||||
if alert:
|
||||
return "🚨" + alert.get_text(strip=True)
|
||||
else:
|
||||
return NO_ALERTS
|
||||
|
||||
|
||||
def get_wxUKgov():
|
||||
# get UK weather warnings
|
||||
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
|
||||
url = 'https://www.metoffice.gov.uk/public/data/PWSCache/WarningsRSS/Region/nw'
|
||||
try:
|
||||
# get UK weather warnings
|
||||
url = 'https://www.metoffice.gov.uk/weather/guides/rss'
|
||||
response = requests.get(url)
|
||||
soup = bs.BeautifulSoup(response.content, 'xml')
|
||||
|
||||
items = soup.find_all('item')
|
||||
alerts = []
|
||||
|
||||
for item in items:
|
||||
title = item.find('title').get_text(strip=True)
|
||||
description = item.find('description').get_text(strip=True)
|
||||
alerts.append(f"🚨 {title}: {description}")
|
||||
|
||||
return "\n".join(alerts) if alerts else NO_ALERTS
|
||||
except Exception as e:
|
||||
logger.warning("Error getting UK weather warnings: " + str(e))
|
||||
return NO_ALERTS
|
||||
|
||||
|
||||
def get_floodUKgov():
|
||||
# get UK flood warnings
|
||||
url = 'https://environment.data.gov.uk/flood-widgets/rss/feed-England.xml'
|
||||
|
||||
return NO_ALERTS
|
||||
73
modules/gpio.py
Normal file
73
modules/gpio.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# GPIO module for MeshLink, concept code, not implemented
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
# https://pypi.org/project/gpio/
|
||||
#import gpio
|
||||
|
||||
# https://pythonhosted.org/RPIO/
|
||||
import RPIO
|
||||
|
||||
from modules.log import *
|
||||
trap_list_gpio = ("gpio", "pin", "relay", "switch", "pwm")
|
||||
|
||||
# set up input channel without pull-up
|
||||
RPIO.setup(7, RPIO.IN)
|
||||
|
||||
# set up input channel with pull-up
|
||||
RPIO.setup(8, RPIO.IN, pull_up_down=RPIO.PUD_UP)
|
||||
|
||||
# set up GPIO output channel
|
||||
RPIO.setup(8, RPIO.OUT)
|
||||
|
||||
# change to BOARD numbering schema
|
||||
RPIO.setmode(RPIO.BOARD)
|
||||
|
||||
# set up PWM channel
|
||||
RPIO.setup(12, RPIO.OUT)
|
||||
p = RPIO.PWM(12)
|
||||
|
||||
def gpio_status():
|
||||
# get status of GPIO pins
|
||||
gpio_status = ""
|
||||
gpio_status += "GPIO 7: " + str(RPIO.input(7)) + "\n"
|
||||
gpio_status += "GPIO 8: " + str(RPIO.input(8)) + "\n"
|
||||
gpio_status += "GPIO 12: " + str(RPIO.input(12)) + "\n"
|
||||
return gpio_status
|
||||
|
||||
def gpio_toggle():
|
||||
# toggle GPIO pin 8
|
||||
RPIO.output(8, not RPIO.input(8))
|
||||
return "GPIO 8 toggled"
|
||||
|
||||
def gpio_pwm():
|
||||
# set PWM on GPIO pin 12
|
||||
p.start(50)
|
||||
return "PWM started"
|
||||
|
||||
def gpio_stop():
|
||||
# stop PWM on GPIO pin 12
|
||||
p.stop()
|
||||
return "PWM stopped"
|
||||
|
||||
def gpio_shutdown():
|
||||
# shutdown GPIO
|
||||
RPIO.cleanup()
|
||||
return "GPIO shutdown"
|
||||
|
||||
def trap_gpio(message):
|
||||
# trap for GPIO commands
|
||||
if "status" in message:
|
||||
return gpio_status()
|
||||
elif "toggle" in message:
|
||||
return gpio_toggle()
|
||||
elif "pwm" in message:
|
||||
return gpio_pwm()
|
||||
elif "stop" in message:
|
||||
return gpio_stop()
|
||||
elif "shutdown" in message:
|
||||
return gpio_shutdown()
|
||||
else:
|
||||
return "GPIO command not recognized"
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# helper functions to use location data like NOAA weather
|
||||
# helper functions to use location data for the API for NOAA weather, FEMA iPAWS, and repeater data
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import json # pip install json
|
||||
@@ -9,7 +9,7 @@ import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from modules.log import *
|
||||
|
||||
trap_list_location = ("whereami", "tide", "moon", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert")
|
||||
trap_list_location = ("whereami", "tide", "wx", "wxc", "wxa", "wxalert", "rlist", "ea", "ealert", "riverflow")
|
||||
|
||||
def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
whereIam = ""
|
||||
@@ -154,9 +154,8 @@ def getArtSciRepeaters(lat=0, lon=0):
|
||||
else:
|
||||
msg = f"no results.. sorry"
|
||||
return msg
|
||||
|
||||
|
||||
def get_tide(lat=0, lon=0):
|
||||
def get_NOAAtide(lat=0, lon=0):
|
||||
station_id = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.error("Location:No GPS data, try sending location for tide")
|
||||
@@ -172,7 +171,7 @@ def get_tide(lat=0, lon=0):
|
||||
|
||||
if station_json['stationList'] == [] or station_json['stationList'] is None:
|
||||
logger.error("Location:No tide station found")
|
||||
return ERROR_FETCHING_DATA
|
||||
return "No tide station found with info provided"
|
||||
|
||||
station_id = station_json['stationList'][0]['stationId']
|
||||
|
||||
@@ -219,7 +218,7 @@ def get_tide(lat=0, lon=0):
|
||||
tide_table = tide_table[:-1]
|
||||
return tide_table
|
||||
|
||||
def get_weather(lat=0, lon=0, unit=0):
|
||||
def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
# get weather report from NOAA for forecast detailed
|
||||
weather = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
@@ -263,7 +262,7 @@ def get_weather(lat=0, lon=0, unit=0):
|
||||
weather = weather[:-1]
|
||||
|
||||
# get any alerts and return the count
|
||||
alerts = getWeatherAlerts(lat, lon)
|
||||
alerts = getWeatherAlertsNOAA(lat, lon)
|
||||
|
||||
if alerts == ERROR_FETCHING_DATA or alerts == NO_DATA_NOGPS or alerts == NO_ALERTS:
|
||||
alert = ""
|
||||
@@ -333,7 +332,7 @@ def abbreviate_noaa(row):
|
||||
|
||||
return line
|
||||
|
||||
def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
|
||||
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 = ""
|
||||
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
|
||||
@@ -381,26 +380,27 @@ def getWeatherAlerts(lat=0, lon=0, useDefaultLatLon=False):
|
||||
data = "\n".join(alerts.split("\n")[:numWxAlerts]), alert_num
|
||||
return data
|
||||
|
||||
wxAlertCache = ""
|
||||
def alertBrodcast():
|
||||
wxAlertCacheNOAA = ""
|
||||
def alertBrodcastNOAA():
|
||||
# get the latest weather alerts and broadcast them if there are any
|
||||
global wxAlertCache
|
||||
currentAlert = getWeatherAlerts(latitudeValue, longitudeValue)
|
||||
global wxAlertCacheNOAA
|
||||
currentAlert = getWeatherAlertsNOAA(latitudeValue, longitudeValue)
|
||||
# check if any reason to discard the alerts
|
||||
if currentAlert == ERROR_FETCHING_DATA or currentAlert == NO_DATA_NOGPS:
|
||||
return False
|
||||
elif currentAlert == NO_ALERTS:
|
||||
wxAlertCache = ""
|
||||
wxAlertCacheNOAA = ""
|
||||
return False
|
||||
# broadcast the alerts send to wxBrodcastCh
|
||||
elif currentAlert[0] != wxAlertCache:
|
||||
elif currentAlert[0] not in wxAlertCacheNOAA:
|
||||
# Check if the current alert is not in the weather alert cache
|
||||
logger.debug("Location:Broadcasting weather alerts")
|
||||
wxAlertCache = currentAlert[0]
|
||||
wxAlertCacheNOAA = currentAlert[0]
|
||||
return currentAlert
|
||||
|
||||
return False
|
||||
|
||||
def getActiveWeatherAlertsDetail(lat=0, lon=0):
|
||||
def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
|
||||
# get the latest details of weather alerts from NOAA
|
||||
alerts = ""
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
@@ -457,7 +457,7 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
alerts = []
|
||||
|
||||
# set the API URL for IPAWS
|
||||
ipawsPIN = "000000"
|
||||
namespace = "urn:oasis:names:tc:emergency:cap:1.2"
|
||||
alert_url = "https://apps.fema.gov/IPAWSOPEN_EAS_SERVICE/rest/feed"
|
||||
if ipawsPIN != "000000":
|
||||
alert_url += "?pin=" + ipawsPIN
|
||||
@@ -482,6 +482,7 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
#pin check
|
||||
if ipawsPIN != "000000":
|
||||
link += "?pin=" + ipawsPIN
|
||||
# get the linked alert data from FEMA
|
||||
linked_data = requests.get(link, timeout=urlTimeoutSeconds)
|
||||
if not linked_data.ok:
|
||||
#logger.warning(f"System: iPAWS Error fetching linked alert data from {link}")
|
||||
@@ -495,21 +496,33 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
|
||||
for info in linked_xml.getElementsByTagName("info"):
|
||||
# extract values from XML
|
||||
eventCode_table = info.getElementsByTagName("eventCode")[0]
|
||||
alertType = eventCode_table.getElementsByTagName("valueName")[0].childNodes[0].nodeValue
|
||||
alertCode = eventCode_table.getElementsByTagName("value")[0].childNodes[0].nodeValue
|
||||
headline = info.getElementsByTagName("headline")[0].childNodes[0].nodeValue
|
||||
description = info.getElementsByTagName("description")[0].childNodes[0].nodeValue
|
||||
|
||||
area_table = info.getElementsByTagName("area")[0]
|
||||
areaDesc = area_table.getElementsByTagName("areaDesc")[0].childNodes[0].nodeValue
|
||||
|
||||
geocode_table = area_table.getElementsByTagName("geocode")[0]
|
||||
geocode_type = geocode_table.getElementsByTagName("valueName")[0].childNodes[0].nodeValue
|
||||
geocode_value = geocode_table.getElementsByTagName("value")[0].childNodes[0].nodeValue
|
||||
sameVal = "NONE"
|
||||
if geocode_type == "SAME":
|
||||
sameVal = geocode_value
|
||||
geocode_value = "NONE"
|
||||
description = ""
|
||||
try:
|
||||
eventCode_table = info.getElementsByTagName("eventCode")[0]
|
||||
alertType = eventCode_table.getElementsByTagName("valueName")[0].childNodes[0].nodeValue
|
||||
alertCode = eventCode_table.getElementsByTagName("value")[0].childNodes[0].nodeValue
|
||||
headline = info.getElementsByTagName("headline")[0].childNodes[0].nodeValue
|
||||
# use headline if no description
|
||||
if info.getElementsByTagName("description") and info.getElementsByTagName("description")[0].childNodes:
|
||||
description = info.getElementsByTagName("description")[0].childNodes[0].nodeValue
|
||||
else:
|
||||
logger.debug(f"System: report this to discord - iPAWS No description for alert: {headline}")
|
||||
description = headline
|
||||
|
||||
area_table = info.getElementsByTagName("area")[0]
|
||||
areaDesc = area_table.getElementsByTagName("areaDesc")[0].childNodes[0].nodeValue
|
||||
|
||||
geocode_table = area_table.getElementsByTagName("geocode")[0]
|
||||
geocode_type = geocode_table.getElementsByTagName("valueName")[0].childNodes[0].nodeValue
|
||||
geocode_value = geocode_table.getElementsByTagName("value")[0].childNodes[0].nodeValue
|
||||
if geocode_type == "SAME":
|
||||
sameVal = geocode_value
|
||||
except Exception as e:
|
||||
logger.warning(f"System: iPAWS Error extracting alert data: {link}")
|
||||
#print(f"DEBUG: {info.toprettyxml()}")
|
||||
continue
|
||||
|
||||
# check if the alert is for the current location, if wanted keep alert
|
||||
if (sameVal in mySAME) or (geocode_value in mySAME):
|
||||
@@ -529,9 +542,9 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
'geocode_value': geocode_value,
|
||||
'description': description
|
||||
})
|
||||
else:
|
||||
# these are discarded some day but logged for debugging currently
|
||||
logger.debug(f"Debug iPAWS: Type:{alertType} Code:{alertCode} Desc:{areaDesc} GeoType:{geocode_type} GeoVal:{geocode_value}, Headline:{headline}")
|
||||
# else:
|
||||
# # these are discarded some day but logged for debugging currently
|
||||
# logger.debug(f"Debug iPAWS: Type:{alertType} Code:{alertCode} Desc:{areaDesc} GeoType:{geocode_type} GeoVal:{geocode_value}, Headline:{headline}")
|
||||
|
||||
# return the numWxAlerts of alerts
|
||||
if len(alerts) > 0:
|
||||
@@ -548,3 +561,52 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
|
||||
return alert
|
||||
|
||||
def get_flood_noaa(lat=0, lon=0, uid=0):
|
||||
# get the latest flood alert from NOAA
|
||||
api_url = "https://api.water.noaa.gov/nwps/v1/gauges/"
|
||||
headers = {'accept': 'application/json'}
|
||||
if uid == 0:
|
||||
return "No flood gauge data found"
|
||||
try:
|
||||
response = requests.get(api_url + str(uid), headers=headers, timeout=urlTimeoutSeconds)
|
||||
if not response.ok:
|
||||
logger.warning("Location:Error fetching flood gauge data from NOAA for " + str(uid))
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching flood gauge data from NOAA for " + str(uid))
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
data = response.json()
|
||||
if not data:
|
||||
return "No flood gauge data found"
|
||||
|
||||
# extract values from JSON
|
||||
try:
|
||||
name = data['name']
|
||||
status_observed_primary = data['status']['observed']['primary']
|
||||
status_observed_primary_unit = data['status']['observed']['primaryUnit']
|
||||
status_observed_secondary = data['status']['observed']['secondary']
|
||||
status_observed_secondary_unit = data['status']['observed']['secondaryUnit']
|
||||
status_observed_floodCategory = data['status']['observed']['floodCategory']
|
||||
status_forecast_primary = data['status']['forecast']['primary']
|
||||
status_forecast_primary_unit = data['status']['forecast']['primaryUnit']
|
||||
status_forecast_secondary = data['status']['forecast']['secondary']
|
||||
status_forecast_secondary_unit = data['status']['forecast']['secondaryUnit']
|
||||
status_forecast_floodCategory = data['status']['forecast']['floodCategory']
|
||||
|
||||
# except KeyError as e:
|
||||
# print(f"Missing key in data: {e}")
|
||||
# except TypeError as e:
|
||||
# print(f"Type error in data: {e}")
|
||||
except Exception as e:
|
||||
logger.warning("Location:Error extracting flood gauge data from NOAA for " + str(uid))
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# format the flood data
|
||||
logger.debug(f"System: NOAA Flood data for {str(uid)}")
|
||||
flood_data = f"Flood Data {name}:\n"
|
||||
flood_data += f"Observed: {status_observed_primary}{status_observed_primary_unit}({status_observed_secondary}{status_observed_secondary_unit}) risk: {status_observed_floodCategory}"
|
||||
flood_data += f"\nForecast: {status_forecast_primary}{status_forecast_primary_unit}({status_forecast_secondary}{status_forecast_secondary_unit}) risk: {status_forecast_floodCategory}"
|
||||
|
||||
return flood_data
|
||||
|
||||
|
||||
@@ -74,4 +74,22 @@ if log_messages_to_file:
|
||||
file_handler = TimedRotatingFileHandler('logs/messages.log', when='midnight', backupCount=log_backup_count)
|
||||
file_handler.setLevel(logging.INFO) # INFO used for messages to disk
|
||||
file_handler.setFormatter(logging.Formatter(msgLogFormat))
|
||||
msgLogger.addHandler(file_handler)
|
||||
msgLogger.addHandler(file_handler)
|
||||
|
||||
# Pretty Timestamp
|
||||
def getPrettyTime(seconds):
|
||||
# convert unix time to minutes, hours, or days, or years for simple display
|
||||
designator = "s"
|
||||
if seconds > 0:
|
||||
seconds = round(seconds / 60)
|
||||
designator = "m"
|
||||
if seconds > 60:
|
||||
seconds = round(seconds / 60)
|
||||
designator = "h"
|
||||
if seconds > 24:
|
||||
seconds = round(seconds / 24)
|
||||
designator = "d"
|
||||
if seconds > 365:
|
||||
seconds = round(seconds / 365)
|
||||
designator = "y"
|
||||
return str(seconds) + designator
|
||||
@@ -28,6 +28,8 @@ retry_int2 = False
|
||||
wiki_return_limit = 3 # limit the number of sentences returned off the first paragraph first hit
|
||||
playingGame = False
|
||||
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
|
||||
cmdHistory = [] # list to hold the last commands
|
||||
seenNodes = [] # list to hold the last seen nodes
|
||||
|
||||
# Read the config file, if it does not exist, create basic config file
|
||||
config = configparser.ConfigParser()
|
||||
@@ -122,11 +124,13 @@ try:
|
||||
welcome_message = (f"{welcome_message}").replace('\\n', '\n') # allow for newlines in the welcome message
|
||||
motd_enabled = config['general'].getboolean('motdEnabled', True)
|
||||
MOTD = config['general'].get('motd', MOTD)
|
||||
autoPingInChannel = config['general'].getboolean('autoPingInChannel', False)
|
||||
enableCmdHistory = config['general'].getboolean('enableCmdHistory', True)
|
||||
lheardCmdIgnoreNode = config['general'].get('lheardCmdIgnoreNode', '').split(',')
|
||||
whoami_enabled = config['general'].getboolean('whoami', True)
|
||||
dad_jokes_enabled = config['general'].getboolean('DadJokes', False)
|
||||
dad_jokes_emojiJokes = config['general'].getboolean('DadJokesEmoji', False)
|
||||
bee_enabled = config['general'].getboolean('bee', False) # 🐝 off by default undocumented
|
||||
solar_conditions_enabled = config['general'].getboolean('spaceWeather', True)
|
||||
wikipedia_enabled = config['general'].getboolean('wikipedia', False)
|
||||
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
|
||||
@@ -153,22 +157,22 @@ try:
|
||||
longitudeValue = config['location'].getfloat('lon', -123.0)
|
||||
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
|
||||
n2yoAPIKey = config['location'].get('n2yoAPIKey', '') # default empty
|
||||
satListConfig = config['location'].get('satList', '25544').split(',') # default 25544 ISS
|
||||
riverListDefault = config['location'].get('riverList', '').split(',') # default 12061500 Skagit River
|
||||
# location alerts
|
||||
emergencyAlertBrodcastEnabled = config['location'].getboolean('eAlertBroadcastEnabled', False) # default False
|
||||
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
|
||||
enableGBalerts = config['location'].getboolean('enableGBalerts', False) # default False
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
|
||||
mySAME = config['location'].get('mySAME', '').split(',') # default empty
|
||||
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
|
||||
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True not enabled yet
|
||||
repeater_lookup = config['location'].get('repeaterLookup', 'rbook') # default repeater lookup source
|
||||
mySAME = config['location'].get('mySAME', '').split(',') # default empty
|
||||
femaAlertBroadcastEnabled = config['location'].getboolean('femaAlertBroadcastEnabled', False) # default False
|
||||
femaAlertBroadcastCh = config['location'].get('femaAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
|
||||
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
|
||||
ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True
|
||||
# brodcast channel for weather alerts
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh')
|
||||
if wxAlertBroadcastChannel:
|
||||
if ',' in wxAlertBroadcastChannel:
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh').split(',')
|
||||
else:
|
||||
wxAlertBroadcastChannel = config['location'].getint('wxAlertBroadcastCh', 2) # default 2
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
emergencyAlertBroadcastCh = config['location'].get('eAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
|
||||
# bbs
|
||||
bbs_enabled = config['bbs'].getboolean('enabled', False)
|
||||
@@ -217,6 +221,8 @@ try:
|
||||
file_monitor_broadcastCh = config['fileMon'].getint('broadcastCh', 2) # default 2
|
||||
read_news_enabled = config['fileMon'].getboolean('enable_read_news', False) # default disabled
|
||||
news_file_path = config['fileMon'].get('news_file_path', 'news.txt') # default news.txt
|
||||
news_random_line_only = config['fileMon'].getboolean('news_random_line', False) # default False
|
||||
enable_runShellCmd = config['fileMon'].getboolean('enable_runShellCmd', False) # default False
|
||||
|
||||
# games
|
||||
game_hop_limit = config['messagingSettings'].getint('game_hop_limit', 5) # default 3 hops
|
||||
|
||||
@@ -9,7 +9,7 @@ import ephem # pip install pyephem
|
||||
from datetime import timedelta
|
||||
from modules.log import *
|
||||
|
||||
trap_list_solarconditions = ("sun", "solar", "hfcond")
|
||||
trap_list_solarconditions = ("sun", "moon", "solar", "hfcond", "satpass")
|
||||
|
||||
def hf_band_conditions():
|
||||
# ham radio HF band conditions
|
||||
@@ -140,3 +140,43 @@ def get_moon(lat=0, lon=0):
|
||||
+ "\nFullMoon:" + moon_table['next_full_moon'] + "\nNewMoon:" + moon_table['next_new_moon']
|
||||
|
||||
return moon_data
|
||||
|
||||
def getNextSatellitePass(satellite, lat=0, lon=0):
|
||||
pass_data = ''
|
||||
# get the next satellite pass for a given satellite
|
||||
visualPassAPI = "https://api.n2yo.com/rest/v1/satellite/visualpasses/"
|
||||
if lat == 0 and lon == 0:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
# API URL
|
||||
if n2yoAPIKey == '':
|
||||
logger.error("System: Missing API key free at https://www.n2yo.com/login/")
|
||||
return "not configured, bug your sysop"
|
||||
url = visualPassAPI + str(satellite) + "/" + str(lat) + "/" + str(lon) + "/0/2/300/" + "&apiKey=" + n2yoAPIKey
|
||||
# get the next pass data
|
||||
try:
|
||||
if not int(satellite):
|
||||
raise Exception("Invalid satellite number")
|
||||
next_pass_data = requests.get(url, timeout=urlTimeoutSeconds)
|
||||
if(next_pass_data.ok):
|
||||
pass_json = next_pass_data.json()
|
||||
if 'info' in pass_json and 'passescount' in pass_json['info'] and pass_json['info']['passescount'] > 0:
|
||||
satname = pass_json['info']['satname']
|
||||
pass_time = pass_json['passes'][0]['startUTC']
|
||||
pass_duration = pass_json['passes'][0]['duration']
|
||||
pass_maxEl = pass_json['passes'][0]['maxEl']
|
||||
pass_rise_time = datetime.fromtimestamp(pass_time).strftime('%a %d %I:%M%p')
|
||||
pass_startAzCompass = pass_json['passes'][0]['startAzCompass']
|
||||
pass_set_time = datetime.fromtimestamp(pass_time + pass_duration).strftime('%a %d %I:%M%p')
|
||||
pass__endAzCompass = pass_json['passes'][0]['endAzCompass']
|
||||
pass_data = f"{satname} @{pass_rise_time} Az:{pass_startAzCompass} for{getPrettyTime(pass_duration)}, MaxEl:{pass_maxEl}° Set@{pass_set_time} Az:{pass__endAzCompass}"
|
||||
elif pass_json['info']['passescount'] == 0:
|
||||
satname = pass_json['info']['satname']
|
||||
pass_data = f"{satname} has no upcoming passes"
|
||||
else:
|
||||
logger.error(f"System: Error fetching satellite pass data {satellite}")
|
||||
pass_data = ERROR_FETCHING_DATA
|
||||
except Exception as e:
|
||||
logger.warning(f"System: User supplied value {satellite} unknown or invalid")
|
||||
pass_data = "Provide NORAD# example use:🛰️satpass 25544,33591"
|
||||
return pass_data
|
||||
@@ -12,13 +12,13 @@ import io # for suppressing output on watchdog
|
||||
from modules.log import *
|
||||
|
||||
# Global Variables
|
||||
debugMetadata = False # packet debug for non text messages
|
||||
trap_list = ("cmd","cmd?") # default trap list
|
||||
help_message = "Bot CMD?:\n"
|
||||
help_message = "Bot CMD?:"
|
||||
asyncLoop = asyncio.new_event_loop()
|
||||
games_enabled = False
|
||||
multiPingList = [{'message_from_id': 0, 'count': 0, 'type': '', 'deviceID': 0, 'channel_number': 0, 'startCount': 0}]
|
||||
|
||||
|
||||
# Ping Configuration
|
||||
if ping_enabled:
|
||||
# ping, pinging, ack, testing, test, pong
|
||||
@@ -28,9 +28,9 @@ if ping_enabled:
|
||||
|
||||
# Sitrep Configuration
|
||||
if sitrep_enabled:
|
||||
trap_list_sitrep = ("sitrep", "lheard")
|
||||
trap_list_sitrep = ("sitrep", "lheard", "sysinfo")
|
||||
trap_list = trap_list + trap_list_sitrep
|
||||
help_message = help_message + ", sitrep"
|
||||
help_message = help_message + ", sitrep, sysinfo"
|
||||
|
||||
# MOTD Configuration
|
||||
if motd_enabled:
|
||||
@@ -51,15 +51,17 @@ if emergency_responder_enabled:
|
||||
|
||||
# whoami Configuration
|
||||
if whoami_enabled:
|
||||
trap_list_whoami = ("whoami", "📍")
|
||||
trap_list_whoami = ("whoami", "📍", "whois")
|
||||
trap_list = trap_list + trap_list_whoami
|
||||
help_message = help_message + ", whoami"
|
||||
|
||||
# Solar Conditions Configuration
|
||||
if solar_conditions_enabled:
|
||||
from modules.solarconditions import * # from the spudgunman/meshing-around repo
|
||||
from modules.space import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_solarconditions # items hfcond, solar, sun, moon
|
||||
help_message = help_message + ", sun, hfcond, solar, moon"
|
||||
if n2yoAPIKey != "":
|
||||
help_message = help_message + ", satpass"
|
||||
else:
|
||||
hf_band_conditions = False
|
||||
|
||||
@@ -73,6 +75,10 @@ if location_enabled:
|
||||
from modules.locationdata import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_location # items tide, whereami, wxc, wx
|
||||
help_message = help_message + ", whereami, wx, wxc, rlist"
|
||||
if enableGBalerts:
|
||||
from modules.globalalert import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_location_eu
|
||||
#help_message = help_message + ", ukalert, ukwx, ukflood"
|
||||
|
||||
# Open-Meteo Configuration for worldwide weather
|
||||
if use_meteo_wxApi:
|
||||
@@ -189,7 +195,18 @@ if file_monitor_enabled or read_news_enabled:
|
||||
from modules.filemon import * # from the spudgunman/meshing-around repo
|
||||
if read_news_enabled:
|
||||
trap_list = trap_list + trap_list_filemon # items readnews
|
||||
help_message = help_message + ", readmail"
|
||||
help_message = help_message + ", readnews"
|
||||
# Bee Configuration uses file monitor module
|
||||
if bee_enabled:
|
||||
trap_list = trap_list + ("🐝",)
|
||||
|
||||
# clean up the help message
|
||||
help_message = help_message.split(", ")
|
||||
help_message.sort()
|
||||
if len(help_message) > 20:
|
||||
# split in half for formatting
|
||||
help_message = help_message[:len(help_message)//2] + ["\nCMD?"] + help_message[len(help_message)//2:]
|
||||
help_message = ", ".join(help_message)
|
||||
|
||||
# BLE dual interface prevention
|
||||
if interface1_type == 'ble' and interface2_type == 'ble':
|
||||
@@ -513,7 +530,7 @@ def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
|
||||
if nodeid == 0:
|
||||
# Send to channel
|
||||
if wantAck:
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"req.ACK " + "Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"req.ACK " + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
|
||||
interface.sendText(text=m, channelIndex=ch, wantAck=True)
|
||||
else:
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
|
||||
@@ -584,23 +601,6 @@ def get_wikipedia_summary(search_term):
|
||||
|
||||
return summary
|
||||
|
||||
def getPrettyTime(seconds):
|
||||
# convert unix time to minutes, hours, or days, or years for simple display
|
||||
designator = "s"
|
||||
if seconds > 0:
|
||||
seconds = round(seconds / 60)
|
||||
designator = "m"
|
||||
if seconds > 60:
|
||||
seconds = round(seconds / 60)
|
||||
designator = "h"
|
||||
if seconds > 24:
|
||||
seconds = round(seconds / 24)
|
||||
designator = "d"
|
||||
if seconds > 365:
|
||||
seconds = round(seconds / 365)
|
||||
designator = "y"
|
||||
return str(seconds) + designator
|
||||
|
||||
def messageTrap(msg):
|
||||
# Check if the message contains a trap word, this is the first filter for listning to messages
|
||||
# after this the message is passed to the command_handler in the bot.py which is switch case filter for applying word to function
|
||||
@@ -669,7 +669,10 @@ def handleMultiPing(nodeID=0, deviceID=1):
|
||||
break
|
||||
|
||||
|
||||
def handleWxBroadcast(deviceID=1):
|
||||
def handleAlertBroadcast(deviceID=1):
|
||||
alertUk = NO_ALERTS
|
||||
alertFema = NO_ALERTS
|
||||
wxAlert = NO_ALERTS
|
||||
# only allow API call every 20 minutes
|
||||
# the watchdog will call this function 3 times, seeing possible throttling on the API
|
||||
clock = datetime.now()
|
||||
@@ -679,15 +682,52 @@ def handleWxBroadcast(deviceID=1):
|
||||
return False
|
||||
|
||||
# check for alerts
|
||||
alert = alertBrodcast()
|
||||
if alert:
|
||||
msg = f"🚨 {alert[1]} EAS ALERTs: {alert[0]}"
|
||||
if isinstance(wxAlertBroadcastChannel, list):
|
||||
for channel in wxAlertBroadcastChannel:
|
||||
send_message(msg, int(channel), 0, deviceID)
|
||||
if wxAlertBroadcastEnabled:
|
||||
alertWx = alertBrodcastNOAA()
|
||||
|
||||
if emergencyAlertBrodcastEnabled:
|
||||
if enableGBalerts:
|
||||
alertUk = get_govUK_alerts()
|
||||
else:
|
||||
send_message(msg, wxAlertBroadcastChannel, 0, deviceID)
|
||||
return True
|
||||
# default USA alerts
|
||||
alertFema = getIpawsAlert(latitudeValue,longitudeValue, shortAlerts=True)
|
||||
|
||||
# format alert
|
||||
if alertWx:
|
||||
wxAlert = f"🚨 {alertWx[1]} EAS WX ALERT: {alertWx[0]}"
|
||||
else:
|
||||
wxAlert = False
|
||||
|
||||
femaAlert = alertFema
|
||||
ukAlert = alertUk
|
||||
|
||||
if emergencyAlertBrodcastEnabled:
|
||||
if NO_ALERTS not in femaAlert and ERROR_FETCHING_DATA not in femaAlert:
|
||||
if isinstance(emergencyAlertBroadcastCh, list):
|
||||
for channel in emergencyAlertBroadcastCh:
|
||||
send_message(femaAlert, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(femaAlert, emergencyAlertBroadcastCh, 0, deviceID)
|
||||
return True
|
||||
if NO_ALERTS not in ukAlert:
|
||||
if isinstance(emergencyAlertBroadcastCh, list):
|
||||
for channel in emergencyAlertBroadcastCh:
|
||||
send_message(ukAlert, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(ukAlert, emergencyAlertBroadcastCh, 0, deviceID)
|
||||
return True
|
||||
|
||||
# pause for 10 seconds
|
||||
time.sleep(10)
|
||||
|
||||
if wxAlertBroadcastEnabled:
|
||||
if wxAlert:
|
||||
if isinstance(wxAlertBroadcastChannel, list):
|
||||
for channel in wxAlertBroadcastChannel:
|
||||
send_message(wxAlert, int(channel), 0, deviceID)
|
||||
else:
|
||||
send_message(wxAlert, wxAlertBroadcastChannel, 0, deviceID)
|
||||
return True
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2
|
||||
@@ -750,7 +790,7 @@ def getNodeFirmware(nodeID=0, nodeInt=1):
|
||||
# this is a workaround because .localNode.getMetadata spits out a lot of debug info which cant be suppressed
|
||||
# Create a StringIO object to capture the
|
||||
output_capture = io.StringIO()
|
||||
with contextlib.redirect_stdout(output_capture):
|
||||
with contextlib.redirect_stdout(output_capture), contextlib.redirect_stderr(output_capture):
|
||||
interface.localNode.getMetadata()
|
||||
console_output = output_capture.getvalue()
|
||||
if "firmware_version" in console_output:
|
||||
@@ -758,22 +798,22 @@ def getNodeFirmware(nodeID=0, nodeInt=1):
|
||||
return fwVer
|
||||
return -1
|
||||
|
||||
def displayNodeTelemetry(nodeID=0, rxNode=0):
|
||||
def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
|
||||
interface = interface1 if rxNode == 1 else interface2
|
||||
global telemetryData
|
||||
|
||||
# throttle the telemetry requests to prevent spamming the device
|
||||
if rxNode == 1:
|
||||
if time.time() - telemetryData[0]['interface1'] < 600:
|
||||
if time.time() - telemetryData[0]['interface1'] < 600 and not userRequested:
|
||||
return -1
|
||||
telemetryData[0]['interface1'] = time.time()
|
||||
elif rxNode == 2:
|
||||
if time.time() - telemetryData[0]['interface2'] < 600:
|
||||
if time.time() - telemetryData[0]['interface2'] < 600 and not userRequested:
|
||||
return -1
|
||||
telemetryData[0]['interface2'] = time.time()
|
||||
|
||||
# some telemetry data is not available in python-meshtastic?
|
||||
# bring in values from the last telemetry request for the node
|
||||
# bring in values from the last telemetry dump for the node
|
||||
numPacketsTx = telemetryData[rxNode]['numPacketsTx']
|
||||
numPacketsRx = telemetryData[rxNode]['numPacketsRx']
|
||||
numPacketsTxErr = telemetryData[rxNode]['numPacketsTxErr']
|
||||
@@ -824,91 +864,106 @@ def displayNodeTelemetry(nodeID=0, rxNode=0):
|
||||
|
||||
positionMetadata = {}
|
||||
def consumeMetadata(packet, rxNode=0):
|
||||
# keep records of recent telemetry data
|
||||
debugMetadata = False
|
||||
packet_type = ''
|
||||
if packet.get('decoded'):
|
||||
packet_type = packet['decoded']['portnum']
|
||||
nodeID = packet['from']
|
||||
try:
|
||||
# keep records of recent telemetry data
|
||||
packet_type = ''
|
||||
if packet.get('decoded'):
|
||||
packet_type = packet['decoded']['portnum']
|
||||
nodeID = packet['from']
|
||||
|
||||
# TELEMETRY packets
|
||||
if packet_type == 'TELEMETRY_APP':
|
||||
#if debugMetadata: print(f"DEBUG TELEMETRY_APP: {packet}\n\n")
|
||||
# get the telemetry data
|
||||
telemetry_packet = packet['decoded']['telemetry']
|
||||
if telemetry_packet.get('deviceMetrics'):
|
||||
deviceMetrics = telemetry_packet['deviceMetrics']
|
||||
if telemetry_packet.get('localStats'):
|
||||
localStats = telemetry_packet['localStats']
|
||||
# Check if 'numPacketsTx' and 'numPacketsRx' exist and are not zero
|
||||
if localStats.get('numPacketsTx') is not None and localStats.get('numPacketsRx') is not None and localStats['numPacketsTx'] != 0:
|
||||
# Assign the values to the telemetry dictionary
|
||||
keys = [
|
||||
'numPacketsTx', 'numPacketsRx', 'numOnlineNodes',
|
||||
'numOfflineNodes', 'numPacketsTxErr', 'numPacketsRxErr', 'numTotalNodes']
|
||||
|
||||
# TELEMETRY packets
|
||||
if packet_type == 'TELEMETRY_APP':
|
||||
if debugMetadata: print(f"DEBUG TELEMETRY_APP: {packet}\n\n")
|
||||
# get the telemetry data
|
||||
telemetry_packet = packet['decoded']['telemetry']
|
||||
if telemetry_packet.get('deviceMetrics'):
|
||||
deviceMetrics = telemetry_packet['deviceMetrics']
|
||||
if telemetry_packet.get('localStats'):
|
||||
localStats = telemetry_packet['localStats']
|
||||
# Check if 'numPacketsTx' and 'numPacketsRx' exist and are not zero
|
||||
if localStats.get('numPacketsTx') is not None and localStats.get('numPacketsRx') is not None and localStats['numPacketsTx'] != 0:
|
||||
# Assign the values to the telemetry dictionary
|
||||
keys = [
|
||||
'numPacketsTx', 'numPacketsRx', 'numOnlineNodes',
|
||||
'numOfflineNodes', 'numPacketsTxErr', 'numPacketsRxErr', 'numTotalNodes']
|
||||
|
||||
for key in keys:
|
||||
if localStats.get(key) is not None:
|
||||
telemetryData[rxNode][key] = localStats.get(key)
|
||||
|
||||
# POSITION_APP packets
|
||||
if packet_type == 'POSITION_APP':
|
||||
if debugMetadata: print(f"DEBUG POSITION_APP: {packet}\n\n")
|
||||
# get the position data
|
||||
keys = ['altitude', 'groundSpeed', 'precisionBits']
|
||||
position_data = packet['decoded']['position']
|
||||
try:
|
||||
if nodeID not in positionMetadata:
|
||||
positionMetadata[nodeID] = {}
|
||||
|
||||
for key in keys:
|
||||
if localStats.get(key) is not None:
|
||||
telemetryData[rxNode][key] = localStats.get(key)
|
||||
|
||||
# POSITION_APP packets
|
||||
if packet_type == 'POSITION_APP':
|
||||
if debugMetadata: print(f"DEBUG POSITION_APP: {packet}\n\n")
|
||||
# get the position data
|
||||
keys = ['altitude', 'groundSpeed', 'precisionBits']
|
||||
position_data = packet['decoded']['position']
|
||||
try:
|
||||
if nodeID not in positionMetadata:
|
||||
positionMetadata[nodeID] = {}
|
||||
|
||||
for key in keys:
|
||||
positionMetadata[nodeID][key] = position_data.get(key, 0)
|
||||
|
||||
# Keep the positionMetadata dictionary at 5 records
|
||||
if len(positionMetadata) > 20:
|
||||
# Remove the oldest entry
|
||||
oldest_nodeID = next(iter(positionMetadata))
|
||||
del positionMetadata[oldest_nodeID]
|
||||
except Exception as e:
|
||||
logger.debug(f"System: POSITION_APP decode error: {e} packet {packet}")
|
||||
positionMetadata[nodeID][key] = position_data.get(key, 0)
|
||||
|
||||
# Keep the positionMetadata dictionary at 5 records
|
||||
if len(positionMetadata) > 20:
|
||||
# Remove the oldest entry
|
||||
oldest_nodeID = next(iter(positionMetadata))
|
||||
del positionMetadata[oldest_nodeID]
|
||||
except Exception as e:
|
||||
logger.debug(f"System: POSITION_APP decode error: {e} packet {packet}")
|
||||
|
||||
# WAYPOINT_APP packets
|
||||
if packet_type == 'WAYPOINT_APP':
|
||||
if debugMetadata: print(f"DEBUG WAYPOINT_APP: {packet['decoded']['waypoint']}\n\n")
|
||||
# get the waypoint data
|
||||
waypoint_data = packet['decoded']['waypoint']
|
||||
keys = ['latitude', 'longitude',]
|
||||
# WAYPOINT_APP packets
|
||||
if packet_type == 'WAYPOINT_APP':
|
||||
if debugMetadata: print(f"DEBUG WAYPOINT_APP: {packet['decoded']['waypoint']}\n\n")
|
||||
# get the waypoint data
|
||||
waypoint_data = packet['decoded']
|
||||
|
||||
# NEIGHBORINFO_APP
|
||||
if packet_type == 'NEIGHBORINFO_APP':
|
||||
if debugMetadata: print(f"DEBUG NEIGHBORINFO_APP: {packet}\n\n")
|
||||
# get the neighbor info data
|
||||
neighbor_data = packet['decoded']['neighborInfo']
|
||||
|
||||
# TRACEROUTE_APP
|
||||
if packet_type == 'TRACEROUTE_APP':
|
||||
if debugMetadata: print(f"DEBUG TRACEROUTE_APP: {packet}\n\n")
|
||||
# get the traceroute data
|
||||
traceroute_data = packet['decoded']['traceroute']
|
||||
# NEIGHBORINFO_APP
|
||||
if packet_type == 'NEIGHBORINFO_APP':
|
||||
if debugMetadata: print(f"DEBUG NEIGHBORINFO_APP: {packet}\n\n")
|
||||
# get the neighbor info data
|
||||
neighbor_data = packet['decoded']
|
||||
|
||||
# TRACEROUTE_APP
|
||||
if packet_type == 'TRACEROUTE_APP':
|
||||
if debugMetadata: print(f"DEBUG TRACEROUTE_APP: {packet}\n\n")
|
||||
# get the traceroute data
|
||||
traceroute_data = packet['decoded']
|
||||
|
||||
# DETECTION_SENSOR_APP
|
||||
if packet_type == 'DETECTION_SENSOR_APP':
|
||||
if debugMetadata: print(f"DEBUG DETECTION_SENSOR_APP: {packet}\n\n")
|
||||
# get the detection sensor data
|
||||
detection_data = packet['decoded']['detectionSensor']
|
||||
# DETECTION_SENSOR_APP
|
||||
if packet_type == 'DETECTION_SENSOR_APP':
|
||||
if debugMetadata: print(f"DEBUG DETECTION_SENSOR_APP: {packet}\n\n")
|
||||
# get the detection sensor data
|
||||
detection_data = packet['decoded']
|
||||
|
||||
# PAXCOUNTER_APP
|
||||
if packet_type == 'PAXCOUNTER_APP':
|
||||
if debugMetadata: print(f"DEBUG PAXCOUNTER_APP: {packet}\n\n")
|
||||
# get the paxcounter data
|
||||
paxcounter_data = packet['decoded']['paxcounter']
|
||||
# PAXCOUNTER_APP
|
||||
if packet_type == 'PAXCOUNTER_APP':
|
||||
if debugMetadata: print(f"DEBUG PAXCOUNTER_APP: {packet}\n\n")
|
||||
# get the paxcounter data
|
||||
paxcounter_data = packet['decoded']
|
||||
|
||||
# REMOTE_HARDWARE_APP
|
||||
if packet_type == 'REMOTE_HARDWARE_APP':
|
||||
if debugMetadata: print(f"DEBUG REMOTE_HARDWARE_APP: {packet}\n\n")
|
||||
# get the remote hardware data
|
||||
remote_hardware_data = packet['decoded']['remoteHardware']
|
||||
|
||||
# REMOTE_HARDWARE_APP
|
||||
if packet_type == 'REMOTE_HARDWARE_APP':
|
||||
if debugMetadata: print(f"DEBUG REMOTE_HARDWARE_APP: {packet}\n\n")
|
||||
# get the remote hardware data
|
||||
remote_hardware_data = packet['decoded']
|
||||
except KeyError as e:
|
||||
logger.critical(f"System: Error consuming metadata: {e} Device:{rxNode}")
|
||||
logger.debug(f"System: Error Packet = {packet}")
|
||||
|
||||
def get_sysinfo(nodeID=0, deviceID=1):
|
||||
# Get the system telemetry data for return on the sysinfo command
|
||||
sysinfo = ''
|
||||
stats = str(displayNodeTelemetry(nodeID, deviceID, userRequested=True)) + " 🤖👀" + str(len(seenNodes))
|
||||
if "numPacketsRx:0" in stats or stats == -1:
|
||||
return "Gathering Telemetry try again later⏳"
|
||||
# replace Telemetry with Int in string
|
||||
stats = stats.replace("Telemetry", "Int")
|
||||
sysinfo += f"📊{stats}"
|
||||
if interface2_enabled:
|
||||
sysinfo += f"📊{stats}"
|
||||
|
||||
return sysinfo
|
||||
|
||||
async def BroadcastScheduler():
|
||||
# handle schedule checks for the broadcast of messages
|
||||
@@ -1043,12 +1098,11 @@ async def handleSentinel(deviceID=1):
|
||||
|
||||
if handleSentinel_loop >= sentry_holdoff and handleSentinel_spotted != enemySpotted:
|
||||
# check the positionMetadata for nodeID and get metadata
|
||||
if positionMetadata and closest_nodes[0]['id'] in positionMetadata:
|
||||
if closest_nodes and positionMetadata and closest_nodes[0]['id'] in positionMetadata:
|
||||
metadata = positionMetadata[closest_nodes[0]['id']]
|
||||
if metadata.get('precisionBits') is not None:
|
||||
resolution = metadata.get('precisionBits')
|
||||
|
||||
|
||||
logger.warning(f"System: {enemySpotted} is close to your location on Interface1 Accuracy is {resolution}bits")
|
||||
send_message(f"Sentry{deviceID}: {enemySpotted}", secure_channel, 0, deviceID)
|
||||
if enableSMTP and email_sentry_alerts:
|
||||
@@ -1082,8 +1136,10 @@ async def watchdog():
|
||||
# multiPing handler
|
||||
handleMultiPing(0,1)
|
||||
|
||||
if wxAlertBroadcastEnabled:
|
||||
handleWxBroadcast(1)
|
||||
# Alert Broadcast
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled:
|
||||
# weather alerts
|
||||
handleAlertBroadcast(1)
|
||||
|
||||
# Telemetry data
|
||||
int1Data = displayNodeTelemetry(0, 1)
|
||||
@@ -1112,10 +1168,12 @@ async def watchdog():
|
||||
await handleSentinel(2)
|
||||
|
||||
# multiPing handler
|
||||
handleMultiPing(0,2)
|
||||
handleMultiPing(0,1)
|
||||
|
||||
if wxAlertBroadcastEnabled:
|
||||
handleWxBroadcast(2)
|
||||
# Alert Broadcast
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled:
|
||||
# weather alerts
|
||||
handleAlertBroadcast(2)
|
||||
|
||||
# Telemetry data
|
||||
int2Data = displayNodeTelemetry(0, 2)
|
||||
@@ -1129,4 +1187,3 @@ async def watchdog():
|
||||
except Exception as e:
|
||||
logger.error(f"System: retrying interface2: {e}")
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import openmeteo_requests # pip install openmeteo-requests
|
||||
from retry_requests import retry # pip install retry_requests
|
||||
#import requests_cache
|
||||
#import openmeteo_requests # pip install openmeteo-requests
|
||||
#from retry_requests import retry # pip install retry_requests
|
||||
|
||||
import requests
|
||||
import json
|
||||
from modules.log import *
|
||||
|
||||
def get_weather_data(api_url, params):
|
||||
response = requests.get(api_url, params=params)
|
||||
response.raise_for_status() # Raise an error for bad status codes
|
||||
return response.json()
|
||||
|
||||
def get_wx_meteo(lat=0, lon=0, unit=0):
|
||||
# set forcast days 1 or 3
|
||||
forecastDays = 3
|
||||
|
||||
# Setup the Open-Meteo API client with cache and retry on error
|
||||
#cache_session = requests_cache.CachedSession('.cache', expire_after = 3600)
|
||||
#retry_session = retry(cache_session, retries = 5, backoff_factor = 0.2)
|
||||
retry_session = retry(retries = 3, backoff_factor = 0.2)
|
||||
openmeteo = openmeteo_requests.Client(session = retry_session)
|
||||
|
||||
# Make sure all required weather variables are listed here
|
||||
# The order of variables in hourly or daily is important to assign them correctly below
|
||||
url = "https://api.open-meteo.com/v1/forecast"
|
||||
@@ -34,27 +35,29 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
|
||||
|
||||
try:
|
||||
# Fetch the weather data
|
||||
responses = openmeteo.weather_api(url, params=params)
|
||||
weather_data = get_weather_data(url, params)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching meteo weather data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# Check if we got a response
|
||||
try:
|
||||
# Process location
|
||||
response = responses[0]
|
||||
logger.debug(f"Got wx data from Open-Meteo in {response.Timezone()} {response.TimezoneAbbreviation()}")
|
||||
|
||||
# Process location
|
||||
logger.debug(f"System: Pulled from Open-Meteo in {weather_data['timezone']} {weather_data['timezone_abbreviation']}")
|
||||
|
||||
# Ensure response is defined
|
||||
response = weather_data
|
||||
|
||||
# Process daily data. The order of variables needs to be the same as requested.
|
||||
daily = response.Daily()
|
||||
daily_weather_code = daily.Variables(0).ValuesAsNumpy()
|
||||
daily_temperature_2m_max = daily.Variables(1).ValuesAsNumpy()
|
||||
daily_temperature_2m_min = daily.Variables(2).ValuesAsNumpy()
|
||||
daily_precipitation_hours = daily.Variables(3).ValuesAsNumpy()
|
||||
daily_precipitation_probability_max = daily.Variables(4).ValuesAsNumpy()
|
||||
daily_wind_speed_10m_max = daily.Variables(5).ValuesAsNumpy()
|
||||
daily_wind_gusts_10m_max = daily.Variables(6).ValuesAsNumpy()
|
||||
daily_wind_direction_10m_dominant = daily.Variables(7).ValuesAsNumpy()
|
||||
daily = response['daily']
|
||||
daily_weather_code = daily['weather_code']
|
||||
daily_temperature_2m_max = daily['temperature_2m_max']
|
||||
daily_temperature_2m_min = daily['temperature_2m_min']
|
||||
daily_precipitation_hours = daily['precipitation_hours']
|
||||
daily_precipitation_probability_max = daily['precipitation_probability_max']
|
||||
daily_wind_speed_10m_max = daily['wind_speed_10m_max']
|
||||
daily_wind_gusts_10m_max = daily['wind_gusts_10m_max']
|
||||
daily_wind_direction_10m_dominant = daily['wind_direction_10m_dominant']
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing meteo weather data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
@@ -191,3 +194,46 @@ def get_wx_meteo(lat=0, lon=0, unit=0):
|
||||
|
||||
return weather_report
|
||||
|
||||
def get_flood_openmeteo(lat=0, lon=0):
|
||||
# set forcast days 1 or 3
|
||||
forecastDays = 3
|
||||
|
||||
# Flood data
|
||||
url = "https://flood-api.open-meteo.com/v1/flood"
|
||||
params = {
|
||||
"latitude": {lat},
|
||||
"longitude": {lon},
|
||||
"timezone": "auto",
|
||||
"daily": "river_discharge",
|
||||
"forecast_days": forecastDays
|
||||
}
|
||||
|
||||
try:
|
||||
# Fetch the flood data
|
||||
flood_data = get_weather_data(url, params)
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching meteo flood data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# Check if we got a response
|
||||
try:
|
||||
# Process location
|
||||
logger.debug(f"System: Pulled River FLow Data from Open-Meteo {flood_data['timezone_abbreviation']}")
|
||||
|
||||
# Ensure response is defined
|
||||
response = flood_data
|
||||
|
||||
# Process daily data. The order of variables needs to be the same as requested.
|
||||
daily = response['daily']
|
||||
daily_river_discharge = daily['river_discharge']
|
||||
# check if none
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing meteo flood data: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# create a flood report
|
||||
flood_report = ""
|
||||
flood_report += "River Discharge: " + str(daily_river_discharge) + "m3/s"
|
||||
|
||||
return flood_report
|
||||
|
||||
34
pong_bot.py
34
pong_bot.py
@@ -4,12 +4,11 @@
|
||||
|
||||
import asyncio
|
||||
import time # for sleep, get some when you can :)
|
||||
from pubsub import pub # pip install pubsub
|
||||
from pubsub import pub # pip install pubsub, use launch.sh for venv
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
# Global Variables
|
||||
cmdHistory = [] # list to hold the last commands
|
||||
DEBUGpacket = False # Debug print the packet rx
|
||||
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
@@ -18,6 +17,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
bot_response = "I'm sorry, I'm afraid I can't do that."
|
||||
|
||||
command_handler = {
|
||||
# Command List processes system.trap_list. system.messageTrap() sends any commands to here
|
||||
"ack": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cmd": lambda: help_message,
|
||||
"cmd?": lambda: help_message,
|
||||
@@ -29,6 +29,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"ping": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"pong": lambda: "🏓PING!!🛜",
|
||||
"sitrep": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
|
||||
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID),
|
||||
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
}
|
||||
@@ -90,6 +91,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
msg = msg + " #" + message.split("#")[1]
|
||||
type = type + " #" + message.split("#")[1]
|
||||
|
||||
|
||||
# check for multi ping request
|
||||
if " " in message:
|
||||
# if stop multi ping
|
||||
@@ -99,6 +101,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
multiPingList.pop(i)
|
||||
msg = "🛑 auto-ping"
|
||||
|
||||
|
||||
# if 3 or more entries (2 or more active), throttle the multi-ping for congestion
|
||||
if len(multiPingList) > 2:
|
||||
msg = "🚫⛔️ auto-ping, service busy. ⏳Try again soon."
|
||||
@@ -109,6 +112,10 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
pingCount = int(message.split(" ")[1])
|
||||
if pingCount == 123 or pingCount == 1234:
|
||||
pingCount = 1
|
||||
elif not autoPingInChannel and not isDM:
|
||||
# no autoping in channels
|
||||
pingCount = 1
|
||||
|
||||
if pingCount > 51:
|
||||
pingCount = 50
|
||||
except:
|
||||
@@ -120,6 +127,10 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
|
||||
else:
|
||||
msg = f"🚦Initalizing {pingCount} auto-ping"
|
||||
|
||||
# if not a DM add the username to the beginning of msg
|
||||
if not useDMForResponse and not isDM:
|
||||
msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + msg
|
||||
|
||||
return msg
|
||||
|
||||
@@ -131,6 +142,12 @@ def handle_motd(message):
|
||||
return "MOTD Set to: " + MOTD
|
||||
else:
|
||||
return MOTD
|
||||
|
||||
def sysinfo(message, message_from_id, deviceID):
|
||||
if "?" in message:
|
||||
return "sysinfo command returns system information."
|
||||
else:
|
||||
return get_sysinfo(message_from_id, deviceID)
|
||||
|
||||
def handle_lheard(message, nodeid, deviceID, isDM):
|
||||
if "?" in message and isDM:
|
||||
@@ -191,6 +208,9 @@ def onReceive(packet, interface):
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
rxNode = 2
|
||||
|
||||
# set the message_from_id
|
||||
message_from_id = packet['from']
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
@@ -200,7 +220,10 @@ def onReceive(packet, interface):
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
message_from_id = packet['from']
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
|
||||
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
|
||||
|
||||
# get the signal strength and snr if available
|
||||
if packet.get('rxSnr') or packet.get('rxRssi'):
|
||||
@@ -229,6 +252,9 @@ def onReceive(packet, interface):
|
||||
if hop_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
elif hop_start == 0 and hop_limit > 0:
|
||||
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:
|
||||
@@ -321,7 +347,7 @@ def onReceive(packet, interface):
|
||||
send_message(rMsg, channel_number, 0, 1)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode)
|
||||
consumeMetadata(packet, rxNode)
|
||||
except KeyError as e:
|
||||
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
|
||||
logger.debug(f"System: Error Packet = {packet}")
|
||||
|
||||
@@ -6,9 +6,6 @@ requests
|
||||
maidenhead
|
||||
beautifulsoup4
|
||||
dadjokes
|
||||
openmeteo_requests
|
||||
retry_requests
|
||||
numpy
|
||||
geopy
|
||||
schedule
|
||||
wikipedia
|
||||
|
||||
7
runShell.sh
Normal file
7
runShell.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
# meshing-around demo script for shell scripting
|
||||
# runShell.sh
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
|
||||
printf "Running meshing-around demo script for shell scripting\n"
|
||||
30
sysEnv.sh
Normal file
30
sysEnv.sh
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# meshing-around shell script for sysinfo
|
||||
# runShell.sh
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
|
||||
# get basic telemetry data. Free space, CPU, RAM, and temperature for a raspberry pi
|
||||
free_space=$(df -h | grep ' /$' | awk '{print $4}')
|
||||
cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
|
||||
ram_usage=$(free | grep Mem | awk '{print $3/$2 * 100.0}')
|
||||
ram_free=$(echo "scale=2; 100 - $ram_usage" | bc)
|
||||
|
||||
# if command vcgencmd is found, part of raspberrypi tools, use it to get temperature
|
||||
if command -v vcgencmd &> /dev/null
|
||||
then
|
||||
# get temperature
|
||||
temp=$(vcgencmd measure_temp | sed "s/temp=//" | sed "s/'C//")
|
||||
# temp in fahrenheit
|
||||
tempf=$(echo "scale=2; $temp * 9 / 5 + 32" | bc)
|
||||
else
|
||||
# get temperature from thermal zone
|
||||
temp=$(paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | grep "temp" | awk '{print $2/1000}' | awk '{s+=$1} END {print s/NR}')
|
||||
tempf=$(echo "scale=2; $temp * 9 / 5 + 32" | bc)
|
||||
fi
|
||||
|
||||
# print telemetry data
|
||||
printf "Disk:%s" "$free_space"
|
||||
printf " RAM:%.2f%%" "$ram_usage"
|
||||
printf " CPU:%.1f%%" "$cpu_usage"
|
||||
printf " CPU-T:%.1f°C (%.1f°F)" "$temp" "$tempf"
|
||||
Reference in New Issue
Block a user