mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
183 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff390cf470 | ||
|
|
17d8cd1067 | ||
|
|
b9348c906d | ||
|
|
6ba3508cc5 | ||
|
|
1c78f154da | ||
|
|
e0a3d0f94e | ||
|
|
066211e9f2 | ||
|
|
5701cd108b | ||
|
|
b877a294ac | ||
|
|
2aedcfc46e | ||
|
|
12147db5d0 | ||
|
|
cef37b574b | ||
|
|
6f121b7aac | ||
|
|
9e31b7f47e | ||
|
|
f3103984ef | ||
|
|
9c8b3f0a54 | ||
|
|
f88cbf210e | ||
|
|
9909113beb | ||
|
|
c1b783b1cd | ||
|
|
9b3b6a5d3d | ||
|
|
cffdb3c089 | ||
|
|
7bb9c9ac55 | ||
|
|
830ec95080 | ||
|
|
0ea575ac70 | ||
|
|
d836255716 | ||
|
|
4f115c9c21 | ||
|
|
63bd5b836d | ||
|
|
5ad9b9a261 | ||
|
|
7a024b681f | ||
|
|
75df5a695b | ||
|
|
0ef8cffd56 | ||
|
|
73e8e063d2 | ||
|
|
82880677f4 | ||
|
|
fe8ba8aaf4 | ||
|
|
cea9147745 | ||
|
|
c1c68d4c10 | ||
|
|
5fcd21680e | ||
|
|
9e1356172f | ||
|
|
de7fdfad11 | ||
|
|
a87055874a | ||
|
|
5c7433091d | ||
|
|
f0ca818461 | ||
|
|
76006dcda7 | ||
|
|
33abe646ae | ||
|
|
c47004c47c | ||
|
|
e66d945be7 | ||
|
|
10afc128f4 | ||
|
|
e6fc794951 | ||
|
|
4839e9ba03 | ||
|
|
bde15e311a | ||
|
|
21c83222e9 | ||
|
|
bbcdd6656a | ||
|
|
7f61b86252 | ||
|
|
25ae27a162 | ||
|
|
a04133e82f | ||
|
|
2a9dfc90ee | ||
|
|
f1bf84f6f0 | ||
|
|
4b91ef10b4 | ||
|
|
cd4497b129 | ||
|
|
01374a8307 | ||
|
|
46c115b783 | ||
|
|
eec7230a84 | ||
|
|
9394fd6ca9 | ||
|
|
c6653da1f3 | ||
|
|
9f47958a03 | ||
|
|
78e51b7be1 | ||
|
|
26fcf6fc02 | ||
|
|
c2336850fe | ||
|
|
54e0d17e70 | ||
|
|
7a6d1f7b29 | ||
|
|
7e26d3f0e5 | ||
|
|
89be8e13a2 | ||
|
|
aa8482ab52 | ||
|
|
69605e0984 | ||
|
|
8e15a3fc99 | ||
|
|
d671b19bce | ||
|
|
943dd4d5a3 | ||
|
|
05d8671b3f | ||
|
|
4bccd33827 | ||
|
|
71ebe7087f | ||
|
|
8dbffe2e63 | ||
|
|
cbea9b5294 | ||
|
|
acdc94cd06 | ||
|
|
e9deb62047 | ||
|
|
f1ad470f88 | ||
|
|
b19f7be0b0 | ||
|
|
053acd1ac6 | ||
|
|
3d5b671d81 | ||
|
|
f090230c96 | ||
|
|
d9040a4ec7 | ||
|
|
e35c954e5d | ||
|
|
93ed84fdee | ||
|
|
9f074e5250 | ||
|
|
12d94fb0dc | ||
|
|
afa2bc4024 | ||
|
|
8dcbf66618 | ||
|
|
902b4f22ee | ||
|
|
7ae0d5e927 | ||
|
|
49b8206e76 | ||
|
|
5a30cc7511 | ||
|
|
a85cc8c593 | ||
|
|
5ae496702d | ||
|
|
1dffa0987d | ||
|
|
f3d07eed97 | ||
|
|
de8266b955 | ||
|
|
d482f2ccc9 | ||
|
|
9f676a4c8d | ||
|
|
5d0dae236c | ||
|
|
bf32eca47d | ||
|
|
dcef6da5bc | ||
|
|
a1ffc8b1f6 | ||
|
|
921b66f9e1 | ||
|
|
0553a43a01 | ||
|
|
5079c67f62 | ||
|
|
785deb2add | ||
|
|
4b0654971c | ||
|
|
d2fd133743 | ||
|
|
d689495ee7 | ||
|
|
b16b4e3c12 | ||
|
|
10109672a7 | ||
|
|
4a3cd2560c | ||
|
|
576898b8fe | ||
|
|
4db9c136d6 | ||
|
|
a1a4c1b0f0 | ||
|
|
7b1b435e45 | ||
|
|
54e716d2cc | ||
|
|
b44fa22c11 | ||
|
|
5829cdcef9 | ||
|
|
f0a93b0191 | ||
|
|
9014a7e8f9 | ||
|
|
6c9f9f2521 | ||
|
|
9bae30bcb1 | ||
|
|
7069ba1f43 | ||
|
|
ae844f8ecd | ||
|
|
af734ccb1f | ||
|
|
1ff5895bad | ||
|
|
f12fa0fe9b | ||
|
|
45c67024e7 | ||
|
|
725cbd8045 | ||
|
|
502a4f2666 | ||
|
|
9aaebaad62 | ||
|
|
d163bffba6 | ||
|
|
36ba04a234 | ||
|
|
0ac683b5c0 | ||
|
|
b16d9322e3 | ||
|
|
868009b650 | ||
|
|
f917df709c | ||
|
|
ab54dc06d7 | ||
|
|
c7b7b182b9 | ||
|
|
b78cf4d022 | ||
|
|
6f492ef382 | ||
|
|
e24c9a9d56 | ||
|
|
b1155dea7d | ||
|
|
0d9245d448 | ||
|
|
858bef7703 | ||
|
|
acf39d0870 | ||
|
|
89a0884600 | ||
|
|
70e11117f1 | ||
|
|
d3f07ae524 | ||
|
|
4f9c36fdad | ||
|
|
df15fb54b0 | ||
|
|
638dc4df16 | ||
|
|
81e91ab6c5 | ||
|
|
05476c2bff | ||
|
|
3b4b0e8c32 | ||
|
|
772218d108 | ||
|
|
dae2e4c4f4 | ||
|
|
5d5595ef8b | ||
|
|
cf16fc3db7 | ||
|
|
70659c9c14 | ||
|
|
b04368f852 | ||
|
|
9e5285a845 | ||
|
|
475d475e18 | ||
|
|
2c4cfa9e81 | ||
|
|
15d7f75507 | ||
|
|
30131bc6d5 | ||
|
|
5373b61f83 | ||
|
|
7eb629676b | ||
|
|
db9b89d0ac | ||
|
|
d7af337a63 | ||
|
|
e3c5eb6add | ||
|
|
b0e57e8aca | ||
|
|
b4168214b6 |
@@ -1 +1,2 @@
|
||||
currently operating under "Agile software development" aka rolling code; no major structure. meshing about .. get it..
|
||||
currently operating under "Agile software development" aka rolling code; no major structure. meshing about .. get it..
|
||||
there is some ideas for adding code in modules/README.md
|
||||
13
Dockerfile
13
Dockerfile
@@ -1,24 +1,21 @@
|
||||
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/*
|
||||
RUN apt-get update && apt-get install -y gettext tzdata locales nano && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set the locale default to en_US.UTF-8
|
||||
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
|
||||
COPY . /app
|
||||
COPY requirements.txt .
|
||||
COPY config.template /app/config.ini
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
COPY . .
|
||||
|
||||
COPY config.ini /app/config.ini
|
||||
COPY entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/script/docker/entrypoint.sh
|
||||
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
ENTRYPOINT ["/bin/bash", "/app/script/docker/entrypoint.sh"]
|
||||
|
||||
67
README.md
67
README.md
@@ -11,13 +11,14 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Automated Responses**: The bot detects keywords like "ping" and responds with "pong" in direct messages (DMs) or group channels.
|
||||
- **Customizable Triggers**: Monitor group channels for specific keywords and set custom responses.
|
||||
- **Emergency Response**: Monitor channels for keywords indicating emergencies and alert a wider audience.
|
||||
- **New Node Hello**: Greet new nodes on the mesh with a hello message
|
||||
|
||||
### Network Tools
|
||||
- **Build, Test Local Mesh**: Ping allow for message delivery testing with more realistic packets vs. telemetry
|
||||
- **Test Node Hardware**: `test` will send incremental sized data into the radio buffer for overall length of message testing
|
||||
|
||||
### Dual Radio/Node Support
|
||||
- **Simultaneous Monitoring**: Monitor two networks at the same time.
|
||||
### Multi Radio/Node Support
|
||||
- **Simultaneous Monitoring**: Monitor up to nine networks at the same time.
|
||||
- **Flexible Messaging**: send mail and messages, between networks.
|
||||
|
||||
### Advanced Messaging Capabilities
|
||||
@@ -26,7 +27,8 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
|
||||
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
|
||||
- **BBS Linking**: Combine multiple bots to expand BBS reach.
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS expanding visability.
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS(Email) expanding visability.
|
||||
- **New Node Hello**: Send a hello to any new node seen in text message.
|
||||
|
||||
### Interactive AI and Data Lookup
|
||||
- **NOAA location Data**: Get localized weather(alerts), River Flow, and Tide information. Open-Meteo is used for wx only outside NOAA coverage.
|
||||
@@ -37,6 +39,9 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
### Proximity Alerts
|
||||
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
|
||||
|
||||
### CheckList / Check In Out
|
||||
- **Asset Tracking**: Maintain a list of node/asset checkin and checkout. Usefull for accountability of people, assets. Radio-Net, FEMA, Trailhead.
|
||||
|
||||
### Fun and Games
|
||||
- **Built-in Games**: Enjoy games like DopeWars, Lemonade Stand, BlackJack, and VideoPoker.
|
||||
- **Command-Based Gameplay**: Issue `games` to display help and start playing.
|
||||
@@ -49,7 +54,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
|
||||
- **NINA alerts for Germany**: Emergency Alerts from xrepository.de feed
|
||||
|
||||
### File Monitor Alerts
|
||||
- **File Monitor**: Monitor a flat/text file for changes, broadcast the contents of the message to the mesh channel.
|
||||
@@ -62,7 +67,7 @@ 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, 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.
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware. For pico or low-powered devices, see projects for embedding, [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), also see [femtofox](https://github.com/noon92/femtofox). 🥔 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
|
||||
|
||||
@@ -73,25 +78,14 @@ git clone https://github.com/spudgunman/meshing-around
|
||||
```
|
||||
The code is under active development, so make sure to pull the latest changes regularly!
|
||||
|
||||
#### Optional Automation of setup
|
||||
#### Quick setup
|
||||
- **Automated Installation**: `install.sh` will automate optional venv and requirements installation.
|
||||
- **Launch Script**: `launch.sh` will activate and launch the app in the venv
|
||||
- **Launch Script**: `launch.sh` only used in a venv install, to launch the bot and the report generator.
|
||||
|
||||
#### Docker Installation
|
||||
If you prefer to use Docker, follow these steps:
|
||||
See further info on the [docker.md](script/docker/README.md)
|
||||
|
||||
1. Ensure your serial port is properly shared.
|
||||
2. Build the Docker image:
|
||||
```sh
|
||||
cd meshing-around
|
||||
docker build -t meshing-around .
|
||||
```
|
||||
3. Run the Docker container:
|
||||
```sh
|
||||
docker run --rm -it --device=/dev/ttyUSB0 meshing-around
|
||||
```
|
||||
|
||||
#### Custom Install
|
||||
#### Manual Install
|
||||
Install the required dependencies using pip:
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
@@ -102,8 +96,10 @@ Copy the configuration template to `config.ini` and edit it to suit your needs:
|
||||
cp config.template config.ini
|
||||
```
|
||||
|
||||
### Configuration
|
||||
Copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
|
||||
### Configuration Guide
|
||||
The following is documentation for the config.ini file
|
||||
|
||||
If you have not done so, or want to 'factory reset', copy the [config.template](config.template) to `config.ini` and set the appropriate interface for your method (serial/ble/tcp). While BLE and TCP will work, they are not as reliable as serial connections. There is a watchdog to reconnect TCP if possible. To get the BLE MAC address, use:
|
||||
```sh
|
||||
meshtastic --ble-scan
|
||||
```
|
||||
@@ -211,8 +207,8 @@ alert_interface = 1
|
||||
### EAS Alerting
|
||||
To Alert on Mesh with the EAS API you can set the channels and enable, checks every 20min.
|
||||
|
||||
#### FEMA iPAWS/EAS and 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
|
||||
#### FEMA iPAWS/EAS and NINA
|
||||
This uses USA: SAME, FIPS, ZIP code to locate the alerts in the feed. By default ignoring Test messages.
|
||||
|
||||
```ini
|
||||
eAlertBroadcastEnabled = False # Goverment IPAWS/CAP Alert Broadcast
|
||||
@@ -221,7 +217,10 @@ 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
|
||||
|
||||
# To use other country services enable only a single optional serivce
|
||||
|
||||
enableDEalerts = False # Use DE Alert Broadcast Data see template for filters
|
||||
```
|
||||
|
||||
#### NOAA EAS
|
||||
@@ -316,7 +315,14 @@ 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
|
||||
### Greet new nodes QRZ module
|
||||
This isnt QRZ.com this is Q code for who is calling me, this will track new nodes and say hello
|
||||
```ini
|
||||
[qrz]
|
||||
enabled = True # QRZ Hello to new nodes
|
||||
qrz_hello_string = "send CMD or DM me for more info." # will be sent to all heard nodes once
|
||||
training = True # Training mode will not send the hello message to new nodes, use this to build up database
|
||||
```
|
||||
|
||||
### Scheduler
|
||||
In the config.ini enable the module
|
||||
@@ -370,7 +376,7 @@ There is no direct support for MQTT in the code, however, reports from Discord a
|
||||
### Radio Propagation & Weather Forcasting
|
||||
| Command | Description | |
|
||||
|---------|-------------|-------------------
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or UK. Headline or expanded details for USA | |
|
||||
| `ea` and `ealert` | Return FEMA iPAWS/EAS alerts in USA or DE Headline or expanded details for USA | |
|
||||
| `hfcond` | Returns a table of HF solar conditions | |
|
||||
| `rlist` | Returns a table of nearby repeaters from RepeaterBook | |
|
||||
| `riverflow` | Return information from NOAA for river flow info. Example: `riverflow modules/settings.py`| |
|
||||
@@ -405,6 +411,13 @@ There is no direct support for MQTT in the code, however, reports from Discord a
|
||||
| `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` |
|
||||
|
||||
### CheckList
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
| `checkin` | Check in the node to the checklist database, you can add a note like `checkin ICO` or `checkin radio4` | ✅ |
|
||||
| `checkout` | Checkout the node in the checklist database, checkout all from node | ✅ |
|
||||
| `checklist` | Display the checklist database, with note | ✅ |
|
||||
|
||||
### Games (via DM)
|
||||
| Command | Description | |
|
||||
|---------|-------------|-
|
||||
|
||||
@@ -8,20 +8,21 @@
|
||||
type = serial
|
||||
port = /dev/ttyACM0
|
||||
# port = /dev/ttyUSB0
|
||||
# port = COM1
|
||||
# hostname = localhost
|
||||
# hostname = meshtastic.local
|
||||
# mac = 00:11:22:33:44:55
|
||||
|
||||
# Additional interface for dual radio support
|
||||
# Additional interface for multi radio support
|
||||
[interface2]
|
||||
enabled = False
|
||||
type = serial
|
||||
port = /dev/ttyUSB0
|
||||
#port = /dev/ttyACM1
|
||||
# port = /dev/ttyACM1
|
||||
# port = COM1
|
||||
# hostname = meshtastic.local
|
||||
# hostname = localhost
|
||||
# mac = 00:11:22:33:44:55
|
||||
|
||||
# example, the third interface would be [interface3] up to 9
|
||||
|
||||
[general]
|
||||
# if False will respond on all channels but the default channel
|
||||
respond_by_dm_only = True
|
||||
@@ -74,6 +75,8 @@ urlTimeout = 10
|
||||
LogMessagesToFile = False
|
||||
# Logging of system messages to file
|
||||
SyslogToFile = True
|
||||
# logging level for the bot (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
sysloglevel = DEBUG
|
||||
# Number of log files to keep in days, 0 to keep all
|
||||
log_backup_count = 32
|
||||
|
||||
@@ -91,7 +94,7 @@ emailSentryAlerts = False
|
||||
# radius in meters to detect someone close to the bot
|
||||
SentryRadius = 100
|
||||
# channel to send a message to when the watchdog is triggered
|
||||
SentryChannel = 9
|
||||
SentryChannel = 2
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 9
|
||||
# list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
@@ -131,21 +134,30 @@ UseMeteoWxAPI = False
|
||||
# NOAA Hydrology unique identifiers, LID or USGS ID
|
||||
riverListDefault =
|
||||
|
||||
# EAS Alert Broadcast
|
||||
# NOAA EAS Alert Broadcast
|
||||
wxAlertBroadcastEnabled = False
|
||||
# EAS Alert Broadcast Channels
|
||||
wxAlertBroadcastCh = 2
|
||||
# Add extra location to the weather alert
|
||||
enableExtraLocationWx = False
|
||||
|
||||
# Goverment IPAWS/CAP Alert Broadcast
|
||||
# Goverment Alert Broadcast defaults to FEMA IPAWS
|
||||
eAlertBroadcastEnabled = False
|
||||
# Goverment Emergency IPAWS/CAP Alert Broadcast Channels
|
||||
# Goverment Alert Broadcast Channels
|
||||
eAlertBroadcastCh = 2
|
||||
|
||||
# FEMA Alert Broadcast Settings
|
||||
# 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
|
||||
|
||||
# Use DE Alert Broadcast Data
|
||||
enableDEalerts = False
|
||||
# comma separated list of regional codes trigger local alert.
|
||||
# find your regional codet at https://www.xrepository.de/api/xrepository/urn:de:bund:destatis:bevoelkerungsstatistik:schluessel:rs_2021-07-31/download/Regionalschl_ssel_2021-07-31.json
|
||||
myRegionalKeysDE = 110000000000,120510000000
|
||||
|
||||
# Satalite Pass Prediction
|
||||
# Register for free API https://www.n2yo.com/login/
|
||||
@@ -153,6 +165,20 @@ n2yoAPIKey =
|
||||
# NORAD list https://www.n2yo.com/satellites/
|
||||
satList = 25544,7530
|
||||
|
||||
# CheckList Checkin/Checkout
|
||||
[checklist]
|
||||
enabled = False
|
||||
checklist_db = data/checklist.db
|
||||
reverse_in_out = False
|
||||
|
||||
[qrz]
|
||||
# QRZ Hello to new nodes with message
|
||||
enabled = False
|
||||
qrz_db = data/qrz.db
|
||||
qrz_hello_string = "send CMD or DM me for more info."
|
||||
# Training mode will not send the hello message to new nodes
|
||||
training = True
|
||||
|
||||
# repeater module
|
||||
[repeater]
|
||||
enabled = False
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#!/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
|
||||
|
||||
exec python /app/mesh_bot.py
|
||||
@@ -1,5 +1,6 @@
|
||||
# /etc/systemd/system/mesh_bot.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable mesh_bot.service
|
||||
# sudo systemctl start mesh_bot.service
|
||||
|
||||
[Unit]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# /etc/systemd/system/mesh_bot.service
|
||||
# /etc/systemd/system/mesh_bot_reporting.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl start mesh_bot.service
|
||||
# sudo systemctl enable mesh_bot_reporting.service
|
||||
# sudo systemctl start mesh_bot_reporting.service
|
||||
|
||||
[Unit]
|
||||
Description=MeshingAround-Reporting
|
||||
|
||||
23
etc/mesh_bot_w3.tmp
Normal file
23
etc/mesh_bot_w3.tmp
Normal file
@@ -0,0 +1,23 @@
|
||||
# /etc/systemd/system/mesh_bot_w3.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable mesh_bot_w3.service
|
||||
# sudo systemctl start mesh_bot_w3.service
|
||||
|
||||
[Unit]
|
||||
Description=MeshingAround-W3Server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 modules/web.py
|
||||
ExecStop=pkill -f mesh_bot_w3.py
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
Restart=on-failure
|
||||
Type=notify #try simple if any problems
|
||||
@@ -1,5 +1,6 @@
|
||||
# /etc/systemd/system/pong_bot.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl enable pong_bot.service
|
||||
# sudo systemctl start pong_bot.service
|
||||
|
||||
[Unit]
|
||||
|
||||
118
install.sh
118
install.sh
@@ -7,8 +7,9 @@ program_path=$(pwd)
|
||||
printf "\n########################"
|
||||
printf "\nMeshing Around Installer\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 "\nThis script will try and install the Meshing Around Bot and its dependencies.\n"
|
||||
printf "Installer works best in raspian/debian/ubuntu or foxbuntu embedded systems.\n"
|
||||
printf "If there is a problem, try running the installer again.\n"
|
||||
printf "\nChecking for dependencies...\n"
|
||||
|
||||
# check if we are in /opt/meshing-around
|
||||
@@ -16,7 +17,7 @@ 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
|
||||
if [[ $(echo "$move" | grep -i "^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"
|
||||
@@ -36,7 +37,7 @@ if [[ $(hostname) == "femtofox" ]]; then
|
||||
embedded="y"
|
||||
else
|
||||
# check if running on embedded
|
||||
printf "\nAre You installing into an embedded system like a luckfox? (y/n)"
|
||||
printf "\nAre You installing into an embedded system like a luckfox or -native? most should say no here (y/n)"
|
||||
read embedded
|
||||
fi
|
||||
|
||||
@@ -79,6 +80,7 @@ sudo usermod -a -G bluetooth $USER
|
||||
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
|
||||
cp etc/mesh_bot_w3.tmp etc/mesh_bot_w3.service
|
||||
|
||||
# generate config file, check if it exists
|
||||
if [[ -f config.ini ]]; then
|
||||
@@ -89,8 +91,15 @@ fi
|
||||
cp config.template config.ini
|
||||
printf "\nConfig files generated!\n"
|
||||
|
||||
# update lat,long in config.ini
|
||||
latlong=$(curl --silent --max-time 20 https://ipinfo.io/loc || echo "48.50,-123.0")
|
||||
IFS=',' read -r lat lon <<< "$latlong"
|
||||
sed -i "s|lat = 48.50|lat = $lat|g" config.ini
|
||||
sed -i "s|lon = -123.0|lon = $lon|g" config.ini
|
||||
echo "lat,long updated in config.ini to $latlong"
|
||||
|
||||
# check if running on embedded
|
||||
if [[ $(echo "${embedded}" | grep -iq "^y") ]]; then
|
||||
if [[ $(echo "${embedded}" | grep -i "^y") ]]; then
|
||||
printf "\nDetected embedded skipping venv\n"
|
||||
else
|
||||
printf "\nRecomended install is in a python virtual environment, do you want to use venv? (y/n)"
|
||||
@@ -149,29 +158,39 @@ else
|
||||
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
|
||||
# if $1 is passed
|
||||
if [[ $1 == "pong" ]]; then
|
||||
bot="pong"
|
||||
elif [[ $1 == "mesh" ]] || [[ $(echo "${embedded}" | grep -i "^y") ]]; then
|
||||
bot="mesh"
|
||||
else
|
||||
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"
|
||||
echo "Mesh bot is a more complex bot more suited for meshing around"
|
||||
echo "None will skip the service install"
|
||||
read bot
|
||||
fi
|
||||
|
||||
# set the correct path in the service file
|
||||
replace="s|/dir/|$program_path/|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
sed -i $replace etc/mesh_bot_w3.service
|
||||
# set the correct user in the service file?
|
||||
|
||||
#ask if we should add a user for the bot
|
||||
if [[ $(echo "${embedded}" | grep -i "^y") ]]; then
|
||||
if [[ $(echo "${embedded}" | grep -i "^n") ]]; 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
|
||||
sudo groupadd meshbot
|
||||
sudo usermod -a -G meshbot meshbot
|
||||
whoami="meshbot"
|
||||
echo "Added user meshbot with no home directory"
|
||||
sudo usermod -a -G dialout $whoami
|
||||
@@ -190,10 +209,12 @@ replace="s|User=pi|User=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
sed -i $replace etc/mesh_bot.service
|
||||
sed -i $replace etc/mesh_bot_reporting.service
|
||||
sed -i $replace etc/mesh_bot_w3.service
|
||||
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
|
||||
sed -i $replace etc/mesh_bot_w3.service
|
||||
printf "\n service files updated\n"
|
||||
|
||||
if [[ $(echo "${bot}" | grep -i "^p") ]]; then
|
||||
@@ -202,6 +223,7 @@ if [[ $(echo "${bot}" | grep -i "^p") ]]; then
|
||||
sudo systemctl enable pong_bot.service
|
||||
sudo systemctl daemon-reload
|
||||
echo "to start pong bot service: systemctl start pong_bot"
|
||||
service="pong_bot"
|
||||
fi
|
||||
|
||||
if [[ $(echo "${bot}" | grep -i "^m") ]]; then
|
||||
@@ -210,6 +232,7 @@ if [[ $(echo "${bot}" | grep -i "^m") ]]; then
|
||||
sudo systemctl enable mesh_bot.service
|
||||
sudo systemctl daemon-reload
|
||||
echo "to start mesh bot service: systemctl start mesh_bot"
|
||||
service="mesh_bot"
|
||||
fi
|
||||
|
||||
# check if running on embedded for final steps
|
||||
@@ -241,10 +264,25 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# document the service install
|
||||
printf "To install the %s service and keep notes, reference following commands:\n\n" "$service" > install_notes.txt
|
||||
printf "sudo cp %s/etc/%s.service /etc/systemd/system/etc/%s.service\n" "$program_path" "$service" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl daemon-reload\n" >> install_notes.txt
|
||||
printf "sudo systemctl enable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl restart %s.service\n\n" "$service" >> install_notes.txt
|
||||
printf "To see logs and stop the service:\n" >> install_notes.txt
|
||||
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
|
||||
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
|
||||
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n"
|
||||
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n" >> install_notes.txt
|
||||
fi
|
||||
|
||||
read -p "Press enter to complete the installation, these commands saved to install_notes.txt"
|
||||
|
||||
printf "\nGood time to reboot? (y/n)"
|
||||
read reboot
|
||||
if [[ $(echo "${reboot}" | grep -i "^y") ]]; then
|
||||
@@ -255,26 +293,56 @@ else
|
||||
# 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"
|
||||
# replace "# hostname = meshtastic.local" with "hostname = localhost" in config.ini
|
||||
replace="s|# hostname = meshtastic.local|hostname = localhost|g"
|
||||
sed -i "$replace" config.ini
|
||||
printf "\nConfig file updated for embedded\n"
|
||||
# add service dependency for meshtasticd into service file
|
||||
#replace="s|After=network.target|After=network.target meshtasticd.service|g"
|
||||
|
||||
# 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"
|
||||
sudo cp /opt/meshing-around/etc/$service.service /etc/systemd/system/$service.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable $service.service
|
||||
sudo systemctl start $service.service
|
||||
printf "Reference following commands:\n\n" "$service" > install_notes.txt
|
||||
printf "sudo systemctl status %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl restart %s.service\n\n" "$service" >> install_notes.txt
|
||||
printf "To see logs and stop the service:\n" >> install_notes.txt
|
||||
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
fi
|
||||
|
||||
printf "\nInstallation complete!\n"
|
||||
|
||||
exit 0
|
||||
|
||||
# to uninstall the product run the following commands as needed
|
||||
|
||||
# sudo systemctl stop mesh_bot
|
||||
# sudo systemctl disable mesh_bot
|
||||
# sudo systemctl stop pong_bot
|
||||
# sudo systemctl disable pong_bot
|
||||
# sudo systemctl stop mesh_bot_reporting
|
||||
# sudo systemctl disable mesh_bot_reporting
|
||||
# sudo rm /etc/systemd/system/mesh_bot.service
|
||||
# sudo rm /etc/systemd/system/mesh_bot_reporting.service
|
||||
# sudo rm /etc/systemd/system/mesh_bot_w3.service
|
||||
# sudo rm /etc/systemd/system/pong_bot.service
|
||||
# sudo systemctl daemon-reload
|
||||
# sudo systemctl reset-failed
|
||||
|
||||
# sudo gpasswd -d meshbot dialout
|
||||
# sudo gpasswd -d meshbot tty
|
||||
# sudo gpasswd -d meshbot bluetooth
|
||||
# sudo groupdel meshbot
|
||||
# sudo userdel meshbot
|
||||
|
||||
# sudo rm -rf /opt/meshing-around
|
||||
|
||||
|
||||
# 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,26 +1,27 @@
|
||||
# Logs and Reports
|
||||
Logs will collect here. Give a day of logs or a bunch of messages to have good reports.
|
||||
|
||||
Reporting is via [../etc/report_generator5.py](../etc/report_generator5.py). The report_generator5 has newer feel and HTML5 coding. The index.html output is published in [../etc/www](../etc/www) there is a .cfg file created on first run for configuring values as needed.
|
||||
- `multi_log_reader = True` on by default will read all logs (or set to false to return daily logs)
|
||||
## Reporting Note
|
||||
Reporting is via [../etc/report_generator5.py](../etc/report_generator5.py). The report_generator5 has newer feel and HTML5 coding. The index.html output is published in [../etc/www](../etc/www) there is a .cfg file created on first run for configuring values as needed (like moving web root)
|
||||
- Make sure to have `SyslogToFile = True` and default of DEBUG log level to fully enable reporting! ‼️
|
||||
- provided serviceTimer templates in etc/
|
||||
- If you are in a venv and using launch.sh you can `launch.sh html5`
|
||||
|
||||

|
||||
|
||||
## Settings
|
||||
Logging messages to disk or 'Syslog' to disk uses the python native logging function.
|
||||
```
|
||||
```conf
|
||||
[general]
|
||||
# logging to file of the non Bot messages only
|
||||
LogMessagesToFile = False
|
||||
# Logging of system messages to file, needed for reporting engine
|
||||
SyslogToFile = True
|
||||
# logging level for the bot (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
sysloglevel = DEBUG
|
||||
# Number of log files to keep in days, 0 to keep all
|
||||
log_backup_count = 32
|
||||
```
|
||||
## Web Reporting WebServer
|
||||
There is a web-server module. You can run `python3 modules/web.py` from the project root directory and it will serve up the web content.
|
||||
|
||||
To change the stdout (what you see on the console) logging level (default is DEBUG) see the following example, line is in [../modules/log.py](../modules/log.py)
|
||||
|
||||
```
|
||||
# Set level for stdout handler
|
||||
stdout_handler.setLevel(logging.INFO)
|
||||
```
|
||||
find it at. http://localhost:8420
|
||||
216
mesh_bot.py
216
mesh_bot.py
@@ -2,10 +2,15 @@
|
||||
# Meshtastic Autoresponder MESH Bot
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
try:
|
||||
from pubsub import pub
|
||||
except ImportError:
|
||||
print(f"Important dependencies are not met, try install.sh\n\n Did you mean to './launch.sh mesh' using a virtual environment.")
|
||||
exit(1)
|
||||
|
||||
import asyncio
|
||||
import time # for sleep, get some when you can :)
|
||||
import random
|
||||
from pubsub import pub # pip install pubsub, use launch.sh for venv
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
@@ -37,8 +42,11 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"bbspost": lambda: handle_bbspost(message, message_from_id, deviceID),
|
||||
"bbsread": lambda: handle_bbsread(message),
|
||||
"blackjack": lambda: handleBlackJack(message, message_from_id, deviceID),
|
||||
"checkin": lambda: handle_checklist(message, message_from_id, deviceID),
|
||||
"checklist": lambda: handle_checklist(message, message_from_id, deviceID),
|
||||
"checkout": lambda: handle_checklist(message, message_from_id, deviceID),
|
||||
"clearsms": lambda: handle_sms(message_from_id, message),
|
||||
"cmd": lambda: help_message,
|
||||
"cmd": lambda: handle_cmd(message, message_from_id, deviceID),
|
||||
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
@@ -132,8 +140,16 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
time.sleep(responseDelay)
|
||||
return bot_response
|
||||
|
||||
def handle_cmd(message, message_from_id, deviceID):
|
||||
# why CMD? its just a command list. a terminal would normally use "Help"
|
||||
# I didnt want to invoke the word "help" in Meshtastic due to its possible emergency use
|
||||
if " " in message and message.split(" ")[1] in trap_list:
|
||||
return "🤖 just use the commands directly in chat"
|
||||
return help_message
|
||||
|
||||
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
|
||||
global multiPing
|
||||
myNodeNum = globals().get(f'myNodeNum{deviceID}', 777)
|
||||
if "?" in message and isDM:
|
||||
return message.split("?")[0].title() + " command returns SNR and RSSI, or hopcount from your message. Try adding e.g. @place or #tag"
|
||||
|
||||
@@ -153,10 +169,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
msg = random.choice(["✋ACK-ACK!\n", "✋Ack to you!\n"])
|
||||
type = "✋ACK"
|
||||
elif "cqcq" in message.lower() or "cq" in message.lower() or "cqcqcq" in message.lower():
|
||||
if deviceID == 1:
|
||||
myname = get_name_from_number(myNodeNum1, 'short', 1)
|
||||
elif deviceID == 2:
|
||||
myname = get_name_from_number(myNodeNum2, 'short', 2)
|
||||
myname = get_name_from_number(myNodeNum, 'short', deviceID)
|
||||
msg = f"QSP QSL OM DE {myname} K\n"
|
||||
else:
|
||||
msg = "🔊 Can you hear me now?"
|
||||
@@ -221,6 +234,7 @@ def handle_alertBell(message_from_id, deviceID, message):
|
||||
return random.choice(msg)
|
||||
|
||||
def handle_emergency(message_from_id, deviceID, message):
|
||||
myNodeNum = globals().get(f'myNodeNum{deviceID}', 777)
|
||||
# if user in bbs_ban_list return
|
||||
if str(message_from_id) in bbs_ban_list:
|
||||
# silent discard
|
||||
@@ -228,13 +242,11 @@ def handle_emergency(message_from_id, deviceID, message):
|
||||
return ''
|
||||
# trgger alert to emergency_responder_alert_channel
|
||||
if message_from_id != 0:
|
||||
if deviceID == 1: rxNode = myNodeNum1
|
||||
elif deviceID == 2: rxNode = myNodeNum2
|
||||
nodeLocation = get_node_location(message_from_id, deviceID)
|
||||
# if default location is returned set to Unknown
|
||||
if nodeLocation[0] == latitudeValue and nodeLocation[1] == longitudeValue:
|
||||
nodeLocation = ["?", "?"]
|
||||
nodeInfo = f"{get_name_from_number(message_from_id, 'short', deviceID)} detected by {get_name_from_number(rxNode, 'short', deviceID)} lastGPS {nodeLocation[0]}, {nodeLocation[1]}"
|
||||
nodeInfo = f"{get_name_from_number(message_from_id, 'short', deviceID)} detected by {get_name_from_number(myNodeNum, 'short', deviceID)} lastGPS {nodeLocation[0]}, {nodeLocation[1]}"
|
||||
msg = f"🔔🚨Intercepted Possible Emergency Assistance needed for: {nodeInfo}"
|
||||
# alert the emergency_responder_alert_channel
|
||||
time.sleep(responseDelay)
|
||||
@@ -336,8 +348,9 @@ def handle_satpass(message_from_id, deviceID, channel_number, message):
|
||||
return passes
|
||||
|
||||
def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel):
|
||||
global llmRunCounter, llmLocationTable, llmTotalRuntime, cmdHistory
|
||||
global llmRunCounter, llmLocationTable, llmTotalRuntime, cmdHistory, seenNodes
|
||||
location_name = 'no location provided'
|
||||
msg = ''
|
||||
|
||||
if location_enabled:
|
||||
# if message_from_id is is the llmLocationTable use the location from the list to save on API calls
|
||||
@@ -363,17 +376,20 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
|
||||
# consider this a command use for the cmdHistory list
|
||||
cmdHistory.append({'nodeID': message_from_id, 'cmd': 'llm-use', 'time': time.time()})
|
||||
|
||||
# if the message_from_id is not in the llmLocationTable send the welcome message
|
||||
for i in range(0, len(llmLocationTable)):
|
||||
if not any(d['nodeID'] == message_from_id for d in llmLocationTable):
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(welcome_message, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(welcome_message, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
# check for a welcome message (is this redundant?)
|
||||
if not any(node['nodeID'] == message_from_id and node['welcome'] == True for node in seenNodes):
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(welcome_message, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(welcome_message, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
# mark the node as welcomed
|
||||
for node in seenNodes:
|
||||
if node['nodeID'] == message_from_id:
|
||||
node['welcome'] = True
|
||||
|
||||
# update the llmLocationTable for future use
|
||||
for i in range(0, len(llmLocationTable)):
|
||||
@@ -392,26 +408,18 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
|
||||
# information for the user on how long the query will take on average
|
||||
if llmRunCounter > 0:
|
||||
averageRuntime = sum(llmTotalRuntime) / len(llmTotalRuntime)
|
||||
if averageRuntime > 25:
|
||||
msg = f"Please wait, average query time is: {int(averageRuntime)} seconds"
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(msg, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(msg, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
msg = f"Average query time is: {int(averageRuntime)} seconds" if averageRuntime > 25 else ''
|
||||
else:
|
||||
msg = "Please wait, response could take 30+ seconds. Fund the SysOp's GPU budget!"
|
||||
|
||||
if msg != '':
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(msg, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(msg, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
time.sleep(responseDelay)
|
||||
|
||||
start = time.time()
|
||||
|
||||
@@ -493,8 +501,9 @@ def handleLemonade(message, nodeID, deviceID):
|
||||
if highScore != 0:
|
||||
if highScore['userID'] != 0:
|
||||
nodeName = get_name_from_number(highScore['userID'])
|
||||
if nodeName.isnumeric() and interface2_enabled:
|
||||
nodeName = get_name_from_number(highScore['userID'], 'long', 2)
|
||||
if nodeName.isnumeric() and multiple_interface:
|
||||
logger.debug(f"System: TODO is multiple interface fix mention this please nodeName: {nodeName}")
|
||||
#nodeName = get_name_from_number(highScore['userID'], 'long', 2)
|
||||
msg += f" HighScore🥇{nodeName} 💰{round(highScore['cash'], 2)}k "
|
||||
|
||||
msg += start_lemonade(nodeID=nodeID, message=message, celsius=False)
|
||||
@@ -533,8 +542,9 @@ def handleBlackJack(message, nodeID, deviceID):
|
||||
if highScore != 0:
|
||||
if highScore['nodeID'] != 0:
|
||||
nodeName = get_name_from_number(highScore['nodeID'])
|
||||
if nodeName.isnumeric() and interface2_enabled:
|
||||
nodeName = get_name_from_number(highScore['nodeID'], 'long', 2)
|
||||
if nodeName.isnumeric() and multiple_interface:
|
||||
logger.debug(f"System: TODO is multiple interface fix mention this please nodeName: {nodeName}")
|
||||
#nodeName = get_name_from_number(highScore['nodeID'], 'long', 2)
|
||||
msg += f" HighScore🥇{nodeName} with {highScore['highScore']} chips. "
|
||||
time.sleep(responseDelay + 1) # short answers with long replies can cause message collision added wait
|
||||
return msg
|
||||
@@ -568,8 +578,9 @@ def handleVideoPoker(message, nodeID, deviceID):
|
||||
if highScore != 0:
|
||||
if highScore['nodeID'] != 0:
|
||||
nodeName = get_name_from_number(highScore['nodeID'])
|
||||
if nodeName.isnumeric() and interface2_enabled:
|
||||
nodeName = get_name_from_number(highScore['nodeID'], 'long', 2)
|
||||
if nodeName.isnumeric() and multiple_interface:
|
||||
logger.debug(f"System: TODO is multiple interface fix mention this please nodeName: {nodeName}")
|
||||
#nodeName = get_name_from_number(highScore['nodeID'], 'long', 2)
|
||||
msg += f" HighScore🥇{nodeName} with {highScore['highScore']} coins. "
|
||||
|
||||
if last_cmd != "" and nodeID != 0:
|
||||
@@ -689,15 +700,20 @@ def handle_wxc(message_from_id, deviceID, cmd):
|
||||
|
||||
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 enableDEalerts:
|
||||
# nina Alerts
|
||||
return get_nina_alerts()
|
||||
if message.lower().startswith("ealert"):
|
||||
# Detailed alert FEMA
|
||||
return getIpawsAlert(str(location[0]), str(location[1]))
|
||||
else:
|
||||
# Headlines only FEMA
|
||||
return getIpawsAlert(str(location[0]), str(location[1]), shortAlerts=True)
|
||||
|
||||
def handle_checklist(message, message_from_id, deviceID):
|
||||
name = get_name_from_number(message_from_id, 'short', deviceID)
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
return process_checklist_command(message_from_id, message, name, location)
|
||||
|
||||
def handle_bbspost(message, message_from_id, deviceID):
|
||||
if "$" in message and not "example:" in message:
|
||||
@@ -770,7 +786,7 @@ def sysinfo(message, message_from_id, deviceID):
|
||||
return "sysinfo command returns system information."
|
||||
else:
|
||||
if enable_runShellCmd and file_monitor_enabled:
|
||||
shellData = call_external_script(None, "sysEnv.sh").rstrip()
|
||||
shellData = call_external_script(None, "script/sysEnv.sh").rstrip()
|
||||
return get_sysinfo(message_from_id, deviceID) + "\n" + shellData
|
||||
else:
|
||||
return get_sysinfo(message_from_id, deviceID)
|
||||
@@ -960,13 +976,14 @@ def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
|
||||
game = "None"
|
||||
|
||||
trackers = [
|
||||
(dwPlayerTracker, "DopeWars", handleDopeWars),
|
||||
(lemonadeTracker, "LemonadeStand", handleLemonade),
|
||||
(vpTracker, "VideoPoker", handleVideoPoker),
|
||||
(jackTracker, "BlackJack", handleBlackJack),
|
||||
(mindTracker, "MasterMind", handleMmind),
|
||||
(golfTracker, "GolfSim", handleGolf),
|
||||
(dwPlayerTracker, "DopeWars", handleDopeWars) if 'dwPlayerTracker' in globals() else None,
|
||||
(lemonadeTracker, "LemonadeStand", handleLemonade) if 'lemonadeTracker' in globals() else None,
|
||||
(vpTracker, "VideoPoker", handleVideoPoker) if 'vpTracker' in globals() else None,
|
||||
(jackTracker, "BlackJack", handleBlackJack) if 'jackTracker' in globals() else None,
|
||||
(mindTracker, "MasterMind", handleMmind) if 'mindTracker' in globals() else None,
|
||||
(golfTracker, "GolfSim", handleGolf) if 'golfTracker' in globals() else None,
|
||||
]
|
||||
trackers = [tracker for tracker in trackers if tracker is not None]
|
||||
|
||||
for tracker, game_name, handle_game_func in trackers:
|
||||
playingGame, game = check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func)
|
||||
@@ -1000,23 +1017,38 @@ def onReceive(packet, interface):
|
||||
# set the value for the incomming interface
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
if port1 in rxInterface:
|
||||
rxNode = 1
|
||||
elif interface2_enabled and port2 in rxInterface:
|
||||
rxNode = 2
|
||||
if port1 in rxInterface: rxNode = 1
|
||||
elif multiple_interface and port2 in rxInterface: rxNode = 2
|
||||
elif multiple_interface and port3 in rxInterface: rxNode = 3
|
||||
elif multiple_interface and port4 in rxInterface: rxNode = 4
|
||||
elif multiple_interface and port5 in rxInterface: rxNode = 5
|
||||
elif multiple_interface and port6 in rxInterface: rxNode = 6
|
||||
elif multiple_interface and port7 in rxInterface: rxNode = 7
|
||||
elif multiple_interface and port8 in rxInterface: rxNode = 8
|
||||
elif multiple_interface and port9 in rxInterface: rxNode = 9
|
||||
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
if hostname1 in rxHost and interface1_type == 'tcp':
|
||||
rxNode = 1
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
rxNode = 2
|
||||
if hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble':
|
||||
rxNode = 1
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
rxNode = 2
|
||||
if interface1_type == 'ble': rxNode = 1
|
||||
elif multiple_interface and interface2_type == 'ble': rxNode = 2
|
||||
elif multiple_interface and interface3_type == 'ble': rxNode = 3
|
||||
elif multiple_interface and interface4_type == 'ble': rxNode = 4
|
||||
elif multiple_interface and interface5_type == 'ble': rxNode = 5
|
||||
elif multiple_interface and interface6_type == 'ble': rxNode = 6
|
||||
elif multiple_interface and interface7_type == 'ble': rxNode = 7
|
||||
elif multiple_interface and interface8_type == 'ble': rxNode = 8
|
||||
elif multiple_interface and interface9_type == 'ble': rxNode = 9
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if packet.get('channel'):
|
||||
@@ -1048,7 +1080,7 @@ def onReceive(packet, interface):
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
|
||||
if message_from_id in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
|
||||
logger.warning(f"System: Packet from self {message_from_id} loop or traffic replay deteted")
|
||||
|
||||
# get the signal strength and snr if available
|
||||
@@ -1110,7 +1142,7 @@ def onReceive(packet, interface):
|
||||
return
|
||||
|
||||
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
|
||||
if packet['to'] == myNodeNum1 or packet['to'] == myNodeNum2:
|
||||
if packet['to'] in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
|
||||
# message is DM to us
|
||||
isDM = True
|
||||
# check if the message contains a trap word, DMs are always responded to
|
||||
@@ -1166,6 +1198,7 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
# message is on a channel
|
||||
if messageTrap(message_string):
|
||||
# message is for us to respond to
|
||||
if ignoreDefaultChannel and channel_number == publicChannel:
|
||||
logger.debug(f"System: ignoreDefaultChannel CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
else:
|
||||
@@ -1188,7 +1221,7 @@ def onReceive(packet, interface):
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, 0, rxNode)
|
||||
|
||||
else:
|
||||
# message is not for bot to respond to
|
||||
# message is not for us to respond to
|
||||
# ignore the message but add it to the message history list
|
||||
if zuluTime:
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
@@ -1208,18 +1241,29 @@ def onReceive(packet, interface):
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
|
||||
|
||||
# repeat the message on the other device
|
||||
if repeater_enabled and interface2_enabled:
|
||||
if repeater_enabled and multiple_interface:
|
||||
# wait a responseDelay to avoid message collision from lora-ack.
|
||||
time.sleep(responseDelay)
|
||||
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
# if channel found in the repeater list repeat the message
|
||||
if str(channel_number) in repeater_channels:
|
||||
if rxNode == 1:
|
||||
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, 2)
|
||||
elif rxNode == 2:
|
||||
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, 1)
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled', False) and i != rxNode:
|
||||
logger.debug(f"Repeating message on Device{i} Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, i)
|
||||
time.sleep(responseDelay)
|
||||
|
||||
# if QRZ enabled check if we have said hello
|
||||
if qrz_hello_enabled:
|
||||
if never_seen_before(message_from_id):
|
||||
name = {get_name_from_number(message_from_id, 'short', rxNode)}
|
||||
# add to qrz_hello list
|
||||
hello(message_from_id, name)
|
||||
# send a hello message as a DM
|
||||
if not train_qrz:
|
||||
time.sleep(responseDelay)
|
||||
send_message(f"Hello {name} {qrz_hello_string}", channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode)
|
||||
@@ -1229,18 +1273,22 @@ def onReceive(packet, interface):
|
||||
|
||||
async def start_rx():
|
||||
print (CustomFormatter.bold_white + f"\nMeshtastic Autoresponder Bot CTL+C to exit\n" + CustomFormatter.reset)
|
||||
if llm_enabled:
|
||||
logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait")
|
||||
llm_query(" ", myNodeNum1)
|
||||
logger.debug(f"System: LLM model {llmModel} loaded")
|
||||
|
||||
# Start the receive subscriber using pubsub via meshtastic library
|
||||
pub.subscribe(onReceive, 'meshtastic.receive')
|
||||
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
|
||||
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
|
||||
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
|
||||
if interface2_enabled:
|
||||
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
|
||||
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
|
||||
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled', False):
|
||||
myNodeNum = globals().get(f'myNodeNum{i}', 0)
|
||||
logger.info(f"System: Autoresponder Started for Device{i} {get_name_from_number(myNodeNum, 'long', i)},"
|
||||
f"{get_name_from_number(myNodeNum, 'short', i)}. NodeID: {myNodeNum}, {decimal_to_hex(myNodeNum)}")
|
||||
|
||||
if llm_enabled:
|
||||
logger.debug(f"System: Ollama LLM Enabled, loading model {llmModel} please wait")
|
||||
llm_query(" ")
|
||||
logger.debug(f"System: LLM model {llmModel} loaded")
|
||||
|
||||
if log_messages_to_file:
|
||||
logger.debug("System: Logging Messages to disk")
|
||||
if syslog_to_file:
|
||||
@@ -1273,7 +1321,7 @@ async def start_rx():
|
||||
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
|
||||
if useDMForResponse:
|
||||
logger.debug(f"System: Respond by DM only")
|
||||
if repeater_enabled and interface2_enabled:
|
||||
if repeater_enabled and multiple_interface:
|
||||
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
|
||||
if radio_detection_enabled:
|
||||
logger.debug(f"System: Radio Detection Enabled using rigctld at {rigControlServerAddress} brodcasting to channels: {sigWatchBroadcastCh} for {get_freq_common_name(get_hamlib('f'))}")
|
||||
@@ -1291,6 +1339,12 @@ async def start_rx():
|
||||
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 qrz_hello_enabled and train_qrz:
|
||||
logger.debug(f"System: QRZ Welcome/Hello Enabled with training mode")
|
||||
if qrz_hello_enabled and not train_qrz:
|
||||
logger.debug(f"System: QRZ Welcome/Hello Enabled")
|
||||
if checklist_enabled:
|
||||
logger.debug(f"System: CheckList Module Enabled")
|
||||
if enableSMTP:
|
||||
if enableImap:
|
||||
logger.debug(f"System: SMTP Email Alerting Enabled using IMAP")
|
||||
|
||||
42
modules/README.md
Normal file
42
modules/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Modules and Adding stuff
|
||||
|
||||
To help with code testing see `etc/simulator.py` to simulate a bot. I also enjoy meshtasticd(linux-native) in noradio with MQTT server and client to just emulate a mesh.
|
||||
|
||||
## By following these steps, you can add a new bbs option to the bot.
|
||||
|
||||
1. **Define the Command Handler**:
|
||||
Add a new function in mesh_bot.py to handle the new command. For example, if you want to add a command `newcommand`:
|
||||
```python
|
||||
def handle_newcommand(message, message_from_id, deviceID):
|
||||
return "This is a response from the new command."
|
||||
```
|
||||
Additionally you can add a whole new module.py, I recommend doing this if you need to import more stuff, try and wedge it into similar spots if you can. You will need to import the file as well, look further at `modules/system.py` for more.
|
||||
2. **Add the Command to the Auto Response**:
|
||||
Update the auto_response function in mesh_bot.py to include the new command:
|
||||
```python
|
||||
def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_number, deviceID, isDM):
|
||||
#...
|
||||
"newcommand": lambda: handle_newcommand(message, message_from_id, deviceID),
|
||||
#...
|
||||
```
|
||||
3. **Update the Trap List and Help**:
|
||||
A quick way to do this is to edit the line 16/17 in `modules/system.py` to include the new command:
|
||||
```python
|
||||
#...
|
||||
trap_list = ("cmd", "cmd?", "newcommand") # default trap list, with the new command added
|
||||
help_message = "Bot CMD?:newcommand, "
|
||||
#...
|
||||
```
|
||||
|
||||
**If looking to merge** the prefered way would be to update `modules/system.py` Adding this block below `ping` which ends around line 28:
|
||||
```python
|
||||
# newcommand Configuration
|
||||
newcommand_enabled = True # settings.py handles the config.ini values; this is a placeholder
|
||||
if newcommand_enabled:
|
||||
trap_list_newcommand = ("newcommand",)
|
||||
trap_list = trap_list + trap_list_newcommand
|
||||
help_message = help_message + ", newcommand"
|
||||
```
|
||||
|
||||
5. **Test the New Command**:
|
||||
Run MeshBot and test the new command by sending a message with the command `newcommand` to ensure it responds correctly.
|
||||
160
modules/checklist.py
Normal file
160
modules/checklist.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# Checkin Checkout database module for the bot
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import sqlite3
|
||||
from modules.log import *
|
||||
import time
|
||||
|
||||
trap_list_checklist = ("checkin", "checkout", "checklist", "purgein", "purgeout")
|
||||
|
||||
def initialize_checklist_database():
|
||||
# create the database
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
# Check if the checkin table exists, and create it if it doesn't
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS checkin
|
||||
(checkin_id INTEGER PRIMARY KEY, checkin_name TEXT, checkin_date TEXT, checkin_time TEXT, location TEXT, checkin_notes TEXT)''')
|
||||
# Check if the checkout table exists, and create it if it doesn't
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS checkout
|
||||
(checkout_id INTEGER PRIMARY KEY, checkout_name TEXT, checkout_date TEXT, checkout_time TEXT, location TEXT, checkout_notes TEXT)''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.debug("System: Ensured data/checklist.db exists with required tables")
|
||||
|
||||
def checkin(name, date, time, location, notes):
|
||||
location = ", ".join(map(str, location))
|
||||
# checkin a user
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("INSERT INTO checkin (checkin_name, checkin_date, checkin_time, location, checkin_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time, location, notes))
|
||||
# # remove any checkouts that are older than the checkin
|
||||
# c.execute("DELETE FROM checkout WHERE checkout_date < ? OR (checkout_date = ? AND checkout_time < ?)", (date, date, time))
|
||||
except sqlite3.OperationalError as e:
|
||||
if "no such table" in str(e):
|
||||
initialize_checklist_database()
|
||||
c.execute("INSERT INTO checkin (checkin_name, checkin_date, checkin_time, location, checkin_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time, location, notes))
|
||||
else:
|
||||
raise
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if reverse_in_out:
|
||||
return "Checked✅Out: " + str(name)
|
||||
else:
|
||||
return "Checked✅In: " + str(name)
|
||||
|
||||
def delete_checkin(checkin_id):
|
||||
# delete a checkin
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
c.execute("DELETE FROM checkin WHERE checkin_id = ?", (checkin_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return "Checkin deleted." + str(checkin_id)
|
||||
|
||||
def checkout(name, date, time_str, location, notes):
|
||||
location = ", ".join(map(str, location))
|
||||
# checkout a user
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
# Check if the user has a checkin before checking out
|
||||
c.execute("""
|
||||
SELECT checkin_id FROM checkin
|
||||
WHERE checkin_name = ?
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM checkout
|
||||
WHERE checkout_name = checkin_name
|
||||
AND (checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time))
|
||||
)
|
||||
ORDER BY checkin_date DESC, checkin_time DESC
|
||||
LIMIT 1
|
||||
""", (name,))
|
||||
checkin_record = c.fetchone()
|
||||
if checkin_record:
|
||||
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time_str, location, notes))
|
||||
# calculate length of time checked in
|
||||
c.execute("SELECT checkin_time FROM checkin WHERE checkin_id = ?", (checkin_record[0],))
|
||||
checkin_time = c.fetchone()[0]
|
||||
checkin_datetime = time.strptime(date + " " + checkin_time, "%Y-%m-%d %H:%M:%S")
|
||||
time_checked_in_seconds = time.time() - time.mktime(checkin_datetime)
|
||||
timeCheckedIn = time.strftime("%H:%M:%S", time.gmtime(time_checked_in_seconds))
|
||||
# # remove the checkin record older than the checkout
|
||||
# c.execute("DELETE FROM checkin WHERE checkin_date < ? OR (checkin_date = ? AND checkin_time < ?)", (date, date, time_str))
|
||||
except sqlite3.OperationalError as e:
|
||||
if "no such table" in str(e):
|
||||
initialize_checklist_database()
|
||||
c.execute("INSERT INTO checkout (checkout_name, checkout_date, checkout_time, location, checkout_notes) VALUES (?, ?, ?, ?, ?)", (name, date, time_str, location, notes))
|
||||
else:
|
||||
raise
|
||||
conn.commit()
|
||||
conn.close()
|
||||
if checkin_record:
|
||||
if reverse_in_out:
|
||||
return "Checked⌛️In: " + str(name) + " duration " + timeCheckedIn
|
||||
else:
|
||||
return "Checked⌛️Out: " + str(name) + " duration " + timeCheckedIn
|
||||
else:
|
||||
return "None found for " + str(name)
|
||||
|
||||
def delete_checkout(checkout_id):
|
||||
# delete a checkout
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
c.execute("DELETE FROM checkout WHERE checkout_id = ?", (checkout_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return "Checkout deleted." + str(checkout_id)
|
||||
|
||||
def list_checkin():
|
||||
# list checkins
|
||||
conn = sqlite3.connect(checklist_db)
|
||||
c = conn.cursor()
|
||||
c.execute("""
|
||||
SELECT * FROM checkin
|
||||
WHERE checkin_id NOT IN (
|
||||
SELECT checkin_id FROM checkout
|
||||
WHERE checkout_date > checkin_date OR (checkout_date = checkin_date AND checkout_time > checkin_time)
|
||||
)
|
||||
""")
|
||||
rows = c.fetchall()
|
||||
conn.close()
|
||||
timeCheckedIn = ""
|
||||
checkin_list = ""
|
||||
for row in rows:
|
||||
#calculate length of time checked in
|
||||
timeCheckedIn = time.strftime("%H:%M:%S", time.gmtime(time.time() - time.mktime(time.strptime(row[2] + " " + row[3], "%Y-%m-%d %H:%M:%S"))))
|
||||
checkin_list += "ID: " + row[1] + " checked-In for " + timeCheckedIn
|
||||
if row[5] != "":
|
||||
checkin_list += "📝" + row[5]
|
||||
if row != rows[-1]:
|
||||
checkin_list += "\n"
|
||||
# if empty list
|
||||
if checkin_list == "":
|
||||
return "No data to display."
|
||||
return checkin_list
|
||||
|
||||
def process_checklist_command(nodeID, message, name="none", location="none"):
|
||||
current_date = time.strftime("%Y-%m-%d")
|
||||
current_time = time.strftime("%H:%M:%S")
|
||||
# if user on bbs_ban_list reject command
|
||||
if str(nodeID) in bbs_ban_list:
|
||||
logger.warning("System: Checklist attempt from the ban list")
|
||||
return "unable to process command"
|
||||
try:
|
||||
comment = message.split(" ", 1)[1]
|
||||
except IndexError:
|
||||
comment = ""
|
||||
# handle checklist commands
|
||||
if ("checkin" in message.lower() and not reverse_in_out) or ("checkout" in message.lower() and reverse_in_out):
|
||||
return checkin(name, current_date, current_time, location, comment)
|
||||
elif ("checkout" in message.lower() and not reverse_in_out) or ("checkin" in message.lower() and reverse_in_out):
|
||||
return checkout(name, current_date, current_time, location, comment)
|
||||
elif "purgein" in message.lower():
|
||||
return delete_checkin(nodeID)
|
||||
elif "purgeout" in message.lower():
|
||||
return delete_checkout(nodeID)
|
||||
elif "checklist" in message.lower():
|
||||
return list_checkin()
|
||||
else:
|
||||
return "Invalid command."
|
||||
@@ -63,7 +63,7 @@ async def watch_file():
|
||||
return content
|
||||
await asyncio.sleep(1) # Check every
|
||||
|
||||
def call_external_script(message, script="runShell.sh"):
|
||||
def call_external_script(message, script="script/runShell.sh"):
|
||||
try:
|
||||
# Debugging: Print the current working directory and resolved script path
|
||||
current_working_directory = os.getcwd()
|
||||
|
||||
@@ -10,8 +10,9 @@ import xml.dom.minidom
|
||||
from modules.log import *
|
||||
|
||||
trap_list_location_eu = ("ukalert", "ukwx", "ukflood")
|
||||
trap_list_location_de = ("dealert", "dewx", "deflood")
|
||||
|
||||
def get_govUK_alerts(shortAlerts=False):
|
||||
def get_govUK_alerts(lat, lon):
|
||||
try:
|
||||
# get UK.gov alerts
|
||||
url = 'https://www.gov.uk/alerts'
|
||||
@@ -27,7 +28,23 @@ def get_govUK_alerts(shortAlerts=False):
|
||||
return "🚨" + alert.get_text(strip=True)
|
||||
else:
|
||||
return NO_ALERTS
|
||||
|
||||
|
||||
def get_nina_alerts():
|
||||
try:
|
||||
# get api.bund.dev alerts
|
||||
alerts = []
|
||||
for regionalKey in myRegionalKeysDE:
|
||||
url = ("https://nina.api.proxy.bund.dev/api31/dashboard/" + regionalKey + ".json")
|
||||
response = requests.get(url)
|
||||
data = response.json()
|
||||
|
||||
for item in data:
|
||||
title = item["i18nTitle"]["de"]
|
||||
alerts.append(f"🚨 {title}")
|
||||
return "\n".join(alerts) if alerts else NO_ALERTS
|
||||
except Exception as e:
|
||||
logger.warning("Error getting NINA DE alerts: " + str(e))
|
||||
return NO_ALERTS
|
||||
|
||||
def get_wxUKgov():
|
||||
# get UK weather warnings
|
||||
@@ -57,4 +74,4 @@ def get_floodUKgov():
|
||||
# get UK flood warnings
|
||||
url = 'https://environment.data.gov.uk/flood-widgets/rss/feed-England.xml'
|
||||
|
||||
return NO_ALERTS
|
||||
return NO_ALERTS
|
||||
|
||||
@@ -211,6 +211,10 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
if result.status_code == 200:
|
||||
result_json = result.json()
|
||||
result = result_json.get("response", "")
|
||||
|
||||
# deepseek-r1 has added <think> </think> tags to the response
|
||||
if "<think>" in result:
|
||||
result = result.split("</think>")[1]
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code}")
|
||||
|
||||
|
||||
@@ -71,7 +71,16 @@ def where_am_i(lat=0, lon=0, short=False, zip=False):
|
||||
def getRepeaterBook(lat=0, lon=0):
|
||||
grid = mh.to_maiden(float(lat), float(lon))
|
||||
data = []
|
||||
repeater_url = f"https://www.repeaterbook.com/repeaters/prox_result.php?city={grid}&lat=&long=&distance=50&Dunit=m&band%5B%5D=4&band%5B%5D=16&freq=&call=&mode%5B%5D=1&mode%5B%5D=2&mode%5B%5D=4&mode%5B%5D=64&status_id=1&use=%25&use=OPEN&order=distance_calc%2C+state_id+ASC"
|
||||
# check if in the US or not
|
||||
usapi ="https://www.repeaterbook.com/repeaters/prox_result.php?"
|
||||
elsewhereapi = "https://www.repeaterbook.com/row_repeaters/prox2_result.php?"
|
||||
if grid[:2] in ['CN', 'DN', 'EN', 'FN', 'CM', 'DM', 'EM', 'FM', 'DL', 'EL', 'FL']:
|
||||
repeater_url = usapi
|
||||
else:
|
||||
repeater_url = elsewhereapi
|
||||
|
||||
repeater_url += f"city={grid}&lat=&long=&distance=50&Dunit=m&band%5B%5D=4&band%5B%5D=16&freq=&call=&mode%5B%5D=1&mode%5B%5D=2&mode%5B%5D=4&mode%5B%5D=64&status_id=1&use=%25&use=OPEN&order=distance_calc%2C+state_id+ASC"
|
||||
|
||||
try:
|
||||
msg = ''
|
||||
response = requests.get(repeater_url)
|
||||
@@ -95,10 +104,8 @@ def getRepeaterBook(lat=0, lon=0):
|
||||
'direction': cells[i + 9].text.strip() if i + 9 < len(cells) else 'N/A'
|
||||
}
|
||||
data.append(repeater)
|
||||
else:
|
||||
msg = "bug?Not enough columns"
|
||||
else:
|
||||
msg = "bug?Table not found"
|
||||
msg = "No Data for your Region"
|
||||
except Exception as e:
|
||||
msg = "No repeaters found 😔"
|
||||
# Limit the output to the first 4 repeaters
|
||||
@@ -357,11 +364,13 @@ def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
|
||||
|
||||
alerts = ""
|
||||
alertxml = xml.dom.minidom.parseString(alert_data.text)
|
||||
|
||||
for i in alertxml.getElementsByTagName("entry"):
|
||||
alerts += (
|
||||
i.getElementsByTagName("title")[0].childNodes[0].nodeValue + "\n"
|
||||
)
|
||||
title = i.getElementsByTagName("title")[0].childNodes[0].nodeValue
|
||||
area_desc = i.getElementsByTagName("cap:areaDesc")[0].childNodes[0].nodeValue
|
||||
if enableExtraLocationWx:
|
||||
alerts += f"{title}. {area_desc.replace(' ', '')}\n"
|
||||
else:
|
||||
alerts += f"{title}\n"
|
||||
|
||||
if alerts == "" or alerts == None:
|
||||
return NO_ALERTS
|
||||
@@ -520,7 +529,7 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
if geocode_type == "SAME":
|
||||
sameVal = geocode_value
|
||||
except Exception as e:
|
||||
logger.warning(f"System: iPAWS Error extracting alert data: {link}")
|
||||
logger.debug(f"System: iPAWS Error extracting alert data: {link}")
|
||||
#print(f"DEBUG: {info.toprettyxml()}")
|
||||
continue
|
||||
|
||||
@@ -599,7 +608,7 @@ def get_flood_noaa(lat=0, lon=0, uid=0):
|
||||
# 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))
|
||||
logger.debug("Location:Error extracting flood gauge data from NOAA for " + str(uid))
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# format the flood data
|
||||
|
||||
@@ -3,6 +3,11 @@ from logging.handlers import TimedRotatingFileHandler
|
||||
import re
|
||||
from datetime import datetime
|
||||
from modules.settings import *
|
||||
# if LOGGING_LEVEL is not set in settings.py, default to DEBUG
|
||||
if not LOGGING_LEVEL:
|
||||
LOGGING_LEVEL = "DEBUG"
|
||||
|
||||
LOGGING_LEVEL = getattr(logging, LOGGING_LEVEL)
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
grey = '\x1b[38;21m'
|
||||
@@ -41,7 +46,7 @@ class plainFormatter(logging.Formatter):
|
||||
|
||||
# Create logger
|
||||
logger = logging.getLogger("MeshBot System Logger")
|
||||
logger.setLevel(logging.DEBUG)
|
||||
logger.setLevel(LOGGING_LEVEL)
|
||||
logger.propagate = False
|
||||
|
||||
msgLogger = logging.getLogger("MeshBot Messages Logger")
|
||||
@@ -56,7 +61,7 @@ today = datetime.now()
|
||||
# Create stdout handler for logging to the console
|
||||
stdout_handler = logging.StreamHandler()
|
||||
# Set level for stdout handler (logs DEBUG level and above)
|
||||
stdout_handler.setLevel(logging.DEBUG)
|
||||
stdout_handler.setLevel(LOGGING_LEVEL)
|
||||
# Set format for stdout handler
|
||||
stdout_handler.setFormatter(CustomFormatter(logFormat))
|
||||
# Add handlers to the logger
|
||||
@@ -65,7 +70,7 @@ logger.addHandler(stdout_handler)
|
||||
if syslog_to_file:
|
||||
# Create file handler for logging to a file
|
||||
file_handler_sys = TimedRotatingFileHandler('logs/meshbot.log', when='midnight', backupCount=log_backup_count)
|
||||
file_handler_sys.setLevel(logging.DEBUG) # DEBUG used by default for system logs to disk
|
||||
file_handler_sys.setLevel(LOGGING_LEVEL) # DEBUG used by default for system logs to disk
|
||||
file_handler_sys.setFormatter(plainFormatter(logFormat))
|
||||
logger.addHandler(file_handler_sys)
|
||||
|
||||
|
||||
57
modules/qrz.py
Normal file
57
modules/qrz.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# Module to respomnd to new nodes we havent seen before with a hello message
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import sqlite3
|
||||
from modules.log import *
|
||||
|
||||
def initalize_qrz_database():
|
||||
# create the database
|
||||
conn = sqlite3.connect(qrz_db)
|
||||
c = conn.cursor()
|
||||
# Check if the qrz table exists, and create it if it doesn't
|
||||
c.execute('''CREATE TABLE IF NOT EXISTS qrz
|
||||
(qrz_id INTEGER PRIMARY KEY, qrz_call TEXT, qrz_name TEXT, qrz_qth TEXT, qrz_notes TEXT)''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def never_seen_before(nodeID):
|
||||
# check if we have seen this node before and sent a hello message
|
||||
conn = sqlite3.connect(qrz_db)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("SELECT * FROM qrz WHERE qrz_call = ?", (nodeID,))
|
||||
row = c.fetchone()
|
||||
conn.close()
|
||||
if row is None:
|
||||
# we have not seen this node before
|
||||
return True
|
||||
else:
|
||||
# we have seen this node before
|
||||
return False
|
||||
except sqlite3.OperationalError as e:
|
||||
if "no such table" in str(e):
|
||||
initalize_qrz_database()
|
||||
logger.warning("QRZ database table not found, created new table")
|
||||
# we have not seen this node before
|
||||
return True
|
||||
else:
|
||||
raise
|
||||
|
||||
def hello(nodeID, name):
|
||||
# send a hello message
|
||||
conn = sqlite3.connect(qrz_db)
|
||||
c = conn.cursor()
|
||||
try:
|
||||
c.execute("INSERT INTO qrz (qrz_call, qrz_name) VALUES (?, ?)", (nodeID, str(name)))
|
||||
except sqlite3.OperationalError as e:
|
||||
if "no such table" in str(e):
|
||||
initalize_qrz_database()
|
||||
c.execute("INSERT INTO qrz (qrz_call, qrz_name) VALUES (?, ?)", (nodeID, str(name)))
|
||||
else:
|
||||
raise
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -91,11 +91,20 @@ if 'smtp' not in config:
|
||||
config['smtp'] = {'sysopEmails': '', 'enableSMTP': 'False', 'enableImap': 'False'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'checklist' not in config:
|
||||
config['checklist'] = {'enabled': 'False', 'checklist_db': 'data/checklist.db'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
if 'qrz' not in config:
|
||||
config['qrz'] = {'enabled': 'False', 'qrz_db': 'data/qrz.db', 'qrz_hello_string': 'send CMD or DM me for more info.'}
|
||||
config.write(open(config_file, 'w'))
|
||||
|
||||
# interface1 settings
|
||||
interface1_type = config['interface'].get('type', 'serial')
|
||||
port1 = config['interface'].get('port', '')
|
||||
hostname1 = config['interface'].get('hostname', '')
|
||||
mac1 = config['interface'].get('mac', '')
|
||||
interface1_enabled = True # gotta have at least one interface
|
||||
|
||||
# interface2 settings
|
||||
if 'interface2' in config:
|
||||
@@ -107,6 +116,81 @@ if 'interface2' in config:
|
||||
else:
|
||||
interface2_enabled = False
|
||||
|
||||
# interface3 settings
|
||||
if 'interface3' in config:
|
||||
interface3_type = config['interface3'].get('type', 'serial')
|
||||
port3 = config['interface3'].get('port', '')
|
||||
hostname3 = config['interface3'].get('hostname', '')
|
||||
mac3 = config['interface3'].get('mac', '')
|
||||
interface3_enabled = config['interface3'].getboolean('enabled', False)
|
||||
else:
|
||||
interface3_enabled = False
|
||||
|
||||
# interface4 settings
|
||||
if 'interface4' in config:
|
||||
interface4_type = config['interface4'].get('type', 'serial')
|
||||
port4 = config['interface4'].get('port', '')
|
||||
hostname4 = config['interface4'].get('hostname', '')
|
||||
mac4 = config['interface4'].get('mac', '')
|
||||
interface4_enabled = config['interface4'].getboolean('enabled', False)
|
||||
else:
|
||||
interface4_enabled = False
|
||||
|
||||
# interface5 settings
|
||||
if 'interface5' in config:
|
||||
interface5_type = config['interface5'].get('type', 'serial')
|
||||
port5 = config['interface5'].get('port', '')
|
||||
hostname5 = config['interface5'].get('hostname', '')
|
||||
mac5 = config['interface5'].get('mac', '')
|
||||
interface5_enabled = config['interface5'].getboolean('enabled', False)
|
||||
else:
|
||||
interface5_enabled = False
|
||||
|
||||
# interface6 settings
|
||||
if 'interface6' in config:
|
||||
interface6_type = config['interface6'].get('type', 'serial')
|
||||
port6 = config['interface6'].get('port', '')
|
||||
hostname6 = config['interface6'].get('hostname', '')
|
||||
mac6 = config['interface6'].get('mac', '')
|
||||
interface6_enabled = config['interface6'].getboolean('enabled', False)
|
||||
else:
|
||||
interface6_enabled = False
|
||||
|
||||
# interface7 settings
|
||||
if 'interface7' in config:
|
||||
interface7_type = config['interface7'].get('type', 'serial')
|
||||
port7 = config['interface7'].get('port', '')
|
||||
hostname7 = config['interface7'].get('hostname', '')
|
||||
mac7 = config['interface7'].get('mac', '')
|
||||
interface7_enabled = config['interface7'].getboolean('enabled', False)
|
||||
else:
|
||||
interface7_enabled = False
|
||||
|
||||
# interface8 settings
|
||||
if 'interface8' in config:
|
||||
interface8_type = config['interface8'].get('type', 'serial')
|
||||
port8 = config['interface8'].get('port', '')
|
||||
hostname8 = config['interface8'].get('hostname', '')
|
||||
mac8 = config['interface8'].get('mac', '')
|
||||
interface8_enabled = config['interface8'].getboolean('enabled', False)
|
||||
else:
|
||||
interface8_enabled = False
|
||||
|
||||
# interface9 settings
|
||||
if 'interface9' in config:
|
||||
interface9_type = config['interface9'].get('type', 'serial')
|
||||
port9 = config['interface9'].get('port', '')
|
||||
hostname9 = config['interface9'].get('hostname', '')
|
||||
mac9 = config['interface9'].get('mac', '')
|
||||
interface9_enabled = config['interface9'].getboolean('enabled', False)
|
||||
else:
|
||||
interface9_enabled = False
|
||||
|
||||
multiple_interface = False
|
||||
if interface2_enabled or interface3_enabled or interface4_enabled or interface5_enabled or interface6_enabled or interface7_enabled or interface8_enabled or interface9_enabled:
|
||||
multiple_interface = True
|
||||
|
||||
|
||||
# variables from the config.ini file
|
||||
try:
|
||||
# general
|
||||
@@ -117,6 +201,7 @@ try:
|
||||
log_messages_to_file = config['general'].getboolean('LogMessagesToFile', False) # default off
|
||||
log_backup_count = config['general'].getint('LogBackupCount', 32) # default 32 days
|
||||
syslog_to_file = config['general'].getboolean('SyslogToFile', True) # default on
|
||||
LOGGING_LEVEL = config['general'].get('sysloglevel', 'DEBUG') # default DEBUG
|
||||
urlTimeoutSeconds = config['general'].getint('urlTimeout', 10) # default 10 seconds
|
||||
store_forward_enabled = config['general'].getboolean('StoreForward', True)
|
||||
storeFlimit = config['general'].getint('StoreLimit', 3) # default 3 messages for S&F
|
||||
@@ -136,7 +221,6 @@ try:
|
||||
llm_enabled = config['general'].getboolean('ollama', False) # https://ollama.com
|
||||
llmModel = config['general'].get('ollamaModel', 'gemma2:2b') # default gemma2:2b
|
||||
ollamaHostName = config['general'].get('ollamaHostName', 'http://localhost:11434') # default localhost
|
||||
|
||||
# emergency response
|
||||
emergency_responder_enabled = config['emergencyHandler'].getboolean('enabled', False)
|
||||
emergency_responder_alert_channel = config['emergencyHandler'].getint('alert_channel', 2) # default 2
|
||||
@@ -165,10 +249,13 @@ try:
|
||||
emergencyAlertBrodcastEnabled = config['location'].getboolean('eAlertBroadcastEnabled', False) # default False
|
||||
wxAlertBroadcastEnabled = config['location'].getboolean('wxAlertBroadcastEnabled', False) # default False
|
||||
enableGBalerts = config['location'].getboolean('enableGBalerts', False) # default False
|
||||
enableDEalerts = config['location'].getboolean('enableDEalerts', False) # default False
|
||||
wxAlertsEnabled = config['location'].getboolean('NOAAalertsEnabled', True) # default True
|
||||
mySAME = config['location'].get('mySAME', '').split(',') # default empty
|
||||
myRegionalKeysDE = config['location'].get('myRegionalKeysDE', '110000000000').split(',') # default city Berlin
|
||||
forecastDuration = config['location'].getint('NOAAforecastDuration', 4) # NOAA forcast days
|
||||
numWxAlerts = config['location'].getint('NOAAalertCount', 2) # default 2 alerts
|
||||
enableExtraLocationWx = config['location'].getboolean('enableExtraLocationWx', False) # default False
|
||||
ipawsPIN = config['location'].get('ipawsPIN', '000000') # default 000000
|
||||
ignoreFEMAtest = config['location'].getboolean('ignoreFEMAtest', True) # default True
|
||||
wxAlertBroadcastChannel = config['location'].get('wxAlertBroadcastCh', '2').split(',') # default Channel 2
|
||||
@@ -181,7 +268,18 @@ try:
|
||||
bbs_admin_list = config['bbs'].get('bbs_admin_list', '').split(',')
|
||||
bbs_link_enabled = config['bbs'].getboolean('bbslink_enabled', False)
|
||||
bbs_link_whitelist = config['bbs'].get('bbslink_whitelist', '').split(',')
|
||||
|
||||
# checklist
|
||||
checklist_enabled = config['checklist'].getboolean('enabled', False)
|
||||
checklist_db = config['checklist'].get('checklist_db', 'data/checklist.db')
|
||||
reverse_in_out = config['checklist'].getboolean('reverse_in_out', False)
|
||||
|
||||
# qrz hello
|
||||
qrz_hello_enabled = config['qrz'].getboolean('enabled', False)
|
||||
qrz_db = config['qrz'].get('qrz_db', 'data/qrz.db')
|
||||
qrz_hello_string = config['qrz'].get('qrz_hello_string', 'send CMD or DM me for more info.')
|
||||
train_qrz = config['qrz'].getboolean('training', True)
|
||||
|
||||
# E-Mail Settings
|
||||
sysopEmails = config['smtp'].get('sysopEmails', '').split(',')
|
||||
enableSMTP = config['smtp'].getboolean('enableSMTP', False)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# helper functions and init for system related tasks
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
import meshtastic.serial_interface #pip install meshtastic
|
||||
import meshtastic.serial_interface #pip install meshtastic or use launch.sh for venv
|
||||
import meshtastic.tcp_interface
|
||||
import meshtastic.ble_interface
|
||||
import time
|
||||
@@ -75,10 +75,14 @@ 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:
|
||||
if enableGBalerts and not enableDEalerts:
|
||||
from modules.globalalert import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_location_eu
|
||||
logger.warning(f"System: GB Alerts not functional at this time need to find a source API")
|
||||
#help_message = help_message + ", ukalert, ukwx, ukflood"
|
||||
if enableDEalerts and not enableGBalerts:
|
||||
from modules.globalalert import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_location_de
|
||||
#help_message = help_message + ", dealert, dewx, deflood"
|
||||
|
||||
# Open-Meteo Configuration for worldwide weather
|
||||
if use_meteo_wxApi:
|
||||
@@ -186,12 +190,24 @@ if store_forward_enabled:
|
||||
trap_list = trap_list + ("messages",)
|
||||
help_message = help_message + ", messages"
|
||||
|
||||
# QRZ Configuration
|
||||
if qrz_hello_enabled:
|
||||
from modules.qrz import * # from the spudgunman/meshing-around repo
|
||||
#trap_list = trap_list + trap_list_qrz # items qrz, qrz?, qrzcall
|
||||
#help_message = help_message + ", qrz"
|
||||
|
||||
# CheckList Configuration
|
||||
if checklist_enabled:
|
||||
from modules.checklist import * # from the spudgunman/meshing-around repo
|
||||
trap_list = trap_list + trap_list_checklist # items checkin, checkout, checklist, purgein, purgeout
|
||||
help_message = help_message + ", checkin, checkout"
|
||||
|
||||
# Radio Monitor Configuration
|
||||
if radio_detection_enabled:
|
||||
from modules.radio import * # from the spudgunman/meshing-around repo
|
||||
|
||||
# File Monitor Configuration
|
||||
if file_monitor_enabled or read_news_enabled:
|
||||
if file_monitor_enabled or read_news_enabled or bee_enabled:
|
||||
from modules.filemon import * # from the spudgunman/meshing-around repo
|
||||
if read_news_enabled:
|
||||
trap_list = trap_list + trap_list_filemon # items readnews
|
||||
@@ -209,62 +225,45 @@ if len(help_message) > 20:
|
||||
help_message = ", ".join(help_message)
|
||||
|
||||
# BLE dual interface prevention
|
||||
if interface1_type == 'ble' and interface2_type == 'ble':
|
||||
logger.critical(f"System: BLE Interface1 and Interface2 cannot both be BLE. Exiting")
|
||||
ble_count = sum(1 for i in range(1, 10) if globals().get(f'interface{i}_type') == 'ble')
|
||||
if ble_count > 1:
|
||||
logger.critical(f"System: Multiple BLE interfaces detected. Only one BLE interface is allowed. Exiting")
|
||||
exit()
|
||||
|
||||
#initialize_interfaces():
|
||||
# Interface1 Configuration
|
||||
try:
|
||||
logger.debug(f"System: Initializing Interface1")
|
||||
if interface1_type == 'serial':
|
||||
interface1 = meshtastic.serial_interface.SerialInterface(port1)
|
||||
elif interface1_type == 'tcp':
|
||||
interface1 = meshtastic.tcp_interface.TCPInterface(hostname1)
|
||||
elif interface1_type == 'ble':
|
||||
interface1 = meshtastic.ble_interface.BLEInterface(mac1)
|
||||
# Initialize interfaces
|
||||
logger.debug(f"System: Initializing Interfaces")
|
||||
interface1 = interface2 = interface3 = interface4 = interface5 = interface6 = interface7 = interface8 = interface9 = None
|
||||
retry_int1 = retry_int2 = retry_int3 = retry_int4 = retry_int5 = retry_int6 = retry_int7 = retry_int8 = retry_int9 = False
|
||||
for i in range(1, 10):
|
||||
interface_type = globals().get(f'interface{i}_type')
|
||||
if not interface_type or interface_type == 'none' or globals().get(f'interface{i}_enabled') == False:
|
||||
# no valid interface found
|
||||
continue
|
||||
try:
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
if interface_type == 'serial':
|
||||
globals()[f'interface{i}'] = meshtastic.serial_interface.SerialInterface(globals().get(f'port{i}'))
|
||||
elif interface_type == 'tcp':
|
||||
globals()[f'interface{i}'] = meshtastic.tcp_interface.TCPInterface(globals().get(f'hostname{i}'))
|
||||
elif interface_type == 'ble':
|
||||
globals()[f'interface{i}'] = meshtastic.ble_interface.BLEInterface(globals().get(f'mac{i}'))
|
||||
else:
|
||||
logger.critical(f"System: Interface Type: {interface_type} not supported. Validate your config against config.template Exiting")
|
||||
exit()
|
||||
except Exception as e:
|
||||
logger.critical(f"System: abort. Initializing Interface{i} {e}")
|
||||
exit()
|
||||
|
||||
# Get the node number of the devices, check if the devices are connected meshtastic devices
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}') and globals().get(f'interface{i}_enabled'):
|
||||
try:
|
||||
globals()[f'myNodeNum{i}'] = globals()[f'interface{i}'].getMyNodeInfo()['num']
|
||||
logger.debug(f"System: Initalized Radio Device{i} Node Number: {globals()[f'myNodeNum{i}']}")
|
||||
except Exception as e:
|
||||
logger.critical(f"System: critical error initializing interface{i} {e}")
|
||||
else:
|
||||
logger.critical(f"System: Interface Type: {interface1_type} not supported. Validate your config against config.template Exiting")
|
||||
exit()
|
||||
except Exception as e:
|
||||
logger.critical(f"System: script abort. Initializing Interface1 {e}")
|
||||
exit()
|
||||
|
||||
# Interface2 Configuration
|
||||
if interface2_enabled:
|
||||
logger.debug(f"System: Initializing Interface2")
|
||||
try:
|
||||
if interface2_type == 'serial':
|
||||
interface2 = meshtastic.serial_interface.SerialInterface(port2)
|
||||
elif interface2_type == 'tcp':
|
||||
interface2 = meshtastic.tcp_interface.TCPInterface(hostname2)
|
||||
elif interface2_type == 'ble':
|
||||
interface2 = meshtastic.ble_interface.BLEInterface(mac2)
|
||||
else:
|
||||
logger.critical(f"System: Interface Type: {interface2_type} not supported. Validate your config against config.template Exiting")
|
||||
exit()
|
||||
except Exception as e:
|
||||
logger.critical(f"System: script abort. Initializing Interface2 {e}")
|
||||
exit()
|
||||
|
||||
#Get the node number of the device, check if the device is connected
|
||||
try:
|
||||
myinfo = interface1.getMyNodeInfo()
|
||||
myNodeNum1 = myinfo['num']
|
||||
except Exception as e:
|
||||
logger.critical(f"System: script abort. {e}")
|
||||
exit()
|
||||
|
||||
if interface2_enabled:
|
||||
try:
|
||||
myinfo2 = interface2.getMyNodeInfo()
|
||||
myNodeNum2 = myinfo2['num']
|
||||
except Exception as e:
|
||||
logger.critical(f"System: script abort. {e}")
|
||||
exit()
|
||||
else:
|
||||
myNodeNum2 = 777
|
||||
|
||||
globals()[f'myNodeNum{i}'] = 777
|
||||
|
||||
#### FUN-ctions ####
|
||||
|
||||
@@ -272,7 +271,7 @@ def decimal_to_hex(decimal_number):
|
||||
return f"!{decimal_number:08x}"
|
||||
|
||||
def get_name_from_number(number, type='long', nodeInt=1):
|
||||
interface = interface1 if nodeInt == 1 else interface2
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
name = ""
|
||||
|
||||
for node in interface.nodes.values():
|
||||
@@ -289,7 +288,7 @@ def get_name_from_number(number, type='long', nodeInt=1):
|
||||
|
||||
|
||||
def get_num_from_short_name(short_name, nodeInt=1):
|
||||
interface = interface1 if nodeInt == 1 else interface2
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
# Get the node number from the short name, converting all to lowercase for comparison (good practice?)
|
||||
logger.debug(f"System: Getting Node Number from Short Name: {short_name} on Device: {nodeInt}")
|
||||
for node in interface.nodes.values():
|
||||
@@ -299,17 +298,18 @@ def get_num_from_short_name(short_name, nodeInt=1):
|
||||
elif str(short_name.lower()) == node['user']['shortName'].lower():
|
||||
return node['num']
|
||||
else:
|
||||
if interface2_enabled:
|
||||
interface = interface2 if nodeInt == 1 else interface1 # check the other interface
|
||||
for node in interface.nodes.values():
|
||||
if short_name == node['user']['shortName']:
|
||||
return node['num']
|
||||
elif str(short_name.lower()) == node['user']['shortName'].lower():
|
||||
return node['num']
|
||||
for int in range(1, 10):
|
||||
if globals().get(f'interface{int}_enabled') and int != nodeInt:
|
||||
other_interface = globals().get(f'interface{int}')
|
||||
for node in other_interface.nodes.values():
|
||||
if short_name == node['user']['shortName']:
|
||||
return node['num']
|
||||
elif str(short_name.lower()) == node['user']['shortName'].lower():
|
||||
return node['num']
|
||||
return 0
|
||||
|
||||
def get_node_list(nodeInt=1):
|
||||
interface = interface1 if nodeInt == 1 else interface2
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
# Get a list of nodes on the device
|
||||
node_list = ""
|
||||
node_list1 = []
|
||||
@@ -319,7 +319,7 @@ def get_node_list(nodeInt=1):
|
||||
if interface.nodes:
|
||||
for node in interface.nodes.values():
|
||||
# ignore own
|
||||
if node['num'] != myNodeNum2 and node['num'] != myNodeNum1:
|
||||
if all(node['num'] != globals().get(f'myNodeNum{i}') for i in range(1, 10)):
|
||||
node_name = get_name_from_number(node['num'], 'short', nodeInt)
|
||||
snr = node.get('snr', 0)
|
||||
|
||||
@@ -337,13 +337,14 @@ def get_node_list(nodeInt=1):
|
||||
#print (f"Node List: {node_list1[:5]}\n")
|
||||
node_list1.sort(key=lambda x: x[1] if x[1] is not None else 0, reverse=True)
|
||||
#print (f"Node List: {node_list1[:5]}\n")
|
||||
if interface2_enabled:
|
||||
if multiple_interface:
|
||||
logger.debug(f"System: FIX ME line 327 Multiple Interface Node List")
|
||||
node_list2.sort(key=lambda x: x[1] if x[1] is not None else 0, reverse=True)
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error sorting node list: {e}")
|
||||
logger.debug(f"Node List1: {node_list1[:5]}\n")
|
||||
if interface2_enabled:
|
||||
logger.debug(f"Node List2: {node_list2[:5]}\n")
|
||||
if multiple_interface:
|
||||
logger.debug(f"FIX ME MULTI INTERFACE Node List2: {node_list2[:5]}\n")
|
||||
node_list = ERROR_FETCHING_DATA
|
||||
|
||||
try:
|
||||
@@ -363,7 +364,7 @@ def get_node_list(nodeInt=1):
|
||||
return node_list
|
||||
|
||||
def get_node_location(number, nodeInt=1, channel=0):
|
||||
interface = interface1 if nodeInt == 1 else interface2
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
# Get the location of a node by its number from nodeDB on device
|
||||
latitude = latitudeValue
|
||||
longitude = longitudeValue
|
||||
@@ -396,7 +397,7 @@ def get_node_location(number, nodeInt=1, channel=0):
|
||||
|
||||
|
||||
def get_closest_nodes(nodeInt=1,returnCount=3):
|
||||
interface = interface1 if nodeInt == 1 else interface2
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
node_list = []
|
||||
|
||||
if interface.nodes:
|
||||
@@ -417,7 +418,7 @@ def get_closest_nodes(nodeInt=1,returnCount=3):
|
||||
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
|
||||
|
||||
if (distance < sentry_radius):
|
||||
if nodeID != myNodeNum1 and myNodeNum2 and str(nodeID) not in sentryIgnoreList:
|
||||
if (nodeID not in [globals().get(f'myNodeNum{i}') for i in range(1, 10)]) and str(nodeID) not in sentryIgnoreList:
|
||||
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
|
||||
|
||||
except Exception as e:
|
||||
@@ -438,8 +439,35 @@ def get_closest_nodes(nodeInt=1,returnCount=3):
|
||||
else:
|
||||
logger.warning(f"System: No nodes found in closest_nodes on interface {nodeInt}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
def handleFavoritNode(nodeInt=1, nodeID=0, aor=False):
|
||||
#aor is add or remove if True add, if False remove
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
myNodeNumber = globals().get(f'myNodeNum{nodeInt}')
|
||||
if aor:
|
||||
interface.getNode(myNodeNumber).addFavorite(nodeID)
|
||||
logger.info(f"System: Added {nodeID} to favorites")
|
||||
else:
|
||||
interface.getNode(myNodeNumber).removeFavorite(nodeID)
|
||||
logger.info(f"System: Removed {nodeID} from favorites")
|
||||
|
||||
def getFavoritNodes(nodeInt=1):
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
myNodeNumber = globals().get(f'myNodeNum{nodeInt}')
|
||||
favList = []
|
||||
for node in interface.getNode(myNodeNumber).favorites:
|
||||
favList.append(node)
|
||||
return favList
|
||||
|
||||
|
||||
def handleSentinelIgnore(nodeInt=1, nodeID=0, aor=False):
|
||||
#aor is add or remove if True add, if False remove
|
||||
if aor:
|
||||
sentryIgnoreList.append(str(nodeID))
|
||||
logger.info(f"System: Added {nodeID} to sentry ignore list")
|
||||
else:
|
||||
sentryIgnoreList.remove(str(nodeID))
|
||||
logger.info(f"System: Removed {nodeID} from sentry ignore list")
|
||||
|
||||
def messageChunker(message):
|
||||
message_list = []
|
||||
if len(message) > MESSAGE_CHUNK_SIZE:
|
||||
@@ -510,7 +538,7 @@ def messageChunker(message):
|
||||
|
||||
def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
|
||||
# Send a message to a channel or DM
|
||||
interface = interface1 if nodeInt == 1 else interface2
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
# Check if the message is empty
|
||||
if message == "" or message == None or len(message) == 0:
|
||||
return False
|
||||
@@ -671,6 +699,7 @@ def handleMultiPing(nodeID=0, deviceID=1):
|
||||
|
||||
def handleAlertBroadcast(deviceID=1):
|
||||
alertUk = NO_ALERTS
|
||||
alertDe = NO_ALERTS
|
||||
alertFema = NO_ALERTS
|
||||
wxAlert = NO_ALERTS
|
||||
# only allow API call every 20 minutes
|
||||
@@ -686,6 +715,8 @@ def handleAlertBroadcast(deviceID=1):
|
||||
alertWx = alertBrodcastNOAA()
|
||||
|
||||
if emergencyAlertBrodcastEnabled:
|
||||
if enableDEalerts:
|
||||
alertDe = get_nina_alerts()
|
||||
if enableGBalerts:
|
||||
alertUk = get_govUK_alerts()
|
||||
else:
|
||||
@@ -700,6 +731,7 @@ def handleAlertBroadcast(deviceID=1):
|
||||
|
||||
femaAlert = alertFema
|
||||
ukAlert = alertUk
|
||||
deAlert = alertDe
|
||||
|
||||
if emergencyAlertBrodcastEnabled:
|
||||
if NO_ALERTS not in femaAlert and ERROR_FETCHING_DATA not in femaAlert:
|
||||
@@ -716,6 +748,14 @@ def handleAlertBroadcast(deviceID=1):
|
||||
else:
|
||||
send_message(ukAlert, emergencyAlertBroadcastCh, 0, deviceID)
|
||||
return True
|
||||
|
||||
if NO_ALERTS not in deAlert:
|
||||
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)
|
||||
@@ -730,40 +770,30 @@ def handleAlertBroadcast(deviceID=1):
|
||||
return True
|
||||
|
||||
def onDisconnect(interface):
|
||||
global retry_int1, retry_int2
|
||||
global retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
|
||||
rxType = type(interface).__name__
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
logger.critical("System: Lost Connection to Device {rxInterface}")
|
||||
if port1 in rxInterface:
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and port2 in rxInterface:
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
logger.critical("System: Lost Connection to Device {rxHost}")
|
||||
if hostname1 in rxHost and interface1_type == 'tcp':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
retry_int2 = True
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
logger.critical("System: Lost Connection to Device BLE")
|
||||
if interface1_type == 'ble':
|
||||
retry_int1 = True
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
retry_int2 = True
|
||||
if rxType in ['SerialInterface', 'TCPInterface', 'BLEInterface']:
|
||||
identifier = interface.__dict__.get('devPath', interface.__dict__.get('hostname', 'BLE'))
|
||||
logger.critical(f"System: Lost Connection to Device {identifier}")
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
if (rxType == 'SerialInterface' and globals().get(f'port{i}') in identifier) or \
|
||||
(rxType == 'TCPInterface' and globals().get(f'hostname{i}') in identifier) or \
|
||||
(rxType == 'BLEInterface' and globals().get(f'interface{i}_type') == 'ble'):
|
||||
globals()[f'retry_int{i}'] = True
|
||||
break
|
||||
|
||||
def exit_handler():
|
||||
# Close the interface and save the BBS messages
|
||||
logger.debug(f"System: Closing Autoresponder")
|
||||
try:
|
||||
try:
|
||||
logger.debug(f"System: Closing Interface1")
|
||||
interface1.close()
|
||||
logger.debug(f"System: Interface1 Closed")
|
||||
if interface2_enabled:
|
||||
interface2.close()
|
||||
logger.debug(f"System: Interface2 Closed")
|
||||
if multiple_interface:
|
||||
for i in range(2, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
logger.debug(f"System: Closing Interface{i}")
|
||||
globals()[f'interface{i}'].close()
|
||||
except Exception as e:
|
||||
logger.error(f"System: closing: {e}")
|
||||
if bbs_enabled:
|
||||
@@ -778,14 +808,16 @@ def exit_handler():
|
||||
# Telemetry Functions
|
||||
telemetryData = {}
|
||||
def initialize_telemetryData():
|
||||
telemetryData[0] = {'interface1': 0, 'interface2': 0, 'lastAlert1': '', 'lastAlert2': ''}
|
||||
telemetryData[1] = {'numPacketsTx': 0, 'numPacketsRx': 0, 'numOnlineNodes': 0, 'numPacketsTxErr': 0, 'numPacketsRxErr': 0, 'numTotalNodes': 0}
|
||||
telemetryData[2] = {'numPacketsTx': 0, 'numPacketsRx': 0, 'numOnlineNodes': 0, 'numPacketsTxErr': 0, 'numPacketsRxErr': 0, 'numTotalNodes': 0}
|
||||
telemetryData[0] = {f'interface{i}': 0 for i in range(1, 10)}
|
||||
telemetryData[0].update({f'lastAlert{i}': '' for i in range(1, 10)})
|
||||
for i in range(1, 10):
|
||||
telemetryData[i] = {'numPacketsTx': 0, 'numPacketsRx': 0, 'numOnlineNodes': 0, 'numPacketsTxErr': 0, 'numPacketsRxErr': 0, 'numTotalNodes': 0}
|
||||
|
||||
# indented to be called from the main loop
|
||||
initialize_telemetryData()
|
||||
|
||||
def getNodeFirmware(nodeID=0, nodeInt=1):
|
||||
interface = interface1 if nodeInt == 1 else interface2
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
# get the firmware version of the node
|
||||
# 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
|
||||
@@ -799,18 +831,15 @@ def getNodeFirmware(nodeID=0, nodeInt=1):
|
||||
return -1
|
||||
|
||||
def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
|
||||
interface = interface1 if rxNode == 1 else interface2
|
||||
interface = globals()[f'interface{rxNode}']
|
||||
myNodeNum = globals().get(f'myNodeNum{rxNode}')
|
||||
global telemetryData
|
||||
|
||||
# throttle the telemetry requests to prevent spamming the device
|
||||
if rxNode == 1:
|
||||
if time.time() - telemetryData[0]['interface1'] < 600 and not userRequested:
|
||||
if 1 <= rxNode <= 9:
|
||||
if time.time() - telemetryData[0][f'interface{rxNode}'] < 600 and not userRequested:
|
||||
return -1
|
||||
telemetryData[0]['interface1'] = time.time()
|
||||
elif rxNode == 2:
|
||||
if time.time() - telemetryData[0]['interface2'] < 600 and not userRequested:
|
||||
return -1
|
||||
telemetryData[0]['interface2'] = time.time()
|
||||
telemetryData[0][f'interface{rxNode}'] = time.time()
|
||||
|
||||
# some telemetry data is not available in python-meshtastic?
|
||||
# bring in values from the last telemetry dump for the node
|
||||
@@ -822,13 +851,13 @@ def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
|
||||
totalOnlineNodes = telemetryData[rxNode]['numOnlineNodes']
|
||||
|
||||
# get the telemetry data for a node
|
||||
chutil = round(interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("channelUtilization", 0), 1)
|
||||
airUtilTx = round(interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("airUtilTx", 0), 1)
|
||||
uptimeSeconds = interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("uptimeSeconds", 0)
|
||||
batteryLevel = interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("batteryLevel", 0)
|
||||
voltage = interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("deviceMetrics", {}).get("voltage", 0)
|
||||
#numPacketsRx = interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("localStats", {}).get("numPacketsRx", 0)
|
||||
#numPacketsTx = interface.nodes.get(decimal_to_hex(myNodeNum1), {}).get("localStats", {}).get("numPacketsTx", 0)
|
||||
chutil = round(interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("channelUtilization", 0), 1)
|
||||
airUtilTx = round(interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("airUtilTx", 0), 1)
|
||||
uptimeSeconds = interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("uptimeSeconds", 0)
|
||||
batteryLevel = interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("batteryLevel", 0)
|
||||
voltage = interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("voltage", 0)
|
||||
#numPacketsRx = interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("localStats", {}).get("numPacketsRx", 0)
|
||||
#numPacketsTx = interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("localStats", {}).get("numPacketsTx", 0)
|
||||
numTotalNodes = len(interface.nodes)
|
||||
|
||||
dataResponse = f"Telemetry:{rxNode}"
|
||||
@@ -960,9 +989,6 @@ def get_sysinfo(nodeID=0, deviceID=1):
|
||||
# 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():
|
||||
@@ -987,15 +1013,23 @@ async def handleSignalWatcher():
|
||||
for ch in sigWatchBroadcastCh:
|
||||
if antiSpam and ch != publicChannel:
|
||||
send_message(msg, int(ch), 0, 1)
|
||||
if interface2_enabled:
|
||||
send_message(msg, int(ch), 0, 2)
|
||||
time.sleep(responseDelay)
|
||||
if multiple_interface:
|
||||
for i in range(2, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
send_message(msg, int(ch), 0, i)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
|
||||
else:
|
||||
if antiSpam and sigWatchBroadcastCh != publicChannel:
|
||||
send_message(msg, int(sigWatchBroadcastCh), 0, 1)
|
||||
if interface2_enabled:
|
||||
send_message(msg, int(sigWatchBroadcastCh), 0, 2)
|
||||
time.sleep(responseDelay)
|
||||
if multiple_interface:
|
||||
for i in range(2, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
send_message(msg, int(sigWatchBroadcastCh), 0, i)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
|
||||
|
||||
@@ -1018,172 +1052,147 @@ async def handleFileWatcher():
|
||||
for ch in file_monitor_broadcastCh:
|
||||
if antiSpam and ch != publicChannel:
|
||||
send_message(msg, int(ch), 0, 1)
|
||||
if interface2_enabled:
|
||||
send_message(msg, int(ch), 0, 2)
|
||||
time.sleep(responseDelay)
|
||||
if multiple_interface:
|
||||
for i in range(2, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
send_message(msg, int(ch), 0, i)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
logger.warning(f"System: antiSpam prevented Alert from FileWatcher")
|
||||
else:
|
||||
if antiSpam and file_monitor_broadcastCh != publicChannel:
|
||||
send_message(msg, int(file_monitor_broadcastCh), 0, 1)
|
||||
if interface2_enabled:
|
||||
send_message(msg, int(file_monitor_broadcastCh), 0, 2)
|
||||
time.sleep(responseDelay)
|
||||
if multiple_interface:
|
||||
for i in range(2, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
send_message(msg, int(file_monitor_broadcastCh), 0, i)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
logger.warning(f"System: antiSpam prevented Alert from FileWatcher")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
pass
|
||||
|
||||
async def retry_interface(nodeID=1):
|
||||
global interface1, interface2, retry_int1, retry_int2, max_retry_count1, max_retry_count2
|
||||
interface = interface1 if nodeID == 1 else interface2
|
||||
retry_int = retry_int1 if nodeID == 1 else retry_int2
|
||||
# retry connecting to the interface
|
||||
# add a check to see if the interface is already open or trying to open
|
||||
async def retry_interface(nodeID):
|
||||
global max_retry_count
|
||||
interface = globals()[f'interface{nodeID}']
|
||||
retry_int = globals()[f'retry_int{nodeID}']
|
||||
max_retry_count = globals()[f'max_retry_count{nodeID}']
|
||||
|
||||
if interface is not None:
|
||||
retry_int = True
|
||||
max_retry_count1 -= 1
|
||||
max_retry_count -= 1
|
||||
try:
|
||||
interface.close()
|
||||
except Exception as e:
|
||||
logger.error(f"System: closing interface{nodeID}: {e}")
|
||||
|
||||
|
||||
logger.debug(f"System: Retrying interface{nodeID} in 15 seconds")
|
||||
if max_retry_count1 == 0:
|
||||
logger.critical(f"System: Max retry count reached for interface1")
|
||||
if max_retry_count == 0:
|
||||
logger.critical(f"System: Max retry count reached for interface{nodeID}")
|
||||
exit_handler()
|
||||
if max_retry_count2 == 0:
|
||||
logger.critical(f"System: Max retry count reached for interface2")
|
||||
exit_handler()
|
||||
# wait 15 seconds before retrying
|
||||
|
||||
await asyncio.sleep(15)
|
||||
|
||||
# retry the interface
|
||||
try:
|
||||
if retry_int:
|
||||
interface = None
|
||||
if nodeID == 1:
|
||||
interface1 = None
|
||||
if nodeID == 2:
|
||||
interface2 = None
|
||||
globals()[f'interface{nodeID}'] = None
|
||||
logger.debug(f"System: Retrying Interface{nodeID}")
|
||||
interface_type = interface1_type if nodeID == 1 else interface2_type
|
||||
interface_type = globals()[f'interface{nodeID}_type']
|
||||
if interface_type == 'serial':
|
||||
interface1 = meshtastic.serial_interface.SerialInterface(port1)
|
||||
globals()[f'interface{nodeID}'] = meshtastic.serial_interface.SerialInterface(globals().get(f'port{nodeID}'))
|
||||
elif interface_type == 'tcp':
|
||||
interface1 = meshtastic.tcp_interface.TCPInterface(hostname1)
|
||||
globals()[f'interface{nodeID}'] = meshtastic.tcp_interface.TCPInterface(globals().get(f'hostname{nodeID}'))
|
||||
elif interface_type == 'ble':
|
||||
interface1 = meshtastic.ble_interface.BLEInterface(mac1)
|
||||
logger.debug(f"System: Interface1 Opened!")
|
||||
retry_int1 = False
|
||||
globals()[f'interface{nodeID}'] = meshtastic.ble_interface.BLEInterface(globals().get(f'mac{nodeID}'))
|
||||
logger.debug(f"System: Interface{nodeID} Opened!")
|
||||
globals()[f'retry_int{nodeID}'] = False
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error Opening interface{nodeID} on: {e}")
|
||||
|
||||
|
||||
handleSentinel_spotted = ""
|
||||
handleSentinel_spotted = []
|
||||
handleSentinel_loop = 0
|
||||
async def handleSentinel(deviceID=1):
|
||||
async def handleSentinel(deviceID):
|
||||
global handleSentinel_spotted, handleSentinel_loop
|
||||
# Locate Closest Nodes and report them to a secure channel
|
||||
# async function for possibly demanding back location data
|
||||
enemySpotted = ""
|
||||
detectedNearby = ""
|
||||
resolution = "unknown"
|
||||
closest_nodes = get_closest_nodes(deviceID)
|
||||
closest_node = closest_nodes[0]['id'] if closest_nodes != ERROR_FETCHING_DATA and closest_nodes else None
|
||||
closest_distance = closest_nodes[0]['distance'] if closest_nodes != ERROR_FETCHING_DATA and closest_nodes else None
|
||||
|
||||
# check if the handleSentinel_spotted list contains the closest node already
|
||||
if closest_node in [i['id'] for i in handleSentinel_spotted]:
|
||||
# check if the distance is closer than the last time, if not just return
|
||||
for i in range(len(handleSentinel_spotted)):
|
||||
if handleSentinel_spotted[i]['id'] == closest_node and closest_distance is not None and closest_distance < handleSentinel_spotted[i]['distance']:
|
||||
handleSentinel_spotted[i]['distance'] = closest_distance
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
if closest_nodes != ERROR_FETCHING_DATA and closest_nodes:
|
||||
if closest_nodes[0]['id'] is not None:
|
||||
enemySpotted = get_name_from_number(closest_nodes[0]['id'], 'long', 1)
|
||||
enemySpotted += ", " + get_name_from_number(closest_nodes[0]['id'], 'short', 1)
|
||||
enemySpotted += ", " + str(closest_nodes[0]['id'])
|
||||
enemySpotted += ", " + decimal_to_hex(closest_nodes[0]['id'])
|
||||
enemySpotted += f" at {closest_nodes[0]['distance']}m"
|
||||
|
||||
if handleSentinel_loop >= sentry_holdoff and handleSentinel_spotted != enemySpotted:
|
||||
# check the positionMetadata for nodeID and get metadata
|
||||
detectedNearby = get_name_from_number(closest_node, 'long', deviceID)
|
||||
detectedNearby += ", " + get_name_from_number(closest_nodes[0]['id'], 'short', deviceID)
|
||||
detectedNearby += ", " + str(closest_nodes[0]['id'])
|
||||
detectedNearby += ", " + decimal_to_hex(closest_nodes[0]['id'])
|
||||
detectedNearby += f" at {closest_distance}m"
|
||||
|
||||
if handleSentinel_loop >= sentry_holdoff and detectedNearby not in ["", None]:
|
||||
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)
|
||||
|
||||
logger.warning(f"System: {detectedNearby} is close to your location on Interface{deviceID} Accuracy is {resolution}bits")
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
send_message(f"Sentry{deviceID}: {detectedNearby}", secure_channel, 0, i)
|
||||
time.sleep(responseDelay + 1)
|
||||
if enableSMTP and email_sentry_alerts:
|
||||
for email in sysopEmails:
|
||||
send_email(email, f"Sentry{deviceID}: {enemySpotted}")
|
||||
send_email(email, f"Sentry{deviceID}: {detectedNearby}")
|
||||
handleSentinel_loop = 0
|
||||
handleSentinel_spotted = enemySpotted
|
||||
handleSentinel_spotted.append({'id': closest_node, 'distance': closest_distance})
|
||||
else:
|
||||
handleSentinel_loop += 1
|
||||
|
||||
async def watchdog():
|
||||
global retry_int1, retry_int2, telemetryData
|
||||
int1Data, int2Data = "", ""
|
||||
global telemetryData, retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
|
||||
while True:
|
||||
await asyncio.sleep(20)
|
||||
#print(f"MeshBot System: watchdog running\r", end="")
|
||||
|
||||
if interface1 is not None and not retry_int1:
|
||||
# getting firmware is a heartbeat to check if the interface is still connected
|
||||
try:
|
||||
firmware = getNodeFirmware(0, 1)
|
||||
except Exception as e:
|
||||
logger.error(f"System: communicating with interface1, trying to reconnect: {e}")
|
||||
retry_int1 = True
|
||||
|
||||
if not retry_int1:
|
||||
# Locate Closest Nodes and report them to a secure channel
|
||||
if sentry_enabled:
|
||||
await handleSentinel(1)
|
||||
|
||||
# multiPing handler
|
||||
handleMultiPing(0,1)
|
||||
|
||||
# Alert Broadcast
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled:
|
||||
# weather alerts
|
||||
handleAlertBroadcast(1)
|
||||
|
||||
# Telemetry data
|
||||
int1Data = displayNodeTelemetry(0, 1)
|
||||
if int1Data != -1 and telemetryData[0]['lastAlert1'] != int1Data:
|
||||
logger.debug(int1Data + f" Firmware:{firmware}")
|
||||
telemetryData[0]['lastAlert1'] = int1Data
|
||||
|
||||
if retry_int1:
|
||||
try:
|
||||
await retry_interface(1)
|
||||
except Exception as e:
|
||||
logger.error(f"System: retrying interface1: {e}")
|
||||
|
||||
if interface2_enabled:
|
||||
if interface2 is not None and not retry_int2:
|
||||
# getting firmware is a heartbeat to check if the interface is still connected
|
||||
# check all interfaces
|
||||
for i in range(1, 10):
|
||||
interface = globals().get(f'interface{i}')
|
||||
retry_int = globals().get(f'retry_int{i}')
|
||||
if interface is not None and not retry_int and globals().get(f'interface{i}_enabled'):
|
||||
try:
|
||||
firmware2 = getNodeFirmware(0, 1)
|
||||
firmware = getNodeFirmware(0, i)
|
||||
except Exception as e:
|
||||
logger.error(f"System: communicating with interface1, trying to reconnect: {e}")
|
||||
retry_int2 = True
|
||||
logger.error(f"System: communicating with interface{i}, trying to reconnect: {e}")
|
||||
globals()[f'retry_int{i}'] = True
|
||||
|
||||
if not retry_int2:
|
||||
# Locate Closest Nodes and report them to a secure channel
|
||||
if not globals()[f'retry_int{i}']:
|
||||
if sentry_enabled:
|
||||
await handleSentinel(2)
|
||||
await handleSentinel(i)
|
||||
|
||||
# multiPing handler
|
||||
handleMultiPing(0,1)
|
||||
handleMultiPing(0, i)
|
||||
|
||||
# Alert Broadcast
|
||||
if wxAlertBroadcastEnabled or emergencyAlertBrodcastEnabled:
|
||||
# weather alerts
|
||||
handleAlertBroadcast(2)
|
||||
handleAlertBroadcast(i)
|
||||
|
||||
# Telemetry data
|
||||
int2Data = displayNodeTelemetry(0, 2)
|
||||
if int2Data != -1 and telemetryData[0]['lastAlert2'] != int2Data:
|
||||
logger.debug(int2Data + f" Firmware:{firmware2}")
|
||||
telemetryData[0]['lastAlert2'] = int2Data
|
||||
|
||||
if retry_int2:
|
||||
intData = displayNodeTelemetry(0, i)
|
||||
if intData != -1 and telemetryData[0][f'lastAlert{i}'] != intData:
|
||||
logger.debug(intData + f" Firmware:{firmware}")
|
||||
telemetryData[0][f'lastAlert{i}'] = intData
|
||||
|
||||
if globals()[f'retry_int{i}'] and globals()[f'interface{i}_enabled']:
|
||||
try:
|
||||
await retry_interface(2)
|
||||
await retry_interface(i)
|
||||
except Exception as e:
|
||||
logger.error(f"System: retrying interface2: {e}")
|
||||
logger.error(f"System: retrying interface{i}: {e}")
|
||||
|
||||
|
||||
65
modules/web.py
Normal file
65
modules/web.py
Normal file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
# This is a simple web server that serves up the content of the webRoot directory
|
||||
# The reporting data is all that is currently being served up
|
||||
# TODO - add interaction to mesh?
|
||||
# to use this today run it seperately and open a browser to http://localhost:8420
|
||||
|
||||
import os
|
||||
import http.server
|
||||
|
||||
# Set the desired IP address
|
||||
server_ip = '127.0.0.1'
|
||||
|
||||
# Set the port for the server
|
||||
PORT = 8420
|
||||
|
||||
# Generate with: openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes
|
||||
SSL = False
|
||||
|
||||
# Set to True to enable logging sdtout
|
||||
webServerLogs = False
|
||||
|
||||
# Determine the directory where this script is located.
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
# Go up one level from the modules directory to the project root.
|
||||
project_root = os.path.abspath(os.path.join(script_dir, ".."))
|
||||
|
||||
# Build the absolute path to the webRoot folder; to where index.html is located.
|
||||
webRoot = os.path.join(project_root, "etc", "www")
|
||||
|
||||
if SSL:
|
||||
import ssl
|
||||
|
||||
# disable logging
|
||||
class QuietHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def log_message(self, format, *args):
|
||||
if webServerLogs:
|
||||
super().log_message(format, *args)
|
||||
|
||||
# Change the current working directory to webRoot
|
||||
os.chdir(webRoot)
|
||||
|
||||
# boot up simple HTTP server
|
||||
httpd = http.server.HTTPServer(('127.0.0.1', PORT), QuietHandler)
|
||||
|
||||
if SSL:
|
||||
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
try:
|
||||
ctx.load_cert_chain(certfile='./server.pem')
|
||||
except FileNotFoundError:
|
||||
print("SSL certificate file not found. Please generate it using the command provided in the comments.")
|
||||
exit(1)
|
||||
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
|
||||
|
||||
# Create the HTTP server instance with the desired IP address
|
||||
httpd = http.server.HTTPServer((server_ip, PORT), QuietHandler)
|
||||
|
||||
# Print out the URL using the IP address stored in server_ip
|
||||
print(f"Serving reports at http://{server_ip}:{PORT} Press ^C to quit.\n\n")
|
||||
|
||||
if not webServerLogs:
|
||||
print("Server Logs are disabled")
|
||||
# Serve forever, that is until the user interrupts the process
|
||||
httpd.serve_forever()
|
||||
exit(0)
|
||||
94
pong_bot.py
94
pong_bot.py
@@ -2,9 +2,15 @@
|
||||
# Meshtastic Autoresponder PONG Bot
|
||||
# K7MHI Kelly Keeton 2024
|
||||
|
||||
try:
|
||||
from pubsub import pub
|
||||
except ImportError:
|
||||
print(f"Important dependencies are not met, try install.sh\n\n Did you mean to './launch.sh pong' using a virtual environment.")
|
||||
exit(1)
|
||||
|
||||
import asyncio
|
||||
import time # for sleep, get some when you can :)
|
||||
from pubsub import pub # pip install pubsub, use launch.sh for venv
|
||||
import random
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
@@ -19,16 +25,15 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
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,
|
||||
"cmd": lambda: handle_cmd(message, message_from_id, deviceID),
|
||||
"cq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"cqcqcq": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"lheard": lambda: handle_lheard(interface1, interface2_enabled, myNodeNum1, myNodeNum2),
|
||||
"lheard": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
|
||||
"motd": lambda: handle_motd(message, MOTD),
|
||||
"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),
|
||||
"sitrep": lambda: lambda: handle_lheard(message, message_from_id, deviceID, isDM),
|
||||
"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),
|
||||
@@ -50,6 +55,13 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
|
||||
return bot_response
|
||||
|
||||
def handle_cmd(message, message_from_id, deviceID):
|
||||
# why CMD? its just a command list. a terminal would normally use "Help"
|
||||
# I didnt want to invoke the word "help" in Meshtastic due to its possible emergency use
|
||||
if " " in message and message.split(" ")[1] in trap_list:
|
||||
return "🤖 just use the commands directly in chat"
|
||||
return help_message
|
||||
|
||||
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
|
||||
global multiPing
|
||||
if "?" in message and isDM:
|
||||
@@ -169,6 +181,7 @@ def handle_lheard(message, nodeid, deviceID, isDM):
|
||||
return bot_response
|
||||
|
||||
def onReceive(packet, interface):
|
||||
global seenNodes
|
||||
# Priocess the incoming packet, handles the responses to the packet with auto_response()
|
||||
# Sends the packet to the correct handler for processing
|
||||
|
||||
@@ -178,6 +191,8 @@ def onReceive(packet, interface):
|
||||
# Valies assinged to the packet
|
||||
rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0
|
||||
pkiStatus = (False, 'ABC')
|
||||
replyIDset = False
|
||||
emojiSeen = False
|
||||
isDM = False
|
||||
|
||||
if DEBUGpacket:
|
||||
@@ -190,23 +205,42 @@ def onReceive(packet, interface):
|
||||
# set the value for the incomming interface
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
if port1 in rxInterface:
|
||||
rxNode = 1
|
||||
elif interface2_enabled and port2 in rxInterface:
|
||||
rxNode = 2
|
||||
if port1 in rxInterface: rxNode = 1
|
||||
elif multiple_interface and port2 in rxInterface: rxNode = 2
|
||||
elif multiple_interface and port3 in rxInterface: rxNode = 3
|
||||
elif multiple_interface and port4 in rxInterface: rxNode = 4
|
||||
elif multiple_interface and port5 in rxInterface: rxNode = 5
|
||||
elif multiple_interface and port6 in rxInterface: rxNode = 6
|
||||
elif multiple_interface and port7 in rxInterface: rxNode = 7
|
||||
elif multiple_interface and port8 in rxInterface: rxNode = 8
|
||||
elif multiple_interface and port9 in rxInterface: rxNode = 9
|
||||
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
if hostname1 in rxHost and interface1_type == 'tcp':
|
||||
rxNode = 1
|
||||
elif interface2_enabled and hostname2 in rxHost and interface2_type == 'tcp':
|
||||
rxNode = 2
|
||||
if hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble':
|
||||
rxNode = 1
|
||||
elif interface2_enabled and interface2_type == 'ble':
|
||||
rxNode = 2
|
||||
if interface1_type == 'ble': rxNode = 1
|
||||
elif multiple_interface and interface2_type == 'ble': rxNode = 2
|
||||
elif multiple_interface and interface3_type == 'ble': rxNode = 3
|
||||
elif multiple_interface and interface4_type == 'ble': rxNode = 4
|
||||
elif multiple_interface and interface5_type == 'ble': rxNode = 5
|
||||
elif multiple_interface and interface6_type == 'ble': rxNode = 6
|
||||
elif multiple_interface and interface7_type == 'ble': rxNode = 7
|
||||
elif multiple_interface and interface8_type == 'ble': rxNode = 8
|
||||
elif multiple_interface and interface9_type == 'ble': rxNode = 9
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
|
||||
# set the message_from_id
|
||||
message_from_id = packet['from']
|
||||
@@ -333,18 +367,17 @@ def onReceive(packet, interface):
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
|
||||
|
||||
# repeat the message on the other device
|
||||
if repeater_enabled and interface2_enabled:
|
||||
if repeater_enabled and multiple_interface:
|
||||
# wait a responseDelay to avoid message collision from lora-ack.
|
||||
time.sleep(responseDelay)
|
||||
rMsg = (f"{message_string} From:{get_name_from_number(message_from_id, 'short', rxNode)}")
|
||||
# if channel found in the repeater list repeat the message
|
||||
if str(channel_number) in repeater_channels:
|
||||
if rxNode == 1:
|
||||
logger.debug(f"Repeating message on Device2 Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, 2)
|
||||
elif rxNode == 2:
|
||||
logger.debug(f"Repeating message on Device1 Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, 1)
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled', False) and i != rxNode:
|
||||
logger.debug(f"Repeating message on Device{i} Channel:{channel_number}")
|
||||
send_message(rMsg, channel_number, 0, i)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode)
|
||||
@@ -357,11 +390,12 @@ async def start_rx():
|
||||
# Start the receive subscriber using pubsub via meshtastic library
|
||||
pub.subscribe(onReceive, 'meshtastic.receive')
|
||||
pub.subscribe(onDisconnect, 'meshtastic.connection.lost')
|
||||
logger.info(f"System: Autoresponder Started for Device1 {get_name_from_number(myNodeNum1, 'long', 1)},"
|
||||
f"{get_name_from_number(myNodeNum1, 'short', 1)}. NodeID: {myNodeNum1}, {decimal_to_hex(myNodeNum1)}")
|
||||
if interface2_enabled:
|
||||
logger.info(f"System: Autoresponder Started for Device2 {get_name_from_number(myNodeNum2, 'long', 2)},"
|
||||
f"{get_name_from_number(myNodeNum2, 'short', 2)}. NodeID: {myNodeNum2}, {decimal_to_hex(myNodeNum2)}")
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}_enabled', False):
|
||||
myNodeNum = globals().get(f'myNodeNum{i}', 0)
|
||||
logger.info(f"System: Autoresponder Started for Device{i} {get_name_from_number(myNodeNum, 'long', i)},"
|
||||
f"{get_name_from_number(myNodeNum, 'short', i)}. NodeID: {myNodeNum}, {decimal_to_hex(myNodeNum)}")
|
||||
|
||||
if log_messages_to_file:
|
||||
logger.debug("System: Logging Messages to disk")
|
||||
if syslog_to_file:
|
||||
@@ -376,7 +410,7 @@ async def start_rx():
|
||||
logger.debug(f"System: Store and Forward Enabled using limit: {storeFlimit}")
|
||||
if useDMForResponse:
|
||||
logger.debug(f"System: Respond by DM only")
|
||||
if repeater_enabled and interface2_enabled:
|
||||
if repeater_enabled and multiple_interface:
|
||||
logger.debug(f"System: Repeater Enabled for Channels: {repeater_channels}")
|
||||
if file_monitor_enabled:
|
||||
logger.debug(f"System: File Monitor Enabled for {file_monitor_file_path}, broadcasting to channels: {file_monitor_broadcastCh}")
|
||||
|
||||
25
script/docker/README.md
Normal file
25
script/docker/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# How do I use this thing?
|
||||
This is not a full turnkey setup for Docker yet but gets you most of the way there!
|
||||
|
||||
## Setup New Image
|
||||
`docker build -t meshing-around .`
|
||||
|
||||
there is also [script/docker/docker-install.bat](script/docker/docker-install.bat) which will automate this.
|
||||
|
||||
## Ollama Image with compose
|
||||
still a WIP
|
||||
`docker compose up -d`
|
||||
|
||||
## Edit the config.ini in the docker
|
||||
To edit the config.ini in the docker you can
|
||||
`docker run -it --entrypoint /bin/bash meshing-around -c "nano /app/config.ini"`
|
||||
|
||||
there is also [script/docker/docker-terminal.bat](script/docker/docker-terminal.bat) which will open nano to edit.
|
||||
ctl+o to write out and exit editor in shell
|
||||
|
||||
## other info
|
||||
1. Ensure your serial port is properly shared.
|
||||
2. Run the Docker container:
|
||||
```sh
|
||||
docker run --rm -it --device=/dev/ttyUSB0 meshing-around
|
||||
```
|
||||
52
script/docker/compose.yaml
Normal file
52
script/docker/compose.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
services:
|
||||
meshing-around:
|
||||
build:
|
||||
context: ../..
|
||||
depends_on:
|
||||
ollama:
|
||||
condition: service_healthy
|
||||
devices:
|
||||
- /dev/ttyAMA10 # Replace this with your actual device!
|
||||
configs:
|
||||
- source: me_config
|
||||
target: /app/config.ini
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway" # Used to access a local linux meshtasticd device via tcp
|
||||
ollama:
|
||||
image: ollama/ollama:0.5.1
|
||||
volumes:
|
||||
- ./ollama:/root/.ollama
|
||||
- ./ollama-entrypoint.sh:./entrypoint.sh
|
||||
container_name: ollama
|
||||
pull_policy: always
|
||||
tty: true
|
||||
restart: always
|
||||
entrypoint:
|
||||
- /usr/bin/bash
|
||||
- /script/docker/entrypoint.sh
|
||||
expose:
|
||||
- 11434
|
||||
healthcheck:
|
||||
test: "apt update && apt install curl -y && curl -f http://localhost:11434/api/tags | grep -q llama3.2:3b"
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 20
|
||||
node-exporter:
|
||||
image: quay.io/prometheus/node-exporter:latest
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/rootfs:ro
|
||||
command:
|
||||
- --path.procfs=/host/proc
|
||||
- --path.rootfs=/rootfs
|
||||
- --path.sysfs=/host/sys
|
||||
- --collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 9100
|
||||
network_mode: host
|
||||
pid: host
|
||||
configs:
|
||||
me_config:
|
||||
file: ./config.ini
|
||||
6
script/docker/docker-install.bat
Normal file
6
script/docker/docker-install.bat
Normal file
@@ -0,0 +1,6 @@
|
||||
REM batch file to install docker on windows
|
||||
REM docker compose up -d
|
||||
cd ../../
|
||||
docker build -t meshing-around .
|
||||
REM docker-compose up -d
|
||||
docker run -it --entrypoint /bin/bash meshing-around -c "nano /app/config.ini"
|
||||
2
script/docker/docker-terminal.bat
Normal file
2
script/docker/docker-terminal.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
REM launch meshing-around container with a terminal
|
||||
docker run -it --entrypoint /bin/bash meshing-around
|
||||
6
script/docker/entrypoint.sh
Normal file
6
script/docker/entrypoint.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
# instruction set the meshing-around docker container entrypoint
|
||||
# Substitute environment variables in the config file (what is the purpose of this?)
|
||||
# envsubst < /app/config.ini > /app/config.tmp && mv /app/config.tmp /app/config.ini
|
||||
# Run the bot
|
||||
exec python /app/mesh_bot.py
|
||||
16
script/docker/ollama-entrypoint.sh
Normal file
16
script/docker/ollama-entrypoint.sh
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start Ollama in the background.
|
||||
/bin/ollama serve &
|
||||
# Record Process ID.
|
||||
pid=$!
|
||||
|
||||
# Pause for Ollama to start.
|
||||
sleep 5
|
||||
|
||||
echo "🔴 Retrieve llama3.2:3b model..."
|
||||
ollama pull llama3.2:3b
|
||||
echo "🟢 Done!"
|
||||
|
||||
# Wait for Ollama process to finish.
|
||||
wait $pid
|
||||
@@ -4,4 +4,4 @@
|
||||
cd "$(dirname "$0")"
|
||||
program_path=$(pwd)
|
||||
|
||||
printf "Running meshing-around demo script for shell scripting\n"
|
||||
printf "Running meshing-around demo script for shell scripting from $program_path\n"
|
||||
53
script/send-environment-metrics.py
Normal file
53
script/send-environment-metrics.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# file name: send-environment-metrics.py
|
||||
# https://github.com/pdxlocations/Meshtastic-Python-Examples/blob/main/send-environment-metrics.py
|
||||
|
||||
from meshtastic.protobuf import portnums_pb2, telemetry_pb2
|
||||
from meshtastic import BROADCAST_ADDR
|
||||
import time
|
||||
|
||||
# For connection over serial
|
||||
# import meshtastic.serial_interface
|
||||
# interface = meshtastic.serial_interface.SerialInterface()
|
||||
|
||||
# For connection over TCP
|
||||
import meshtastic.tcp_interface
|
||||
interface = meshtastic.tcp_interface.TCPInterface(hostname='127.0.0.1', noProto=False)
|
||||
|
||||
# Create a telemetry data object
|
||||
telemetry_data = telemetry_pb2.Telemetry()
|
||||
telemetry_data.time = int(time.time())
|
||||
telemetry_data.local_stats.upTime = 0
|
||||
telemetry_data.environment_metrics.temperature = 0
|
||||
# telemetry_data.environment_metrics.voltage = 0
|
||||
# telemetry_data.environment_metrics.current = 0
|
||||
# telemetry_data.environment_metrics.relative_humidity = 0
|
||||
# telemetry_data.environment_metrics.barometric_pressure = 0
|
||||
# telemetry_data.environment_metrics.gas_resistance = 0
|
||||
# telemetry_data.environment_metrics.iaq = 0
|
||||
# telemetry_data.environment_metrics.distance = 0
|
||||
# telemetry_data.environment_metrics.lux = 0
|
||||
# telemetry_data.environment_metrics.white_lux = 0
|
||||
# telemetry_data.environment_metrics.ir_lux = 0
|
||||
# telemetry_data.environment_metrics.uv_lux = 0
|
||||
# telemetry_data.environment_metrics.wind_direction = 0
|
||||
# telemetry_data.environment_metrics.wind_speed = 0
|
||||
# telemetry_data.environment_metrics.wind_gust = 0
|
||||
# telemetry_data.environment_metrics.wind_lull = 0
|
||||
# telemetry_data.environment_metrics.weight = 0
|
||||
|
||||
# Read the uptime
|
||||
with open('/proc/uptime', 'r') as uptime:
|
||||
telemetry_data.local_stats.upTime = int(float(uptime.readline().split()[0]))
|
||||
|
||||
# Read the CPU temperature
|
||||
with open('/sys/class/thermal/thermal_zone0/temp', 'r') as cpu_temp:
|
||||
telemetry_data.environment_metrics.temperature = int(cpu_temp.read()) / 1000
|
||||
|
||||
interface.sendData(
|
||||
telemetry_data,
|
||||
destinationId=BROADCAST_ADDR,
|
||||
portNum=portnums_pb2.PortNum.TELEMETRY_APP,
|
||||
wantResponse=False,
|
||||
)
|
||||
|
||||
interface.close()
|
||||
@@ -23,8 +23,5 @@ else
|
||||
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"
|
||||
# print telemetry data rounded to 2 decimal places
|
||||
printf "Disk:%s RAM:%.2f%% CPU:%.2f%% CPU-T:%.2f°C (%.2f°F)\n" "$free_space" "$ram_usage" "$cpu_usage" "$temp" "$tempf"
|
||||
Reference in New Issue
Block a user