Compare commits

...

69 Commits

Author SHA1 Message Date
SpudGunMan
a8b2aefa28 Update locationdata.py 2025-10-21 22:31:57 -07:00
SpudGunMan
849565cacb Update joke.py 2025-10-21 21:40:20 -07:00
SpudGunMan
fdec3a6754 Update README.md 2025-10-21 20:56:47 -07:00
SpudGunMan
f3a97bc567 Update README.md 2025-10-21 20:45:40 -07:00
SpudGunMan
02625ad0f2 Update install.sh 2025-10-21 20:18:55 -07:00
SpudGunMan
b4ba4b0daf Update locationdata.py 2025-10-21 18:52:40 -07:00
SpudGunMan
fd7f8a94f5 Update locationdata.py 2025-10-21 18:47:29 -07:00
SpudGunMan
d252250edd last_alert_time 🚀
throttle sending alerts for the same node more than once every 30 minutes
2025-10-21 16:42:36 -07:00
SpudGunMan
d6410e0461 Update joke.py 2025-10-21 15:47:06 -07:00
SpudGunMan
050b4ab3ce Update joke.py 2025-10-21 15:46:48 -07:00
SpudGunMan
8ac1a1eed7 Update joke.py 2025-10-21 15:45:30 -07:00
SpudGunMan
370a417ce6 Update config.template 2025-10-21 15:10:13 -07:00
SpudGunMan
378b05df35 PingRefactor
anyone notice this?
2025-10-21 14:50:06 -07:00
SpudGunMan
d002c5ede8 remove LastHop 2025-10-21 14:26:43 -07:00
SpudGunMan
cd03cc56b4 🐇 2025-10-21 14:24:24 -07:00
SpudGunMan
d4fd484706 sentry_alert.sh
this enhances the sentry to optionally run a shell command you would create in the script/directory which will fire every time the alert fires. sentry_alert_near.sh and sentry_alert_far.sh are the needed files. it will error and remind you it cant find them.
2025-10-21 14:21:09 -07:00
SpudGunMan
82d519279e servicePackAttack
enhance for armbian builds
2025-10-21 13:34:38 -07:00
SpudGunMan
09302e8c91 ntp 2025-10-21 13:17:15 -07:00
SpudGunMan
91fc4605ec enhance 2025-10-21 12:57:10 -07:00
SpudGunMan
abc6c07ee3 Update locationdata.py 2025-10-21 12:52:34 -07:00
SpudGunMan
bbf8b04bd3 Update locationdata.py 2025-10-21 12:45:29 -07:00
SpudGunMan
5fd293c990 only seen with soft nodes
## FIXME needs better like a default interface setting or hash lookup
2025-10-21 12:33:13 -07:00
SpudGunMan
a9a65a6c6d refactor rxInt 2025-10-21 11:00:26 -07:00
SpudGunMan
34e95c86d6 log IP if there 2025-10-21 10:52:28 -07:00
Kelly
80891090c3 Merge pull request #222 from pdxlocations/allow-port-numbers
Missed a Spot to support hostname:port
thanks!
2025-10-21 10:49:00 -07:00
SpudGunMan
1b098fbf7b Update pong_bot.py 2025-10-21 10:47:42 -07:00
SpudGunMan
165d76cf8d add IP 2025-10-21 10:45:44 -07:00
Kelly
045c9d433b Merge pull request #223 from SpudGunMan/copilot/add-pylint-disable-comment 2025-10-21 10:28:51 -07:00
SpudGunMan
fbe5e008de Revert "Update joke.py"
This reverts commit 004adc7d9a.
2025-10-21 10:28:02 -07:00
Kelly
004adc7d9a Update joke.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-21 10:23:12 -07:00
copilot-swe-agent[bot]
9a2033452f Add Pylint disable comment above variable on line 58
Co-authored-by: SpudGunMan <12676665+SpudGunMan@users.noreply.github.com>
2025-10-21 17:08:51 +00:00
copilot-swe-agent[bot]
5638204f82 Initial plan 2025-10-21 17:05:27 +00:00
pdxlocations
e5c3b0cceb refactor 2025-10-21 09:33:01 -07:00
pdxlocations
18ac53b230 Refactor TCP interface handling to support hostname:poert 2025-10-21 09:17:54 -07:00
SpudGunMan
4aa65dad6a Update mesh_bot.py
https://github.com/SpudGunMan/meshing-around/issues/220
2025-10-21 09:03:00 -07:00
SpudGunMan
f65a7b7934 refactorMwx 2025-10-21 06:56:03 -07:00
SpudGunMan
886293087a Update greetings.yml 2025-10-20 23:10:25 -07:00
SpudGunMan
e05e6f3451 Update greetings.yml 2025-10-20 23:07:31 -07:00
SpudGunMan
4b5dd934e9 enhance 2025-10-20 23:07:03 -07:00
Kelly
008ddfb5a2 Merge pull request #221 from SpudGunMan/lab
DependaBot
2025-10-20 23:02:13 -07:00
SpudGunMan
e1330b9b9e DependaBot
DependaBot
2025-10-20 22:56:34 -07:00
SpudGunMan
7b43213094 Update README.md 2025-10-20 21:57:46 -07:00
SpudGunMan
b17c2b17ee Update system.py 2025-10-20 20:20:25 -07:00
SpudGunMan
b6505ee577 Update system.py 2025-10-20 20:19:43 -07:00
SpudGunMan
f7379b7ca5 Update system.py 2025-10-20 20:19:30 -07:00
SpudGunMan
ec9a1d88db Update system.py 2025-10-20 20:15:29 -07:00
SpudGunMan
a339570afe Update system.py 2025-10-20 20:10:08 -07:00
SpudGunMan
f5af9f419a Update system.py 2025-10-20 20:06:39 -07:00
SpudGunMan
ad5c1c90da Update system.py 2025-10-20 19:55:57 -07:00
SpudGunMan
eb1e0c82ea enhance Sentinel
# list of watched nodes numbers
sentryWatchList =
monitors for INSIDE or OUTSIDE the zone
2025-10-20 19:50:06 -07:00
SpudGunMan
8d2277bc59 Update SECURITY.md 2025-10-20 17:46:57 -07:00
Kelly
dc9908a72c Revise security support information and reporting process
Updated the security support table and reporting guidelines.
2025-10-20 17:08:31 -07:00
SpudGunMan
21123d2993 sysinfo from a DM
now lets you see the IP addresses of the bot
2025-10-20 16:33:58 -07:00
SpudGunMan
7dc3134d0b Update system.py 2025-10-20 16:20:27 -07:00
SpudGunMan
b125178492 Update survey.py 2025-10-20 16:16:49 -07:00
SpudGunMan
2ad9e84c33 Update pong_bot.py 2025-10-20 15:55:30 -07:00
SpudGunMan
63bd288caa fixMOTD
theMOTD is how did this happen
2025-10-20 15:40:32 -07:00
SpudGunMan
5c7d199831 surveyReport
run survey report to see return data
2025-10-20 15:34:09 -07:00
SpudGunMan
f56a39eeb6 refactor 2025-10-20 13:07:30 -07:00
SpudGunMan
ae5991ee39 moreTime
to spare
2025-10-20 13:03:33 -07:00
SpudGunMan
1324f83f17 enhance schedule
allow basic Joke and Weather messages with out extra config
# value can also be joke (everyXmin) or weather (hour) for special scheduled messages
# custom for module/scheduler.py custom schedule examples
2025-10-20 12:07:22 -07:00
SpudGunMan
08ae8c31a0 enhance:fix
I added some things to help any future aarg, sorry I broke it again I also
2025-10-20 11:45:49 -07:00
SpudGunMan
957e803951 cleanup 2025-10-20 11:35:26 -07:00
SpudGunMan
2de3441d67 enhance
with time stamp and also better CSV answers for review
2025-10-20 09:54:44 -07:00
SpudGunMan
2b420022f9 timeFix
will be make year 2000
2025-10-20 09:54:25 -07:00
SpudGunMan
d01f143adf Update system.py 2025-10-20 09:24:18 -07:00
SpudGunMan
5f5aeeadac Update system.py 2025-10-20 09:22:45 -07:00
SpudGunMan
d57826613c moved
to slurp repo
2025-10-20 09:19:18 -07:00
SpudGunMan
af09dc0cf9 improveUse 2025-10-19 22:43:51 -07:00
23 changed files with 491 additions and 443 deletions

11
.github/dependabot.yml vendored Normal file
View 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
View 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"

View File

@@ -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)
![Example Use](etc/pong-bot.jpg "Example Use")
## 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
View 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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"\nDetected 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

View File

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

View File

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