mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-03-28 17:32:36 +01:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8b2aefa28 | ||
|
|
849565cacb | ||
|
|
fdec3a6754 | ||
|
|
f3a97bc567 | ||
|
|
02625ad0f2 | ||
|
|
b4ba4b0daf | ||
|
|
fd7f8a94f5 | ||
|
|
d252250edd | ||
|
|
d6410e0461 | ||
|
|
050b4ab3ce | ||
|
|
8ac1a1eed7 | ||
|
|
370a417ce6 | ||
|
|
378b05df35 | ||
|
|
d002c5ede8 | ||
|
|
cd03cc56b4 | ||
|
|
d4fd484706 | ||
|
|
82d519279e | ||
|
|
09302e8c91 | ||
|
|
91fc4605ec | ||
|
|
abc6c07ee3 | ||
|
|
bbf8b04bd3 | ||
|
|
5fd293c990 | ||
|
|
a9a65a6c6d | ||
|
|
34e95c86d6 | ||
|
|
80891090c3 | ||
|
|
1b098fbf7b | ||
|
|
165d76cf8d | ||
|
|
045c9d433b | ||
|
|
fbe5e008de | ||
|
|
004adc7d9a | ||
|
|
9a2033452f | ||
|
|
5638204f82 | ||
|
|
e5c3b0cceb | ||
|
|
18ac53b230 | ||
|
|
4aa65dad6a | ||
|
|
f65a7b7934 | ||
|
|
886293087a | ||
|
|
e05e6f3451 | ||
|
|
4b5dd934e9 | ||
|
|
008ddfb5a2 | ||
|
|
e1330b9b9e | ||
|
|
7b43213094 | ||
|
|
b17c2b17ee | ||
|
|
b6505ee577 | ||
|
|
f7379b7ca5 | ||
|
|
ec9a1d88db | ||
|
|
a339570afe | ||
|
|
f5af9f419a | ||
|
|
ad5c1c90da | ||
|
|
eb1e0c82ea | ||
|
|
8d2277bc59 | ||
|
|
dc9908a72c | ||
|
|
21123d2993 | ||
|
|
7dc3134d0b | ||
|
|
b125178492 | ||
|
|
2ad9e84c33 | ||
|
|
63bd288caa | ||
|
|
5c7d199831 | ||
|
|
f56a39eeb6 | ||
|
|
ae5991ee39 | ||
|
|
1324f83f17 | ||
|
|
08ae8c31a0 | ||
|
|
957e803951 | ||
|
|
2de3441d67 | ||
|
|
2b420022f9 | ||
|
|
d01f143adf | ||
|
|
5f5aeeadac | ||
|
|
d57826613c | ||
|
|
af09dc0cf9 |
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
18
.github/workflows/greetings.yml
vendored
Normal file
18
.github/workflows/greetings.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Greetings
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v3
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue_message: "Dependabot's first issue"
|
||||
17
README.md
17
README.md
@@ -2,6 +2,8 @@
|
||||
|
||||
Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance your [Meshtastic](https://meshtastic.org/docs/introduction/) network experience with a variety of powerful tools and fun features, connectivity and utility through text-based message delivery. Whether you're looking to perform network tests, send messages, or even play games, [mesh_bot.py](mesh_bot.py) has you covered.
|
||||
|
||||
TLDR: [Getting Started](#getting-started)
|
||||
|
||||

|
||||
|
||||
## Key Features
|
||||
@@ -23,10 +25,9 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **Flexible Messaging**: send mail and messages, between networks.
|
||||
|
||||
### Advanced Messaging Capabilities
|
||||
- **Mail Messaging**: Leave messages for other devices, which are sent as DMs when the device is seen.
|
||||
- **Mail Messaging**: Leave messages for other devices, which are sent as DMs when the device is seen. Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
|
||||
- **Scheduler**: Schedule messages like weather updates or reminders for weekly VHF nets.
|
||||
- **Store and Forward**: Replay messages with the `messages` command, and log messages locally to disk.
|
||||
- **Send Mail**: Send mail to nodes using `bbspost @nodeNumber #message` or `bbspost @nodeShortName #message`.
|
||||
- **Store and Forward**: Like voicemail, see messages missed with the `messages` command. Can also log messages locally to disk.
|
||||
- **BBS Linking**: Combine multiple bots to expand BBS reach.
|
||||
- **E-Mail/SMS**: Send mesh-messages to E-Mail or SMS(Email) expanding visibility.
|
||||
- **New Node Hello**: Send a hello to any new node seen in text message.
|
||||
@@ -39,7 +40,7 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
- **GeoMeasuring**: HowFar from point to point using collected GPS packets on the bot to plot a course or space. Find Center of points for Fox&Hound direction finding.
|
||||
|
||||
### Proximity Alerts
|
||||
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
|
||||
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites, or put a geo-fence. You can also run a script or send a email. Another idea is to lower the cycle and use the bot as a 'king of the hill' or 🧭geocache game. You can also run a script to change a node config or turn on the lights🚥, have it drop an alert.txt to send a message like "Hello Start the 📊Survey"
|
||||
- **High Flying Alerts**: Get notified when nodes with high altitude are seen on mesh
|
||||
- **Voice/Command Triggers**: The following keywords can be used via voice (VOX) to trigger bot functions "Hey Chirpy!"
|
||||
- Say "Hey Chirpy.."
|
||||
@@ -197,7 +198,7 @@ Players can `q: join` to join the game, `q: leave` to leave the game, `q: score`
|
||||
To Answer a question, just type the answer prefixed with `q: <answer>`
|
||||
|
||||
#### Survey
|
||||
To use the Survey feature edit the json files in data/survey multiple surveys are possible such as `survey snow`
|
||||
To use the Survey feature edit the json files in data/survey multiple surveys are possible such as `survey snow` you can pull data back with `survey report` or `survey report snow`
|
||||
|
||||
## Other Install Options
|
||||
|
||||
@@ -469,7 +470,7 @@ broadcastCh = 2 # channel to send the message to can be 2,3 multiple channels co
|
||||
enable_read_news = False # news command will return the contents of a text file
|
||||
news_file_path = news.txt
|
||||
news_random_line = False # only return a single random line from the news file
|
||||
enable_runShellCmd = False # enable the use of exernal shell commands, this enables some data in `sysinfo`
|
||||
enable_runShellCmd = False # enable the use of exernal shell commands, this enables more data in `sysinfo` DM
|
||||
# if runShellCmd and you think it is safe to allow the x: command to run
|
||||
# direct shell command handler the x: command in DMs user must be in bbs_admin_list
|
||||
allowXcmd = True
|
||||
@@ -522,7 +523,9 @@ enabled = False # enable or disable the scheduler module
|
||||
interface = 1 # channel to send the message to
|
||||
channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun.
|
||||
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
|
||||
# custom for module/scheduler.py custom schedule examples
|
||||
interval = # interval to use when time is not set (e.g. every 2 days)
|
||||
time = # time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
```
|
||||
|
||||
14
SECURITY.md
Normal file
14
SECURITY.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| git pull| :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If its serious, its likley big. otherwise post issues, reachout on discord.
|
||||
@@ -127,19 +127,28 @@ alert_interface = 1
|
||||
[sentry]
|
||||
# detect anyone close to the bot
|
||||
SentryEnabled = True
|
||||
reqLocationEnabled = False
|
||||
emailSentryAlerts = False
|
||||
# radius in meters to detect someone close to the bot
|
||||
SentryRadius = 100
|
||||
# device interface and channel to send the alert message to
|
||||
SentryInterface = 1
|
||||
SentryChannel = 2
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 9
|
||||
emailSentryAlerts = False
|
||||
# Enable detection sensor alert, requires external GPIO sensor connected to node
|
||||
detectionSensorAlert = False
|
||||
|
||||
# list of ignored nodes numbers ex: 2813308004,4258675309
|
||||
sentryIgnoreList =
|
||||
# Enable detection sensor alert, requires external sensor connected to node
|
||||
detectionSensorAlert = False
|
||||
# list of watched nodes numbers ex: 2813308004,4258675309
|
||||
sentryWatchList =
|
||||
|
||||
# radius in meters to detect someone close to the bot
|
||||
SentryRadius = 100
|
||||
# holdoff time multiplied by seconds(20) of the watchdog
|
||||
SentryHoldoff = 9
|
||||
|
||||
# Enable running external shell command when sentry alert is triggered
|
||||
cmdShellSentryAlerts = False
|
||||
# External shell command to run when sentry alert is triggered
|
||||
sentryAlertNear = sentry_alert_near.sh
|
||||
sentryAlertAway = sentry_alert_away.sh
|
||||
|
||||
# HighFlying Node alert
|
||||
highFlyingAlert = True
|
||||
@@ -239,7 +248,7 @@ enableDEalerts = False
|
||||
myRegionalKeysDE = 110000000000,120510000000
|
||||
|
||||
# Satalite Pass Prediction
|
||||
# Register for free API https://www.n2yo.com/login/
|
||||
# Register for free API https://www.n2yo.com/login/ personal data page at bottom 'Are you developer?'
|
||||
n2yoAPIKey =
|
||||
# NORAD list https://www.n2yo.com/satellites/
|
||||
satList = 25544,7530
|
||||
@@ -277,7 +286,9 @@ channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
# enable overides the above and uses the motd as the message
|
||||
schedulerMotd = False
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun. or custom for module/scheduler.py
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun.
|
||||
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
|
||||
# custom for module/scheduler.py custom schedule examples
|
||||
value =
|
||||
# interval to use when time is not set (e.g. every 2 days)
|
||||
interval =
|
||||
|
||||
@@ -14,6 +14,8 @@ Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 mesh_bot.py
|
||||
ExecStop=pkill -f mesh_bot.py
|
||||
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
|
||||
@@ -14,6 +14,8 @@ Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 modules/web.py
|
||||
ExecStop=pkill -f mesh_bot_w3.py
|
||||
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
@@ -21,3 +23,6 @@ Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
Restart=on-failure
|
||||
Type=notify #try simple if any problems
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
||||
224
etc/meshview.ino
224
etc/meshview.ino
@@ -1,224 +0,0 @@
|
||||
// Example to receive and decode Meshtastic UDP packets
|
||||
// Make sure to install the meashtastic library and generate the .pb.h and .pb.c files from the Meshtastic .proto definitions
|
||||
// https://github.com/meshtastic/protobufs/tree/master/meshtastic
|
||||
|
||||
// Example to receive and decode Meshtastic UDP packets
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <WiFiUdp.h>
|
||||
// #include <AESLib.h> // or another AES library
|
||||
|
||||
#include "pb_decode.h"
|
||||
#include "meshtastic/mesh.pb.h" // MeshPacket, Position, etc.
|
||||
#include "meshtastic/portnums.pb.h" // Port numbers enum
|
||||
#include "meshtastic/telemetry.pb.h" // Telemetry message
|
||||
|
||||
const char* ssid = "YOUR_WIFI_SSID";
|
||||
const char* password = "YOUR_WIFI_PASSWORD";
|
||||
|
||||
const char* default_key = "1PG7OiApB1nwvP+rz05pAQ=="; // Your network key here
|
||||
uint8_t aes_key[16]; // Buffer for decoded key
|
||||
|
||||
const char* MCAST_GRP = "224.0.0.69";
|
||||
const uint16_t MCAST_PORT = 4403;
|
||||
|
||||
unsigned long udpPacketCount = 0;
|
||||
|
||||
WiFiUDP udp;
|
||||
IPAddress multicastIP;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(1000);
|
||||
|
||||
Serial.println("Scanning for WiFi networks...");
|
||||
int n = WiFi.scanNetworks();
|
||||
if (n == 0) {
|
||||
Serial.println("No networks found.");
|
||||
} else {
|
||||
Serial.print(n);
|
||||
Serial.println(" networks found:");
|
||||
for (int i = 0; i < n; ++i) {
|
||||
Serial.print(i + 1);
|
||||
Serial.print(": ");
|
||||
Serial.print(WiFi.SSID(i));
|
||||
Serial.print(" (RSSI ");
|
||||
Serial.print(WiFi.RSSI(i));
|
||||
Serial.print(")");
|
||||
Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? " [OPEN]" : " [SECURED]");
|
||||
delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
Serial.println("Connecting to WiFi...");
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(ssid, password);
|
||||
|
||||
unsigned long startAttemptTime = millis();
|
||||
const unsigned long wifiTimeout = 20000;
|
||||
|
||||
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < wifiTimeout) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
}
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println("\nWiFi connected.");
|
||||
Serial.print("IP address: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
|
||||
multicastIP.fromString(MCAST_GRP);
|
||||
if (udp.beginMulticast(multicastIP, MCAST_PORT)) {
|
||||
Serial.println("UDP multicast listener started.");
|
||||
} else {
|
||||
Serial.println("Failed to start UDP multicast listener.");
|
||||
}
|
||||
} else {
|
||||
Serial.print("\nFailed to connect to WiFi. SSID: ");
|
||||
Serial.println(ssid);
|
||||
Serial.println("Check SSID, range, and password.");
|
||||
}
|
||||
}
|
||||
|
||||
void printHex(const uint8_t* buf, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
Serial.printf("%02X ", buf[i]);
|
||||
}
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
void printAscii(const uint8_t* buf, size_t len) {
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char c = static_cast<char>(buf[i]);
|
||||
Serial.print(isprint(c) ? c : '.');
|
||||
}
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
void decodeKey() {
|
||||
// Convert base64 key to raw bytes
|
||||
// You may need to add a base64 decoding function/library
|
||||
// Example: decode_base64(default_key, aes_key, sizeof(aes_key));
|
||||
}
|
||||
|
||||
void decryptPayload(const uint8_t* encrypted, size_t len, uint8_t* decrypted) {
|
||||
// Use AESLib or similar to decrypt
|
||||
// Example: aes128_dec_single(decrypted, encrypted, aes_key);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
int packetSize = udp.parsePacket();
|
||||
if (!packetSize) {
|
||||
delay(50);
|
||||
return;
|
||||
}
|
||||
|
||||
udpPacketCount++;
|
||||
Serial.print("UDP packets seen: ");
|
||||
Serial.println(udpPacketCount);
|
||||
|
||||
uint8_t buffer[512];
|
||||
int len = udp.read(buffer, sizeof(buffer));
|
||||
if (len <= 0) {
|
||||
Serial.println("Failed to read UDP packet.");
|
||||
delay(50);
|
||||
return;
|
||||
}
|
||||
|
||||
// Always show raw payload
|
||||
Serial.print("Raw UDP payload (hex): ");
|
||||
printHex(buffer, len);
|
||||
Serial.print("Raw UDP payload (ASCII): ");
|
||||
printAscii(buffer, len);
|
||||
|
||||
// Decode outer MeshPacket
|
||||
meshtastic_MeshPacket pkt = meshtastic_MeshPacket_init_zero;
|
||||
pb_istream_t stream = pb_istream_from_buffer(buffer, len);
|
||||
|
||||
if (!pb_decode(&stream, meshtastic_MeshPacket_fields, &pkt)) {
|
||||
Serial.println("Failed to decode meshtastic_MeshPacket.");
|
||||
delay(50);
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic MeshPacket fields
|
||||
Serial.print("id: "); Serial.println(pkt.id);
|
||||
Serial.print("rx_time: "); Serial.println(pkt.rx_time);
|
||||
Serial.print("rx_snr: "); Serial.println(pkt.rx_snr, 2);
|
||||
Serial.print("rx_rssi: "); Serial.println(pkt.rx_rssi);
|
||||
Serial.print("hop_limit: "); Serial.println(pkt.hop_limit);
|
||||
Serial.print("priority: "); Serial.println(pkt.priority);
|
||||
Serial.print("from: "); Serial.println(pkt.from);
|
||||
Serial.print("to: "); Serial.println(pkt.to);
|
||||
Serial.print("channel: "); Serial.println(pkt.channel);
|
||||
|
||||
// Only proceed if we have a decoded Data variant
|
||||
if (pkt.which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
|
||||
Serial.println("Packet does not contain decoded Data (maybe encrypted or other variant).");
|
||||
delay(50);
|
||||
return;
|
||||
}
|
||||
|
||||
const meshtastic_Data& data = pkt.decoded;
|
||||
Serial.print("Portnum: "); Serial.println(data.portnum);
|
||||
Serial.print("Payload size: "); Serial.println(data.payload.size);
|
||||
|
||||
if (data.payload.size == 0) {
|
||||
Serial.println("No inner payload bytes.");
|
||||
delay(50);
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode by portnum
|
||||
switch (data.portnum) {
|
||||
|
||||
case meshtastic_PortNum_TEXT_MESSAGE_APP: {
|
||||
// Current schemas do not use a separate user.pb.h. Text payload is plain bytes.
|
||||
Serial.print("Decoded text message: ");
|
||||
printAscii(data.payload.bytes, data.payload.size);
|
||||
break;
|
||||
}
|
||||
|
||||
case meshtastic_PortNum_POSITION_APP: {
|
||||
meshtastic_Position pos = meshtastic_Position_init_zero;
|
||||
pb_istream_t ps = pb_istream_from_buffer(data.payload.bytes, data.payload.size);
|
||||
if (pb_decode(&ps, meshtastic_Position_fields, &pos)) {
|
||||
Serial.print("Position lat="); Serial.print(pos.latitude_i / 1e7, 7);
|
||||
Serial.print(" lon="); Serial.print(pos.longitude_i / 1e7, 7);
|
||||
Serial.print(" alt="); Serial.println(pos.altitude);
|
||||
} else {
|
||||
Serial.println("Failed to decode Position payload.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case meshtastic_PortNum_TELEMETRY_APP: {
|
||||
meshtastic_Telemetry tel = meshtastic_Telemetry_init_zero;
|
||||
pb_istream_t ts = pb_istream_from_buffer(data.payload.bytes, data.payload.size);
|
||||
if (pb_decode(&ts, meshtastic_Telemetry_fields, &tel)) {
|
||||
// Print a few common fields if present
|
||||
if (tel.which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
||||
const meshtastic_DeviceMetrics& m = tel.variant.device_metrics;
|
||||
Serial.print("Telemetry battery_level="); Serial.print(m.battery_level);
|
||||
Serial.print(" voltage="); Serial.print(m.voltage);
|
||||
Serial.print(" air_util_tx="); Serial.println(m.air_util_tx);
|
||||
} else {
|
||||
Serial.println("Telemetry decoded, different variant. Raw bytes:");
|
||||
printHex(data.payload.bytes, data.payload.size);
|
||||
}
|
||||
} else {
|
||||
Serial.println("Failed to decode Telemetry payload.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
Serial.print("Unhandled portnum "); Serial.print((int)data.portnum);
|
||||
Serial.println(", showing payload as hex:");
|
||||
printHex(data.payload.bytes, data.payload.size);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
delay(50);
|
||||
}
|
||||
@@ -14,6 +14,8 @@ Group=pi
|
||||
WorkingDirectory=/dir/
|
||||
ExecStart=python3 pong_bot.py
|
||||
ExecStop=pkill -f pong_bot.py
|
||||
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
# Disable Python's buffering of STDOUT and STDERR, so that output from the
|
||||
# service shows up immediately in systemd's logs
|
||||
|
||||
14
install.sh
14
install.sh
@@ -13,6 +13,12 @@ printf "Installer works best in raspian/debian/ubuntu or foxbuntu embedded syste
|
||||
printf "If there is a problem, try running the installer again.\n"
|
||||
printf "\nChecking for dependencies...\n"
|
||||
|
||||
# fuse
|
||||
fi [[ -f config.ini ]]; then
|
||||
printf "\nDetected existing installation, please backup and remove existing installation before proceeding\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if we are in /opt/meshing-around
|
||||
if [ $program_path != "/opt/meshing-around" ]; then
|
||||
printf "\nIt is suggested to project path to /opt/meshing-around\n"
|
||||
@@ -207,6 +213,12 @@ sudo chown -R $whoami:$whoami $program_path/logs
|
||||
sudo chown -R $whoami:$whoami $program_path/data
|
||||
echo "Permissions set for meshbot on logs and data directories"
|
||||
|
||||
# check and see if some sort of NTP is running
|
||||
if ! systemctl is-active --quiet ntp.service && \
|
||||
! systemctl is-active --quiet systemd-timesyncd.service && \
|
||||
! systemctl is-active --quiet chronyd.service; then
|
||||
printf "\nNo NTP service detected, it is recommended to have NTP running for proper bot operation.\n"
|
||||
|
||||
# set the correct user in the service file
|
||||
replace="s|User=pi|User=$whoami|g"
|
||||
sed -i $replace etc/pong_bot.service
|
||||
@@ -299,6 +311,7 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "Reporting chron job added to run report_generator5.py\n" >> install_notes.txt
|
||||
printf "chronjob: %s\n" "$chronjob" >> install_notes.txt
|
||||
printf "*** Stay Up to date using 'bash update.sh' ***\n" >> install_notes.txt
|
||||
|
||||
if [[ $(echo "${venv}" | grep -i "^y") ]]; then
|
||||
printf "\nFor running on venv, virtual launch bot with './launch.sh mesh' in path $program_path\n" >> install_notes.txt
|
||||
@@ -344,6 +357,7 @@ else
|
||||
printf "sudo journalctl -u %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl stop %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl disable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "*** Stay Up to date using 'bash update.sh' ***\n" >> install_notes.txt
|
||||
fi
|
||||
|
||||
printf "\nInstallation complete!\n"
|
||||
|
||||
129
mesh_bot.py
129
mesh_bot.py
@@ -11,6 +11,7 @@ except ImportError:
|
||||
import asyncio
|
||||
import time # for sleep, get some when you can :)
|
||||
import random
|
||||
from datetime import datetime
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
|
||||
@@ -90,7 +91,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"sun": lambda: handle_sun(message_from_id, deviceID, channel_number),
|
||||
"survey": lambda: surveyHandler(message, message_from_id, deviceID),
|
||||
"s:": lambda: surveyHandler(message, message_from_id, deviceID),
|
||||
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID),
|
||||
"sysinfo": lambda: sysinfo(message, message_from_id, deviceID, isDM),
|
||||
"test": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"testing": lambda: handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number),
|
||||
"tictactoe": lambda: handleTicTacToe(message, message_from_id, deviceID),
|
||||
@@ -250,10 +251,13 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
else:
|
||||
msg = "🔊 Can you hear me now?"
|
||||
|
||||
if hop == "Direct":
|
||||
msg = msg + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
msg = msg + hop
|
||||
# append SNR/RSSI or hop info
|
||||
if hop.startswith("Direct?") and (snr != 0 or rssi != 0):
|
||||
msg += f"? SNR:{snr} RSSI:{rssi}"
|
||||
elif hop.startswith("Direct"):
|
||||
msg += f"SNR:{snr} RSSI:{rssi}"
|
||||
elif hop:
|
||||
msg += f"{hop}"
|
||||
|
||||
if "@" in message:
|
||||
msg = msg + " @" + message.split("@")[1]
|
||||
@@ -336,7 +340,7 @@ def handle_emergency(message_from_id, deviceID, message):
|
||||
|
||||
def handle_motd(message, message_from_id, isDM):
|
||||
global MOTD
|
||||
msg = ''
|
||||
msg = MOTD
|
||||
isAdmin = isNodeAdmin(message_from_id)
|
||||
if "?" in message:
|
||||
msg = "Message of the day, set with 'motd $ HelloWorld!'"
|
||||
@@ -1036,7 +1040,6 @@ def quizHandler(message, nodeID, deviceID):
|
||||
return "🧠Please provide an answer or command, or send q: ?"
|
||||
|
||||
def surveyHandler(message, nodeID, deviceID):
|
||||
from modules.settings import surveyTracker
|
||||
user_id = nodeID
|
||||
location = get_node_location(nodeID, deviceID)
|
||||
msg = ''
|
||||
@@ -1055,10 +1058,13 @@ def surveyHandler(message, nodeID, deviceID):
|
||||
return survey_module.end_survey(user_id=nodeID)
|
||||
|
||||
# Handle report command
|
||||
if surveySays == "report":
|
||||
#return survey_module.quiz_report()
|
||||
# reminder to fix int and open question reporting
|
||||
return "Report not implemented yet"
|
||||
if 'report' in surveySays:
|
||||
if str(nodeID) not in bbs_admin_list:
|
||||
return "You do not have permission to view survey reports."
|
||||
# remove the words 'survey' and 'report' from the message
|
||||
report = msg_lower.replace("survey", "").replace("report", "").strip()
|
||||
results = survey_module.get_survey_results(survey_name=report if report else None)
|
||||
return survey_module.format_survey_results(results)
|
||||
|
||||
# Update last played or add new tracker entry
|
||||
found = False
|
||||
@@ -1256,7 +1262,7 @@ def handle_sun(message_from_id, deviceID, channel_number, vox=False):
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_sun(str(location[0]), str(location[1]))
|
||||
|
||||
def sysinfo(message, message_from_id, deviceID):
|
||||
def sysinfo(message, message_from_id, deviceID, isDM):
|
||||
if "?" in message:
|
||||
return "sysinfo command returns system information."
|
||||
else:
|
||||
@@ -1268,6 +1274,11 @@ def sysinfo(message, message_from_id, deviceID):
|
||||
if shellData == "" or shellData == None:
|
||||
# no data returned from the script
|
||||
shellData = "shell script data missing"
|
||||
# if not an admin remove any line in the shellData that had 'IP:' in it
|
||||
if (str(message_from_id) not in bbs_admin_list) or (not isDM):
|
||||
shell_lines = shellData.splitlines()
|
||||
filtered_lines = [line for line in shell_lines if 'IP:' not in line]
|
||||
shellData = "\n".join(filtered_lines)
|
||||
return get_sysinfo(message_from_id, deviceID) + "\n" + shellData.rstrip()
|
||||
else:
|
||||
return get_sysinfo(message_from_id, deviceID)
|
||||
@@ -1444,18 +1455,16 @@ def onReceive(packet, interface):
|
||||
# extract interface details from inbound packet
|
||||
rxType = type(interface).__name__
|
||||
|
||||
# Valies assinged to the packet
|
||||
rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0
|
||||
# Values assinged to the packet
|
||||
rxNode = message_from_id = snr = rssi = hop = hop_away = channel_number = hop_start = hop_count = hop_limit = 0
|
||||
pkiStatus = (False, 'ABC')
|
||||
rxNodeHostName = None
|
||||
replyIDset = False
|
||||
emojiSeen = False
|
||||
simulator_flag = False
|
||||
isDM = False
|
||||
channel_number = 0
|
||||
hop_away = 0
|
||||
hop_start = 0
|
||||
hop_count = 0
|
||||
channel_name = "unknown"
|
||||
session_passkey = None
|
||||
playingGame = False
|
||||
|
||||
if DEBUGpacket:
|
||||
@@ -1465,45 +1474,45 @@ def onReceive(packet, interface):
|
||||
# Debug print the packet for debugging
|
||||
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
|
||||
|
||||
# set the value for the incomming interface
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
if port1 in rxInterface: rxNode = 1
|
||||
elif multiple_interface and port2 in rxInterface: rxNode = 2
|
||||
elif multiple_interface and port3 in rxInterface: rxNode = 3
|
||||
elif multiple_interface and port4 in rxInterface: rxNode = 4
|
||||
elif multiple_interface and port5 in rxInterface: rxNode = 5
|
||||
elif multiple_interface and port6 in rxInterface: rxNode = 6
|
||||
elif multiple_interface and port7 in rxInterface: rxNode = 7
|
||||
elif multiple_interface and port8 in rxInterface: rxNode = 8
|
||||
elif multiple_interface and port9 in rxInterface: rxNode = 9
|
||||
|
||||
# determine the rxNode based on the interface type
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
if rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble': rxNode = 1
|
||||
elif multiple_interface and interface2_type == 'ble': rxNode = 2
|
||||
elif multiple_interface and interface3_type == 'ble': rxNode = 3
|
||||
elif multiple_interface and interface4_type == 'ble': rxNode = 4
|
||||
elif multiple_interface and interface5_type == 'ble': rxNode = 5
|
||||
elif multiple_interface and interface6_type == 'ble': rxNode = 6
|
||||
elif multiple_interface and interface7_type == 'ble': rxNode = 7
|
||||
elif multiple_interface and interface8_type == 'ble': rxNode = 8
|
||||
elif multiple_interface and interface9_type == 'ble': rxNode = 9
|
||||
rxNodeHostName = interface.__dict__.get('ip', None)
|
||||
rxNode = next(
|
||||
(i for i in range(1, 10)
|
||||
if multiple_interface and rxHost and
|
||||
globals().get(f'hostname{i}', '').split(':', 1)[0] in rxHost and
|
||||
globals().get(f'interface{i}_type', '') == 'tcp'),None)
|
||||
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
rxNode = next(
|
||||
(i for i in range(1, 10)
|
||||
if globals().get(f'port{i}', '') in rxInterface),None)
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if rxType == 'BLEInterface':
|
||||
rxNode = next(
|
||||
(i for i in range(1, 10)
|
||||
if globals().get(f'interface{i}_type', '') == 'ble'),0)
|
||||
|
||||
if rxNode is None:
|
||||
# default to interface 1 ## FIXME needs better like a default interface setting or hash lookup
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] in ['ADMIN_APP', 'SIMULATOR_APP']:
|
||||
session_passkey = packet.get('decoded', {}).get('admin', {}).get('sessionPasskey', None)
|
||||
rxNode = 1
|
||||
|
||||
# check if the packet has a channel flag use it ## FIXME needs to be channel hash lookup
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel')
|
||||
channel_name = "unknown"
|
||||
# get channel name from channel number from connected devices
|
||||
for device in channel_list:
|
||||
if device["interface_id"] == rxNode:
|
||||
device_channels = device['channels']
|
||||
for chan_name, info in device_channels.items():
|
||||
if info['number'] == channel_number:
|
||||
channel_name = chan_name
|
||||
break
|
||||
|
||||
# get channel hashes for the interface
|
||||
device = next((d for d in channel_list if d["interface_id"] == rxNode), None)
|
||||
if device:
|
||||
@@ -1592,8 +1601,8 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
hop_count = hop_away
|
||||
|
||||
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
|
||||
hop = "Last Hop"
|
||||
if hop == "" and hop_count > 0:
|
||||
hop = f"{hop_count} Hop" if hop_count == 1 else f"{hop_count} Hops"
|
||||
|
||||
if hop_start == hop_limit and "lora" in str(transport_mechanism).lower():
|
||||
hop = "Direct"
|
||||
@@ -1601,11 +1610,15 @@ def onReceive(packet, interface):
|
||||
if ((hop_start == 0 and hop_limit >= 0) or via_mqtt or ("mqtt" in str(transport_mechanism).lower())):
|
||||
hop = "MQTT"
|
||||
|
||||
## FIXME should this be here?
|
||||
if hop == "" and hop_count ==0 and (snr != 0 or rssi != 0):
|
||||
hop = "Direct?"
|
||||
|
||||
if "unknown" in str(transport_mechanism).lower() and (snr == 0 and rssi == 0):
|
||||
hop = "IP-Network"
|
||||
|
||||
if enableHopLogs:
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism}")
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism} Hostname:{rxNodeHostName}")
|
||||
|
||||
# check with stringSafeChecker if the message is safe
|
||||
if stringSafeCheck(message_string) is False:
|
||||
@@ -1629,7 +1642,7 @@ def onReceive(packet, interface):
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# DM is useful for games or LLM
|
||||
if games_enabled and (hop == "Direct" or hop_count < game_hop_limit):
|
||||
if games_enabled and ("Direct" in hop or hop_count < game_hop_limit):
|
||||
playingGame = checkPlayingGame(message_from_id, message_string, rxNode, channel_number)
|
||||
elif hop_count >= game_hop_limit:
|
||||
if games_enabled:
|
||||
@@ -1824,6 +1837,10 @@ async def start_rx():
|
||||
|
||||
if sentry_enabled:
|
||||
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel} requestLOC:{reqLocationEnabled}")
|
||||
if sentryIgnoreList:
|
||||
logger.debug(f"System: Sentry BlockList Enabled for nodes: {sentryIgnoreList}")
|
||||
if sentryWatchList:
|
||||
logger.debug(f"System: Sentry WatchList Enabled for nodes: {sentryWatchList}")
|
||||
|
||||
if highfly_enabled:
|
||||
logger.debug(f"System: HighFly Enabled using {highfly_altitude}m limit reporting to channel:{highfly_channel}")
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import pickle # pip install pickle
|
||||
from modules.log import *
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
useSynchCompression = False
|
||||
|
||||
@@ -97,7 +98,7 @@ def bbs_delete_message(messageID = 0, fromNode = 0):
|
||||
|
||||
def bbs_post_message(subject, message, fromNode, threadID=0, replytoID=0):
|
||||
# post a message to the bbsdb
|
||||
now = today.strftime('%Y-%m-%d %H:%M:%S')
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
thread = threadID
|
||||
replyto = replytoID
|
||||
# post a message to the bbsdb and assign a messageID
|
||||
|
||||
@@ -6,6 +6,7 @@ import asyncio
|
||||
import random
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
trap_list_filemon = ("readnews",)
|
||||
|
||||
@@ -69,28 +70,33 @@ async def watch_file():
|
||||
return content
|
||||
await asyncio.sleep(1) # Check every
|
||||
|
||||
def call_external_script(message, script="script/runShell.sh"):
|
||||
# Call an external script with the message as an argument this is a example only
|
||||
def call_external_script(message, script="runShell.sh"):
|
||||
# If no path is given, assume script/ directory
|
||||
if "/" not in script and "\\" not in script:
|
||||
script = os.path.join("script", script)
|
||||
try:
|
||||
current_working_directory = os.getcwd()
|
||||
script_path = os.path.join(current_working_directory, script)
|
||||
|
||||
if not os.path.exists(script_path):
|
||||
# try the raw script name
|
||||
# Try the raw script name
|
||||
script_path = script
|
||||
if not os.path.exists(script_path):
|
||||
logger.warning(f"FileMon: Script not found: {script_path}")
|
||||
return "sorry I can't do that"
|
||||
|
||||
# Use subprocess.run for better resource management
|
||||
result = subprocess.run(
|
||||
["bash", script_path, message],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error(f"FileMon: Script error: {result.stderr.strip()}")
|
||||
return None
|
||||
|
||||
output = result.stdout.strip()
|
||||
return output
|
||||
return output if output else None
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error calling external script: {e}")
|
||||
return None
|
||||
|
||||
@@ -46,15 +46,27 @@ lameJokes = [
|
||||
"Chuck Norris can kill two stones with one bird.",
|
||||
"Chuck Norris can speak braille.",
|
||||
"Chuck Norris can build a snowman out of rain.",
|
||||
"Chuck Norris can hear sign language.",
|
||||
"Death once had a near-Chuck Norris experience.",
|
||||
"Chuck Norris can unscramble an egg.",
|
||||
"Chuck Norris can win a game of Connect Four in only three moves.",
|
||||
"Chuck Norris can make a snowman out of rain.",
|
||||
"Chuck Norris can strangle you with a cordless phone.",
|
||||
"Chuck Norris can do a wheelie on a unicycle.",
|
||||
"Chuck Norris can kill two stones with one bird."]
|
||||
"This is a test. A test of the Joke Brodcast System. If this had been an actual joke, you would have been amused.",
|
||||
"Chuck Norris doesn't join mesh networks. Mesh networks join Chuck's topology.",
|
||||
"Every time Chuck Norris sends a packet, it arrives before he hits 'send'",
|
||||
"Chuck Norris doesn't need LoRa. His roundhouse kick has a 15km range with zero latency.",
|
||||
"When Chuck Norris uses a node, the bandwidth doubles out of fear.",
|
||||
"Chuck Norris once pinged a device. It replied with an apology and a firmware update.",
|
||||
"Chuck Norris doesn't use AES encryption. His packets are so secure, they punch hackers in the bits.",
|
||||
"The Meshtastic protocol has a hidden mode: “Chuck Norris mode.” It only activates when he blinks.",
|
||||
"Chuck Norris doesn't need a GPS fix. Satellites triangulate themselves around him.",
|
||||
"Chuck Norris's mesh node doesn't sleep. It meditates while transmitting at full power.",
|
||||
"Chuck Norris doesn't broadcast. He declares.",
|
||||
"Chuck Norris once bridged two mesh networks using a shoelace.",
|
||||
"Chuck Norris's packets don't hop. They teleport out of respect.",
|
||||
"Chuck Norris doesn't need a repeater. Client_Mute is set to 'Always'.",
|
||||
"Chuck Norris's mesh messages are entangled. When he sends one, it's already received.",
|
||||
"Chuck Norris doesn't mesh with others. Others mesh with Chuck.",
|
||||
"Chuck Norris's node doesn't need a case. The PCB is armored with his beard hair.",
|
||||
"Chuck Norris once typed “Hello World” and the world replied 'Hello Chuck.'",
|
||||
]
|
||||
|
||||
# pylint: disable=C0103, W0612
|
||||
imtellingyourightnowiAmTellingYouRightNowThatMotherfErBackThereIsNotReal = ["🐦", "🦅", "🦆", "🦉", "🦜", "🐤", "🐥", "🐣", "🐔", "🐧", "🦚", "🦢", "🦩", "🦤", "🦃", "🐓"]
|
||||
|
||||
def tableOfContents():
|
||||
@@ -178,4 +190,4 @@ def tell_joke(nodeID=0, vox=False):
|
||||
return renderedLaugh
|
||||
except Exception as e:
|
||||
return random.choice(lameJokes)
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from modules.log import *
|
||||
# https://github.com/ollama/ollama/blob/main/docs/faq.md#how-do-i-configure-ollama-server
|
||||
import requests
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
if not rawLLMQuery:
|
||||
# this may be removed in the future
|
||||
|
||||
@@ -7,6 +7,7 @@ import maidenhead as mh # pip install maidenhead
|
||||
import requests # pip install requests
|
||||
import bs4 as bs # pip install beautifulsoup4
|
||||
import xml.dom.minidom
|
||||
from datetime import datetime
|
||||
from modules.log import *
|
||||
import math
|
||||
|
||||
@@ -174,8 +175,8 @@ def get_NOAAtide(lat=0, lon=0):
|
||||
station_id = ""
|
||||
location = lat,lon
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.error("Location:No GPS data, try sending location for tide")
|
||||
return NO_DATA_NOGPS
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
station_lookup_url = "https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/tidepredstations.json?lat=" + str(lat) + "&lon=" + str(lon) + "&radius=50"
|
||||
try:
|
||||
station_data = requests.get(station_lookup_url, timeout=urlTimeoutSeconds)
|
||||
@@ -239,7 +240,8 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
weather = ""
|
||||
location = lat,lon
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
return NO_DATA_NOGPS
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
|
||||
# get weather data from NOAA units for metric unit = 1 is metric
|
||||
if use_metric:
|
||||
@@ -322,6 +324,7 @@ def abbreviate_noaa(data=""):
|
||||
"between four and five inches possible": "4-5in",
|
||||
"between five and six inches possible": "5-6in",
|
||||
"between six and eight inches possible": "6-8in",
|
||||
"gusts as high as": "gusts to",
|
||||
}
|
||||
# Single words (no spaces)
|
||||
word_replacements = {
|
||||
@@ -367,6 +370,7 @@ def abbreviate_noaa(data=""):
|
||||
"temperature": "temp:",
|
||||
"amounts": "amts:",
|
||||
"afternoon": "Aftn",
|
||||
"around": "~",
|
||||
"evening": "Eve",
|
||||
}
|
||||
|
||||
@@ -388,12 +392,11 @@ def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
|
||||
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
|
||||
alerts = ""
|
||||
location = lat,lon
|
||||
if useDefaultLatLon:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
if float(lat) == 0 and float(lon) == 0 and not useDefaultLatLon:
|
||||
return NO_DATA_NOGPS
|
||||
else:
|
||||
if useDefaultLatLon:
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
|
||||
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
|
||||
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
|
||||
@@ -466,8 +469,8 @@ def getActiveWeatherAlertsDetailNOAA(lat=0, lon=0):
|
||||
alerts = ""
|
||||
location = lat,lon
|
||||
if float(lat) == 0 and float(lon) == 0:
|
||||
logger.warning("Location:No GPS data, try sending location for weather alerts")
|
||||
return NO_DATA_NOGPS
|
||||
lat = latitudeValue
|
||||
lon = longitudeValue
|
||||
|
||||
alert_url = "https://api.weather.gov/alerts/active.atom?point=" + str(lat) + "," + str(lon)
|
||||
#alert_url = "https://api.weather.gov/alerts/active.atom?area=WA"
|
||||
@@ -527,10 +530,10 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
try:
|
||||
alert_data = requests.get(alert_url, timeout=urlTimeoutSeconds)
|
||||
if not alert_data.ok:
|
||||
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
|
||||
logger.warning(f"System: iPAWS fetching IPAWS alerts from FEMA (HTTP {alert_data.status_code})")
|
||||
return ERROR_FETCHING_DATA
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("System: iPAWS fetching IPAWS alerts from FEMA")
|
||||
except Exception as e:
|
||||
logger.warning(f"System: iPAWS fetching IPAWS alerts from FEMA failed: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# main feed bulletins
|
||||
@@ -612,14 +615,13 @@ def getIpawsAlert(lat=0, lon=0, shortAlerts = False):
|
||||
|
||||
# check if the alert is for the SAME location, if wanted keep alert
|
||||
if (sameVal in mySAMEList) or (geocode_value in mySAMEList) or mySAMEList == ['']:
|
||||
# ignore the FEMA test alerts
|
||||
ignore_alert = False
|
||||
if ignoreFEMAenable:
|
||||
ignore_alert = False
|
||||
for word in ignoreFEMAwords:
|
||||
if word.lower() in headline.lower():
|
||||
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing {word} at {areaDesc}")
|
||||
ignore_alert = True
|
||||
break
|
||||
ignore_alert = any(
|
||||
word.lower() in headline.lower()
|
||||
for word in ignoreFEMAwords)
|
||||
if ignore_alert:
|
||||
logger.debug(f"System: Filtering FEMA Alert by WORD: {headline} containing one of {ignoreFEMAwords} at {areaDesc}")
|
||||
if ignore_alert:
|
||||
continue
|
||||
|
||||
@@ -744,7 +746,7 @@ def get_volcano_usgs(lat=0, lon=0):
|
||||
return alerts
|
||||
|
||||
def get_nws_marine(zone, days=3):
|
||||
# forcast from NWS coastal products
|
||||
# forecast from NWS coastal products
|
||||
try:
|
||||
marine_pz_data = requests.get(zone, timeout=urlTimeoutSeconds)
|
||||
if not marine_pz_data.ok:
|
||||
@@ -753,18 +755,21 @@ def get_nws_marine(zone, days=3):
|
||||
except (requests.exceptions.RequestException):
|
||||
logger.warning("Location:Error fetching NWS Marine PZ data")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
|
||||
marine_pz_data = marine_pz_data.text
|
||||
#validate data
|
||||
todayDate = today.strftime("%Y%m%d")
|
||||
if marine_pz_data.startswith("Expires:"):
|
||||
expires = marine_pz_data.split(";;")[0].split(":")[1]
|
||||
expires_date = expires[:8]
|
||||
if expires_date < todayDate:
|
||||
logger.debug("Location: NWS Marine PZ data expired")
|
||||
todayDate = datetime.now().strftime("%Y%m%d")
|
||||
if marine_pz_data and marine_pz_data.startswith("Expires:"):
|
||||
try:
|
||||
expires = marine_pz_data.split(";;")[0].split(":")[1]
|
||||
expires_date = expires[:8]
|
||||
if expires_date < todayDate:
|
||||
logger.debug("Location: NWS Marine PZ data expired")
|
||||
return ERROR_FETCHING_DATA
|
||||
except Exception as e:
|
||||
logger.debug(f"Location: NWS Marine PZ data parse error: {e}")
|
||||
return ERROR_FETCHING_DATA
|
||||
else:
|
||||
logger.debug("Location: NWS Marine PZ data not valid")
|
||||
logger.debug("Location: NWS Marine PZ data not valid or empty")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
# process the marine forecast data
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import logging
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
import re
|
||||
from datetime import datetime, timedelta
|
||||
from modules.settings import *
|
||||
# if LOGGING_LEVEL is not set in settings.py, default to DEBUG
|
||||
if not LOGGING_LEVEL:
|
||||
@@ -38,11 +36,17 @@ class CustomFormatter(logging.Formatter):
|
||||
return formatter.format(record)
|
||||
|
||||
class plainFormatter(logging.Formatter):
|
||||
ansi_escape = re.compile(r'\x1b\[([0-9]+)(;[0-9]+)*m')
|
||||
ansi_codes = [
|
||||
'\x1b[38;21m', '\x1b[38;5;231m', '\x1b[38;5;39m', '\x1b[38;5;226m',
|
||||
'\x1b[38;5;196m', '\x1b[38;5;46m', '\x1b[38;5;129m', '\x1b[31;1m',
|
||||
'\x1b[37;1m', '\x1b[0m'
|
||||
]
|
||||
|
||||
def format(self, record):
|
||||
message = super().format(record)
|
||||
return self.ansi_escape.sub('', message)
|
||||
for code in self.ansi_codes:
|
||||
message = message.replace(code, '')
|
||||
return message
|
||||
|
||||
# Create logger
|
||||
logger = logging.getLogger("MeshBot System Logger")
|
||||
@@ -56,7 +60,6 @@ msgLogger.propagate = False
|
||||
# Define format for logs
|
||||
logFormat = '%(asctime)s | %(levelname)8s | %(message)s'
|
||||
msgLogFormat = '%(asctime)s | %(message)s'
|
||||
today = datetime.now()
|
||||
|
||||
# Create stdout handler for logging to the console
|
||||
stdout_handler = logging.StreamHandler()
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# modules/scheduler.py 2025 meshing-around
|
||||
# Scheduler setup for Mesh Bot
|
||||
import asyncio
|
||||
import schedule
|
||||
from modules.log import logger
|
||||
from modules.system import send_message, BroadcastScheduler
|
||||
from modules.system import send_message
|
||||
# methods available for custom scheduler messages
|
||||
from mesh_bot import tell_joke, welcome_message, MOTD, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
|
||||
|
||||
async def setup_scheduler(
|
||||
schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
|
||||
schedulerValue, schedulerTime, schedulerInterval, logger, BroadcastScheduler
|
||||
):
|
||||
schedulerValue, schedulerTime, schedulerInterval, logger, BroadcastScheduler):
|
||||
|
||||
# methods available for custom scheduler messages
|
||||
from mesh_bot import tell_joke, welcome_message, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
|
||||
schedulerValue = schedulerValue.lower().strip()
|
||||
schedulerTime = schedulerTime.strip()
|
||||
schedulerInterval = schedulerInterval.strip()
|
||||
@@ -23,7 +24,8 @@ async def setup_scheduler(
|
||||
scheduler_message = schedulerMessage
|
||||
|
||||
# Basic Scheduler Options
|
||||
if 'custom' not in schedulerValue:
|
||||
basicOptions = ['day', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'hour', 'min']
|
||||
if any(option.lower() in schedulerValue.lower() for option in basicOptions):
|
||||
# Basic scheduler job to run the schedule see examples below for custom schedules
|
||||
if schedulerValue.lower() == 'day':
|
||||
if schedulerTime != '':
|
||||
@@ -49,16 +51,28 @@ async def setup_scheduler(
|
||||
elif 'min' in schedulerValue.lower():
|
||||
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {schedulerInterval} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
else:
|
||||
# Default schedule if no valid configuration is provided
|
||||
elif 'joke' in schedulerValue.lower():
|
||||
# Schedule to send a joke every specified interval
|
||||
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the joke scheduler to send a joke every {schedulerInterval} minutes on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'weather' in schedulerValue.lower():
|
||||
# Schedule to send weather updates every specified interval
|
||||
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(handle_wxc(0, schedulerInterface, 'wx'), schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the weather scheduler to send weather updates every {schedulerInterval} hours on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'custom' in schedulerValue.lower():
|
||||
# Custom scheduler job to run the schedule see examples below
|
||||
|
||||
# custom scheduler job to run the schedule see examples below
|
||||
logger.debug(f"System: Starting the scheduler to send reminder every Monday at noon on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
logger.debug(f"System: Starting the custom scheduler default to send reminder every Monday at noon on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Enabled Reminder"))
|
||||
|
||||
# send a joke every 15 minutes
|
||||
#schedule.every(15).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
|
||||
|
||||
# Place your custom schedule code below this line, helps with merges
|
||||
|
||||
# Place your custom schedule code above this line
|
||||
|
||||
# Start the Broadcast Scheduler
|
||||
await BroadcastScheduler()
|
||||
except Exception as e:
|
||||
|
||||
@@ -271,6 +271,7 @@ try:
|
||||
secure_interface = config['sentry'].getint('SentryInterface', 1) # default 1
|
||||
sentry_holdoff = config['sentry'].getint('SentryHoldoff', 9) # default 9
|
||||
sentryIgnoreList = config['sentry'].get('sentryIgnoreList', '').split(',')
|
||||
sentryWatchList = config['sentry'].get('sentryWatchList', '').split(',')
|
||||
sentry_radius = config['sentry'].getint('SentryRadius', 100) # default 100 meters
|
||||
email_sentry_alerts = config['sentry'].getboolean('emailSentryAlerts', False) # default False
|
||||
highfly_enabled = config['sentry'].getboolean('highFlyingAlert', True) # default True
|
||||
@@ -281,6 +282,9 @@ try:
|
||||
highfly_check_openskynetwork = config['sentry'].getboolean('highflyOpenskynetwork', True) # default True check with OpenSkyNetwork if highfly detected
|
||||
detctionSensorAlert = config['sentry'].getboolean('detectionSensorAlert', False) # default False
|
||||
reqLocationEnabled = config['sentry'].getboolean('reqLocationEnabled', False) # default False
|
||||
cmdShellSentryAlerts = config['sentry'].getboolean('cmdShellSentryAlerts', False) # default False
|
||||
sentryAlertNear = config['sentry'].get('sentryAlertNear', 'sentry_alert_near.sh') # default sentry_alert_near.sh
|
||||
sentryAlertFar = config['sentry'].get('sentryAlertFar', 'sentry_alert_far.sh') # default sentry_alert_far.sh
|
||||
|
||||
# location
|
||||
location_enabled = config['location'].getboolean('enabled', True)
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
import json
|
||||
import os # For file operations
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from collections import Counter
|
||||
from modules.log import *
|
||||
|
||||
@@ -61,8 +63,9 @@ class SurveyModule:
|
||||
'answers': [],
|
||||
'location': location if surveyRecordLocation and location is not None else 'N/A'
|
||||
}
|
||||
msg = f"'{survey_name}'📝survey\nSend answer' or 'end'\n"
|
||||
msg = f"'{survey_name}'📝survey\n"
|
||||
msg += self.show_question(user_id)
|
||||
msg += f"\nSend answer' or 'end'"
|
||||
return msg
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting survey for user {user_id}: {e}")
|
||||
@@ -96,17 +99,92 @@ class SurveyModule:
|
||||
filename = os.path.join(self.response_dir, f'{survey_name}_responses.csv')
|
||||
try:
|
||||
with open(filename, 'a', encoding='utf-8') as f:
|
||||
row = list(map(str, self.responses[user_id]['answers']))
|
||||
if surveyRecordID:
|
||||
row.insert(0, str(user_id))
|
||||
if surveyRecordLocation:
|
||||
location = self.responses[user_id].get('location')
|
||||
row.insert(1 if surveyRecordID else 0, str(location) if location is not None else "N/A")
|
||||
# Always write: timestamp, userID, position, answers...
|
||||
timestamp = datetime.now().strftime('%d%m%Y%H%M%S')
|
||||
user_id_str = str(user_id)
|
||||
location = self.responses[user_id].get('location', "N/A")
|
||||
answers = list(map(str, self.responses[user_id]['answers']))
|
||||
row = [timestamp, user_id_str, str(location)] + answers
|
||||
f.write(','.join(row) + '\n')
|
||||
logger.info(f"Survey: Responses for user {user_id} saved for survey '{survey_name}' to {filename}.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving responses to {filename}: {e}")
|
||||
|
||||
def format_survey_results(self, results):
|
||||
if isinstance(results, dict) and "error" in results:
|
||||
return results["error"]
|
||||
if not results:
|
||||
return "No results found."
|
||||
msg = "📊Survey Results:\n"
|
||||
for idx, q in enumerate(results):
|
||||
msg += f"\nQ{idx+1}: {q['question']}\n"
|
||||
if q['type'] == 'multiple_choice':
|
||||
for opt, count in q['summary'].items():
|
||||
msg += f" {opt}: {count}\n"
|
||||
elif q['type'] == 'integer':
|
||||
s = q['summary']
|
||||
msg += f" Count: {s['count']}, Avg: {s['average']:.2f}, Min: {s['min']}, Max: {s['max']}\n"
|
||||
elif q['type'] == 'text':
|
||||
msg += f" Responses: {q['summary']['responses_count']}\n"
|
||||
return msg
|
||||
|
||||
def get_survey_results(self, survey_name='example'):
|
||||
if survey_name not in self.surveys:
|
||||
return {"error": f"Survey '{survey_name}' not found."}
|
||||
filename = os.path.join(self.response_dir, f'{survey_name}_responses.csv')
|
||||
questions = self.surveys[survey_name]
|
||||
results = []
|
||||
try:
|
||||
with open(filename, encoding='utf-8') as f:
|
||||
reader = csv.reader(f)
|
||||
lines = []
|
||||
for row in reader:
|
||||
if not row or len(row) < 4:
|
||||
continue
|
||||
# If location field is split due to comma, join columns 2 and 3
|
||||
if row[2].startswith('[') and not row[2].endswith(']') and len(row) > 4:
|
||||
location = row[2] + ',' + row[3]
|
||||
answers = row[4:]
|
||||
else:
|
||||
location = row[2]
|
||||
answers = row[3:]
|
||||
lines.append(answers)
|
||||
|
||||
for q_idx, question in enumerate(questions):
|
||||
qtype = question.get('type', 'multiple_choice')
|
||||
answers = [row[q_idx] for row in lines if len(row) > q_idx]
|
||||
|
||||
summary = {}
|
||||
if qtype == 'multiple_choice':
|
||||
counts = Counter(answers)
|
||||
summary = {chr(65+i): counts.get(chr(65+i), 0) for i in range(len(question.get('options', [])))}
|
||||
|
||||
elif qtype == 'integer':
|
||||
ints = [int(a) for a in answers if a.isdigit()]
|
||||
summary = {
|
||||
"count": len(ints),
|
||||
"average": sum(ints)/len(ints) if ints else 0,
|
||||
"min": min(ints) if ints else None,
|
||||
"max": max(ints) if ints else None
|
||||
}
|
||||
|
||||
elif qtype == 'text':
|
||||
summary = {"responses_count": len([a for a in answers if a.strip()])}
|
||||
|
||||
|
||||
results.append({
|
||||
"question": question['question'],
|
||||
"type": qtype,
|
||||
"summary": summary
|
||||
})
|
||||
|
||||
return results
|
||||
except FileNotFoundError:
|
||||
return {"error": f"No responses recorded yet for '{survey_name}'."}
|
||||
except Exception as e:
|
||||
logger.error(f"Error summarizing survey results: {e}")
|
||||
return NO_ALERTS
|
||||
|
||||
def answer(self, user_id, answer, location=None):
|
||||
try:
|
||||
"""Record an answer and return the next question or end message."""
|
||||
@@ -125,7 +203,8 @@ class SurveyModule:
|
||||
return "Please answer with a letter (A, B, C, ...)."
|
||||
option_index = ord(answer_char) - 65
|
||||
if 0 <= option_index < len(question['options']):
|
||||
self.responses[user_id]['answers'].append(str(option_index))
|
||||
# Valid answer record letter, not index
|
||||
self.responses[user_id]['answers'].append(answer_char)
|
||||
self.responses[user_id]['current_question'] += 1
|
||||
return f"Recorded..\n" + self.show_question(user_id)
|
||||
else:
|
||||
|
||||
@@ -290,7 +290,7 @@ if voxDetectionEnabled:
|
||||
from modules.radio import * # from the spudgunman/meshing-around repo
|
||||
|
||||
# File Monitor Configuration
|
||||
if file_monitor_enabled or read_news_enabled or bee_enabled:
|
||||
if file_monitor_enabled or read_news_enabled or bee_enabled or enable_runShellCmd or cmdShellSentryAlerts:
|
||||
from modules.filemon import * # from the spudgunman/meshing-around repo
|
||||
if read_news_enabled:
|
||||
trap_list = trap_list + trap_list_filemon # items readnews
|
||||
@@ -410,8 +410,9 @@ for i in range(1, 10):
|
||||
# add channel hash to channel_list
|
||||
for device in channel_list:
|
||||
interface_id = device["interface_id"]
|
||||
interface = globals().get(f'interface{interface_id}')
|
||||
for channel_name, channel_number in device["channels"].items():
|
||||
psk_base64 = base64.b64encode(channel.settings.psk).decode('utf-8')
|
||||
psk_base64 = "AQ==" # default PSK
|
||||
channel_hash = generate_hash(channel_name, psk_base64)
|
||||
# add hash to the channel entry in channel_list under key 'hash'
|
||||
for entry in channel_list:
|
||||
@@ -510,7 +511,6 @@ def get_name_from_number(number, type='long', nodeInt=1):
|
||||
name = str(decimal_to_hex(number)) # If name not found, use the ID as string
|
||||
return name
|
||||
|
||||
|
||||
def get_num_from_short_name(short_name, nodeInt=1):
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
# Get the node number from the short name, converting all to lowercase for comparison (good practice?)
|
||||
@@ -1505,6 +1505,14 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
logger.info(f"System: High Altitude {position_data['altitude']}m on Device: {rxNode} Channel: {channel} NodeID:{nodeID} Lat:{position_data.get('latitude', 0)} Lon:{position_data.get('longitude', 0)}")
|
||||
altFeet = round(position_data['altitude'] * 3.28084, 2)
|
||||
msg = f"🚀 High Altitude Detected! NodeID:{nodeID} Alt:{altFeet:,.0f}ft/{position_data['altitude']:,.0f}m"
|
||||
|
||||
# throttle sending alerts for the same node more than once every 30 minutes
|
||||
last_alert_time = positionMetadata[nodeID].get('lastHighFlyAlert', 0)
|
||||
current_time = time.time()
|
||||
if current_time - last_alert_time < 1800:
|
||||
return False # less than 30 minutes since last alert
|
||||
positionMetadata[nodeID]['lastHighFlyAlert'] = current_time
|
||||
|
||||
if highfly_check_openskynetwork:
|
||||
# check get_openskynetwork to see if the node is an aircraft
|
||||
if 'latitude' in position_data and 'longitude' in position_data:
|
||||
@@ -1522,6 +1530,7 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
if abs(node_alt - plane_alt) <= 900: # within 900m
|
||||
msg += f"\n✈️Detected near:\n{flight_info}"
|
||||
send_message(msg, highfly_channel, 0, highfly_interface)
|
||||
|
||||
# Keep the positionMetadata dictionary at a maximum size
|
||||
if len(positionMetadata) > MAX_SEEN_NODES:
|
||||
# Remove the oldest entry
|
||||
@@ -2005,45 +2014,62 @@ handleSentinel_spotted = []
|
||||
handleSentinel_loop = 0
|
||||
async def handleSentinel(deviceID):
|
||||
global handleSentinel_spotted, handleSentinel_loop
|
||||
detectedNearby = ""
|
||||
detectedNearby = None
|
||||
resolution = "unknown"
|
||||
closest_nodes = await 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:
|
||||
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"
|
||||
closest_nodes = await get_closest_nodes(deviceID, returnCount=10)
|
||||
#logger.debug(f"handleSentinel: closest_nodes={closest_nodes}")
|
||||
|
||||
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')
|
||||
if not closest_nodes or closest_nodes == ERROR_FETCHING_DATA:
|
||||
return
|
||||
|
||||
logger.warning(f"System: {detectedNearby} is close to your location on Interface{deviceID} Accuracy is {resolution}bits")
|
||||
send_message(f"Sentry{deviceID}: {detectedNearby}", secure_channel, 0, secure_interface)
|
||||
if enableSMTP and email_sentry_alerts:
|
||||
for email in sysopEmails:
|
||||
send_email(email, f"Sentry{deviceID}: {detectedNearby}")
|
||||
handleSentinel_loop = 0
|
||||
handleSentinel_spotted.append({'id': closest_node, 'distance': closest_distance})
|
||||
else:
|
||||
# Find any watched node inside or outside the zone
|
||||
for node in closest_nodes:
|
||||
node_id = node['id']
|
||||
distance = node['distance']
|
||||
|
||||
if str(node_id) in sentryIgnoreList:
|
||||
return
|
||||
# Message conditions
|
||||
if distance >= sentry_radius and str(node_id) and str(node_id) in sentryWatchList:
|
||||
# Outside zone
|
||||
detectedNearby = f"{get_name_from_number(node_id, 'long', deviceID)}, {get_name_from_number(node_id, 'short', deviceID)}, {node_id}, {decimal_to_hex(node_id)} at {distance}m (OUTSIDE ZONE)"
|
||||
elif distance <= sentry_radius and str(node_id) not in sentryWatchList:
|
||||
# Inside the zone
|
||||
detectedNearby = f"{get_name_from_number(node_id, 'long', deviceID)}, {get_name_from_number(node_id, 'short', deviceID)}, {node_id}, {decimal_to_hex(node_id)} at {distance}m (INSIDE ZONE)"
|
||||
|
||||
#logger.debug(f"handleSentinel: loop={handleSentinel_loop}/{sentry_holdoff}, detectedNearby={detectedNearby} closest_nodes={closest_nodes}")
|
||||
if detectedNearby:
|
||||
handleSentinel_loop += 1
|
||||
#logger.debug(f"handleSentinel: detectedNearby={detectedNearby}, loop={handleSentinel_loop}/{sentry_holdoff}")
|
||||
if handleSentinel_loop >= sentry_holdoff:
|
||||
# Get resolution if available
|
||||
if positionMetadata and node_id in positionMetadata:
|
||||
metadata = positionMetadata[node_id]
|
||||
if metadata.get('precisionBits') is not None:
|
||||
resolution = metadata.get('precisionBits')
|
||||
# Send message alert
|
||||
logger.warning(f"System: {detectedNearby} on Interface{deviceID} Accuracy is {resolution}bits")
|
||||
send_message(f"Sentry{deviceID}: {detectedNearby}", secure_channel, 0, secure_interface)
|
||||
|
||||
# Send email alerts
|
||||
if enableSMTP and email_sentry_alerts:
|
||||
for email in sysopEmails:
|
||||
send_email(email, f"Sentry{deviceID}: {detectedNearby}")
|
||||
|
||||
# Execute external script alerts
|
||||
if cmdShellSentryAlerts and distance <= sentry_radius:
|
||||
# inside zone
|
||||
call_external_script('', script=sentryAlertNear)
|
||||
logger.info(f"System: Sentry Script Alert {sentryAlertNear} for NodeID:{node_id} on Interface{deviceID}")
|
||||
elif cmdShellSentryAlerts and distance >= sentry_radius:
|
||||
# outside zone
|
||||
call_external_script('', script=sentryAlertFar)
|
||||
logger.info(f"System: Sentry Script Alert {sentryAlertFar} for NodeID:{node_id} on Interface{deviceID}")
|
||||
|
||||
handleSentinel_loop = 0 # Loop reset
|
||||
else:
|
||||
handleSentinel_loop = 0 # Reset if nothing detected
|
||||
|
||||
async def process_vox_queue():
|
||||
# process the voxMsgQueue
|
||||
|
||||
98
pong_bot.py
98
pong_bot.py
@@ -10,6 +10,7 @@ except ImportError:
|
||||
|
||||
import asyncio
|
||||
import time # for sleep, get some when you can :)
|
||||
from datetime import datetime
|
||||
import random
|
||||
from modules.log import *
|
||||
from modules.system import *
|
||||
@@ -92,10 +93,13 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
else:
|
||||
msg = "🔊 Can you hear me now?"
|
||||
|
||||
if hop == "Direct":
|
||||
msg = msg + f"SNR:{snr} RSSI:{rssi}"
|
||||
else:
|
||||
msg = msg + hop
|
||||
# append SNR/RSSI or hop info
|
||||
if hop.startswith("Direct?") and (snr != 0 or rssi != 0):
|
||||
msg += f"? SNR:{snr} RSSI:{rssi}"
|
||||
elif hop.startswith("Direct"):
|
||||
msg += f"SNR:{snr} RSSI:{rssi}"
|
||||
elif hop:
|
||||
msg += f"{hop}"
|
||||
|
||||
if "@" in message:
|
||||
msg = msg + " @" + message.split("@")[1]
|
||||
@@ -150,7 +154,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
def handle_motd(message, message_from_id, isDM):
|
||||
global MOTD
|
||||
isAdmin = False
|
||||
msg = ""
|
||||
msg = MOTD
|
||||
# check if the message_from_id is in the bbs_admin_list
|
||||
if bbs_admin_list != ['']:
|
||||
for admin in bbs_admin_list:
|
||||
@@ -216,11 +220,16 @@ def onReceive(packet, interface):
|
||||
rxType = type(interface).__name__
|
||||
|
||||
# Valies assinged to the packet
|
||||
rxNode, message_from_id, snr, rssi, hop, hop_away, channel_number = 0, 0, 0, 0, 0, 0, 0
|
||||
rxNode = message_from_id = snr = rssi = hop = hop_away = channel_number = hop_start = hop_count = hop_limit = 0
|
||||
pkiStatus = (False, 'ABC')
|
||||
replyIDset = False
|
||||
rxNodeHostName = None
|
||||
emojiSeen = False
|
||||
simulator_flag = False
|
||||
isDM = False
|
||||
channel_name = "unknown"
|
||||
session_passkey = None
|
||||
playingGame = False
|
||||
|
||||
if DEBUGpacket:
|
||||
# Debug print the interface object
|
||||
@@ -229,46 +238,45 @@ def onReceive(packet, interface):
|
||||
# Debug print the packet for debugging
|
||||
logger.debug(f"Packet Received\n {packet} \n END of packet \n")
|
||||
|
||||
# set the value for the incomming interface
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
if port1 in rxInterface: rxNode = 1
|
||||
elif multiple_interface and port2 in rxInterface: rxNode = 2
|
||||
elif multiple_interface and port3 in rxInterface: rxNode = 3
|
||||
elif multiple_interface and port4 in rxInterface: rxNode = 4
|
||||
elif multiple_interface and port5 in rxInterface: rxNode = 5
|
||||
elif multiple_interface and port6 in rxInterface: rxNode = 6
|
||||
elif multiple_interface and port7 in rxInterface: rxNode = 7
|
||||
elif multiple_interface and port8 in rxInterface: rxNode = 8
|
||||
elif multiple_interface and port9 in rxInterface: rxNode = 9
|
||||
|
||||
# determine the rxNode based on the interface type
|
||||
if rxType == 'TCPInterface':
|
||||
rxHost = interface.__dict__.get('hostname', 'unknown')
|
||||
if rxHost and hostname1 in rxHost and interface1_type == 'tcp': rxNode = 1
|
||||
elif multiple_interface and rxHost and hostname2 in rxHost and interface2_type == 'tcp': rxNode = 2
|
||||
elif multiple_interface and rxHost and hostname3 in rxHost and interface3_type == 'tcp': rxNode = 3
|
||||
elif multiple_interface and rxHost and hostname4 in rxHost and interface4_type == 'tcp': rxNode = 4
|
||||
elif multiple_interface and rxHost and hostname5 in rxHost and interface5_type == 'tcp': rxNode = 5
|
||||
elif multiple_interface and rxHost and hostname6 in rxHost and interface6_type == 'tcp': rxNode = 6
|
||||
elif multiple_interface and rxHost and hostname7 in rxHost and interface7_type == 'tcp': rxNode = 7
|
||||
elif multiple_interface and rxHost and hostname8 in rxHost and interface8_type == 'tcp': rxNode = 8
|
||||
elif multiple_interface and rxHost and hostname9 in rxHost and interface9_type == 'tcp': rxNode = 9
|
||||
rxNodeHostName = interface.__dict__.get('ip', None)
|
||||
rxNode = next(
|
||||
(i for i in range(1, 10)
|
||||
if multiple_interface and rxHost and
|
||||
globals().get(f'hostname{i}', '').split(':', 1)[0] in rxHost and
|
||||
globals().get(f'interface{i}_type', '') == 'tcp'),None)
|
||||
|
||||
if rxType == 'BLEInterface':
|
||||
if interface1_type == 'ble': rxNode = 1
|
||||
elif multiple_interface and interface2_type == 'ble': rxNode = 2
|
||||
elif multiple_interface and interface3_type == 'ble': rxNode = 3
|
||||
elif multiple_interface and interface4_type == 'ble': rxNode = 4
|
||||
elif multiple_interface and interface5_type == 'ble': rxNode = 5
|
||||
elif multiple_interface and interface6_type == 'ble': rxNode = 6
|
||||
elif multiple_interface and interface7_type == 'ble': rxNode = 7
|
||||
elif multiple_interface and interface8_type == 'ble': rxNode = 8
|
||||
elif multiple_interface and interface9_type == 'ble': rxNode = 9
|
||||
if rxType == 'SerialInterface':
|
||||
rxInterface = interface.__dict__.get('devPath', 'unknown')
|
||||
rxNode = next(
|
||||
(i for i in range(1, 10)
|
||||
if globals().get(f'port{i}', '') in rxInterface),None)
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if rxType == 'BLEInterface':
|
||||
rxNode = next(
|
||||
(i for i in range(1, 10)
|
||||
if globals().get(f'interface{i}_type', '') == 'ble'),0)
|
||||
|
||||
if rxNode is None:
|
||||
# default to interface 1 ## FIXME needs better like a default interface setting or hash lookup
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] in ['ADMIN_APP', 'SIMULATOR_APP']:
|
||||
session_passkey = packet.get('decoded', {}).get('admin', {}).get('sessionPasskey', None)
|
||||
rxNode = 1
|
||||
|
||||
# check if the packet has a channel flag use it ## FIXME needs to be channel hash lookup
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel')
|
||||
channel_name = "unknown"
|
||||
# get channel name from channel number from connected devices
|
||||
for device in channel_list:
|
||||
if device["interface_id"] == rxNode:
|
||||
device_channels = device['channels']
|
||||
for chan_name, info in device_channels.items():
|
||||
if info['number'] == channel_number:
|
||||
channel_name = chan_name
|
||||
break
|
||||
|
||||
# get channel hashes for the interface
|
||||
device = next((d for d in channel_list if d["interface_id"] == rxNode), None)
|
||||
if device:
|
||||
@@ -340,8 +348,8 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
hop_count = hop_away
|
||||
|
||||
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
|
||||
hop = "Last Hop"
|
||||
if hop == "" and hop_count > 0:
|
||||
hop = f"{hop_count} Hop" if hop_count == 1 else f"{hop_count} Hops"
|
||||
|
||||
if hop_start == hop_limit and "lora" in str(transport_mechanism).lower():
|
||||
hop = "Direct"
|
||||
@@ -349,11 +357,15 @@ def onReceive(packet, interface):
|
||||
if ((hop_start == 0 and hop_limit >= 0) or via_mqtt or ("mqtt" in str(transport_mechanism).lower())):
|
||||
hop = "MQTT"
|
||||
|
||||
## FIXME should this be here?
|
||||
if hop == "" and hop_count ==0 and (snr != 0 or rssi != 0):
|
||||
hop = "Direct?"
|
||||
|
||||
if "unknown" in str(transport_mechanism).lower() and (snr == 0 and rssi == 0):
|
||||
hop = "IP-Network"
|
||||
|
||||
if enableHopLogs:
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism}")
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start} calculated_hop_count:{hop_count} final_hop_value:{hop} via_mqtt:{via_mqtt} transport_mechanism:{transport_mechanism} Hostname:{rxNodeHostName}")
|
||||
|
||||
# check with stringSafeChecker if the message is safe
|
||||
if stringSafeCheck(message_string) is False:
|
||||
|
||||
@@ -42,3 +42,15 @@ then
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Get public and local IP addresses
|
||||
public_ip=$(curl -s https://ifconfig.me 2>/dev/null)
|
||||
public_ip=${public_ip:-""}
|
||||
local_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
local_ip=${local_ip:-""}
|
||||
if [ -n "$public_ip" ]; then
|
||||
echo "Public IP: $public_ip"
|
||||
fi
|
||||
if [ -n "$local_ip" ]; then
|
||||
echo "Local IP: $local_ip"
|
||||
fi
|
||||
Reference in New Issue
Block a user