forked from iarv/meshing-around
Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 011bac41f2 | |||
| 20467ea886 | |||
| bbfd71f011 | |||
| e1ff87a197 | |||
| a859f830bb | |||
| d99698e7f3 | |||
| 5ecc563e96 | |||
| eeeb43cacc | |||
| 9fdcea56fc | |||
| 24a33fe882 | |||
| 5710cebf39 | |||
| b66487863d | |||
| b3c4d208b7 | |||
| f41ff2d5f7 | |||
| 48366bc595 | |||
| 02dd64382d | |||
| 731b48ad65 | |||
| 69a7082669 | |||
| fafa7d8a51 | |||
| 6e69b5f014 | |||
| 03895248cd | |||
| a79de8a325 | |||
| 740b53f02f | |||
| 76e75551c6 | |||
| 51752ae896 | |||
| d81e773c0c | |||
| 1f1ed1ca70 | |||
| df9f3806a3 | |||
| 081ccd9e2e | |||
| d9a7dafe6e | |||
| 921225965b | |||
| 3659254785 | |||
| 7c502608f6 | |||
| 427c25f80b | |||
| c3f15390ea | |||
| e1476a44c6 | |||
| 72070fef3e | |||
| b63ea677f6 | |||
| f8389500b8 | |||
| b257625a45 | |||
| a233d8c7b3 | |||
| 11c9742ebe | |||
| 5af28c3dc2 | |||
| aebb9e3c20 | |||
| d5916f4ccc | |||
| 056159a3f3 | |||
| 2f6049d94b | |||
| a2d7f664ab | |||
| b26491b646 | |||
| 22e97b0eec | |||
| f540866d08 | |||
| c9729c8214 | |||
| 49901cbbee | |||
| 2aa2b80935 | |||
| 95695f4f58 | |||
| b641d2b5e8 | |||
| 51d8faab12 | |||
| 7a1396b99d | |||
| 819bbbcaf4 | |||
| 0eeda96670 | |||
| 18cca4ffdd | |||
| d169fe2dff | |||
| 1c732dfe17 | |||
| bdad3927e5 | |||
| 0e0d6416d9 | |||
| 0da780371a | |||
| 37bf30cbc0 | |||
| 817a8601dd | |||
| 47cca409be | |||
| e08a82ec39 | |||
| 345541dfb5 | |||
| 6e89762f1d | |||
| 0fb26bc16a | |||
| f1ad5966af | |||
| ac57d4683f | |||
| eab099e5ee | |||
| 685bd3491d | |||
| b8d64f3a9e | |||
| 852d491030 | |||
| 76565c5546 | |||
| af1ec1630e | |||
| 0c2b36a206 | |||
| c0934096f0 | |||
| 819bfaba90 | |||
| 8041a1296b | |||
| 10d93b4fd3 | |||
| 19dedef1e6 | |||
| d4af0c7e8b | |||
| 8730f0fd38 | |||
| 9cda8daf65 | |||
| a9223f1613 | |||
| 04ca4c99b8 | |||
| 3072520e63 | |||
| bd6603766b | |||
| 075a23bd2b | |||
| a8e4f653ed | |||
| 374a44f4a9 | |||
| 3c8d2e646e | |||
| e5df983244 | |||
| fa5f9250c4 | |||
| 3f7a831690 | |||
| 89aaaddae9 | |||
| e1919616c2 | |||
| 8b9e637006 | |||
| 0df3e32901 | |||
| 1c2fa174ea | |||
| c97aefcef1 | |||
| dfb94c3993 | |||
| 7d62f69f12 | |||
| cf896767fb | |||
| 1eb4cf71ed | |||
| e959124eac | |||
| d787c72812 | |||
| 9f0dd56d43 | |||
| aa71e6045a | |||
| a140ad83cd | |||
| 93c2d731e8 | |||
| d8da553af9 | |||
| 9d9f070908 | |||
| 0f2061af55 | |||
| d8423584d4 | |||
| 843320d268 | |||
| 216128b15a | |||
| f8bc574753 | |||
| 6193c5933f | |||
| b668965bda | |||
| ae039b5baf | |||
| 824d43f16e | |||
| 2de76e6c5e | |||
| afb02602fd | |||
| 99528c2bcf | |||
| b53f5821f3 | |||
| 93fc6547b8 | |||
| 9a7e321dff | |||
| 39257f2d39 | |||
| 8c5abecac3 | |||
| 16dcc96037 | |||
| b1d32a7745 | |||
| 631a2f53ea | |||
| 32903c97e3 | |||
| 6e61e8122d | |||
| d109803f9d | |||
| 09ed4f57cf | |||
| acfb8078a9 | |||
| 84f9693833 | |||
| 50fdcf486d | |||
| eab5afccc8 | |||
| ea9db47c2d | |||
| cf3a9c5b43 | |||
| adedaa092c | |||
| f204237a63 | |||
| 057a400041 | |||
| 4cdf68f074 | |||
| 003a11c557 | |||
| 8d309fa579 | |||
| 232f9c24db | |||
| 39dccd149b | |||
| b921c73fa7 | |||
| f3ec1cbe93 | |||
| a6bcfda0ac |
@@ -41,7 +41,15 @@ Welcome to the Mesh Bot project! This feature-rich bot is designed to enhance yo
|
||||
### Proximity Alerts
|
||||
- **Location-Based Alerts**: Get notified when members arrive back at a configured lat/long, perfect for remote locations like campsites.
|
||||
- **High Flying Alerts**: Get notified when nodes with high altitude are seen on mesh
|
||||
- **Hey Chirpy**: Voice activate send messages with "hey chirpy"
|
||||
- **Voice/Command Triggers**: The following keywords can be used via voice (VOX) to trigger bot functions "Hey Chirpy!"
|
||||
- Say "Hey Chirpy.."
|
||||
- `joke`: Tells a joke
|
||||
- `weather`: Returns local weather forecast
|
||||
- `moon`: Returns moonrise/set and phase info
|
||||
- `daylight`: Returns sunrise/sunset times
|
||||
- `river`: Returns NOAA river flow info
|
||||
- `tide`: Returns NOAA tide information
|
||||
- `satellite`: Returns satellite pass info
|
||||
|
||||
### CheckList / Check In Out
|
||||
- **Asset Tracking**: Maintain a list of node/asset checkin and checkout. Useful foraccountability of people, assets. Radio-Net, FEMA, Trailhead.
|
||||
@@ -114,6 +122,7 @@ git clone https://github.com/spudgunman/meshing-around
|
||||
| `whoami` | Returns details of the node asking, also returned when position exchanged 📍 | ✅ |
|
||||
| `whois` | Returns details known about node, more data with bbsadmin node | ✅ |
|
||||
| `echo` | Echo string back, disabled by default | ✅ |
|
||||
| `bannode` | Admin option to prevent a node from using bot. `bannode list` will load and use the data/bbs_ban_list.txt db | ✅ |
|
||||
|
||||
### Radio Propagation & Weather Forecasting
|
||||
| Command | Description | |
|
||||
@@ -220,7 +229,7 @@ meshtastic --ble-scan
|
||||
# config.ini
|
||||
# type can be serial, tcp, or ble.
|
||||
# port is the serial port to use; commented out will try to auto-detect
|
||||
# hostname is the IP address of the device to connect to for TCP type
|
||||
# hostname is the IP/DNS and port for tcp type default is host:4403
|
||||
# mac is the MAC address of the device to connect to for BLE type
|
||||
|
||||
[interface]
|
||||
@@ -517,7 +526,7 @@ value = # value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
interval = # interval to use when time is not set (e.g. every 2 days)
|
||||
time = # time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
```
|
||||
The basic brodcast message can be setup in condig.ini. For advanced, See mesh_bot.py around the bottom of file, line [1491](https://github.com/SpudGunMan/meshing-around/blob/e94581936530c76ea43500eebb43f32ba7ed5e19/mesh_bot.py#L1491) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
|
||||
The basic brodcast message can be setup in condig.ini. For advanced, See the [modules/scheduler.py](modules/scheduler.py) to edit the schedule. See [schedule documentation](https://schedule.readthedocs.io/en/stable/) for more. Recomend to backup changes so they dont get lost.
|
||||
|
||||
```python
|
||||
#Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
@@ -528,7 +537,7 @@ schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now
|
||||
```
|
||||
|
||||
#### BBS Link
|
||||
The scheduler also handles the BBS Link Broadcast message, this would be an example of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initiator, one direction pull.
|
||||
The scheduler also handles the BBS Link Broadcast message, this would be an example of a mesh-admin channel on 8 being used to pass BBS post traffic between two bots as the initiator, one direction pull. The message just needs to have bbslink
|
||||
```python
|
||||
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 8 on device 1
|
||||
schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 8, 0, 1))
|
||||
@@ -586,7 +595,8 @@ I used ideas and snippets from other responder bots and want to call them out!
|
||||
- **mikecarper**: ideas, and testing. hamtest
|
||||
- **c.merphy360**: high altitude alerts
|
||||
- **Iris**: testing and finding 🐞
|
||||
- **Cisien, bitflip, Woof, propstg, snydermesh, trs2982, FJRPilot, F0X, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **FJRPiolt**: testing bugs out!!
|
||||
- **Cisien, bitflip, Woof, propstg, snydermesh, trs2982, F0X, mesb1, and Hailo1999**: For testing and feature ideas on Discord and GitHub.
|
||||
- **Meshtastic Discord Community**: For tossing out ideas and testing code.
|
||||
|
||||
### Tools
|
||||
|
||||
+8
-4
@@ -1,7 +1,7 @@
|
||||
#config.ini
|
||||
# type can be serial, tcp, or ble
|
||||
# port is the serial port to use, commented out will try to auto-detect
|
||||
# hostname is the IP address of the device to connect to for tcp type
|
||||
# hostname is the IP/DNS and port for tcp type default is host:4403
|
||||
# mac is the MAC address of the device to connect to for ble type
|
||||
|
||||
[interface]
|
||||
@@ -127,6 +127,7 @@ 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
|
||||
@@ -276,7 +277,7 @@ channel = 2
|
||||
message = "MeshBot says Hello! DM for more info."
|
||||
# enable overides the above and uses the motd as the message
|
||||
schedulerMotd = False
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun
|
||||
# value can be min,hour,day,mon,tue,wed,thu,fri,sat,sun. or custom for module/scheduler.py
|
||||
value =
|
||||
# interval to use when time is not set (e.g. every 2 days)
|
||||
interval =
|
||||
@@ -307,6 +308,7 @@ voxLanguage = en-us
|
||||
voxInputDevice = default
|
||||
voxOnTrapList = True
|
||||
voxTrapList = chirpy
|
||||
voxEnableCmd = True
|
||||
|
||||
|
||||
[fileMon]
|
||||
@@ -376,8 +378,10 @@ tictactoe = True
|
||||
# enable or disable the quiz game module questions are in data/quiz.json
|
||||
quiz = False
|
||||
|
||||
# enable or disable the survey game module questions are in data/survey/survey.json
|
||||
# enable or disable the survey game module questions are in data/survey/*_survey.json
|
||||
survey = False
|
||||
# this is the default survey to use when command givcen, from data/survey/example_survey.json
|
||||
defaultSurvey = example
|
||||
# Whether to record user ID in responses
|
||||
surveyRecordID=True
|
||||
# Whether to record location on start of survey
|
||||
@@ -392,7 +396,7 @@ splitDelay = 2.5
|
||||
MESSAGE_CHUNK_SIZE = 160
|
||||
# Request Acknowledgement of message OTA
|
||||
wantAck = False
|
||||
# Max limit buffer for radio testing
|
||||
# Max limit buffer for radio testing in bytes
|
||||
maxBuffer = 200
|
||||
#Enable Extra logging of Hop count data
|
||||
enableHopLogs = False
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// 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);
|
||||
}
|
||||
+1
-1
@@ -287,7 +287,7 @@ if [[ $(echo "${embedded}" | grep -i "^n") ]]; then
|
||||
|
||||
# document the service install
|
||||
printf "To install the %s service and keep notes, reference following commands:\n\n" "$service" > install_notes.txt
|
||||
printf "sudo cp %s/etc/%s.service /etc/systemd/system/etc/%s.service\n" "$program_path" "$service" "$service" >> install_notes.txt
|
||||
printf "sudo cp %s/etc/%s.service /etc/systemd/system/%s.service\n" "$program_path" "$service" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl daemon-reload\n" >> install_notes.txt
|
||||
printf "sudo systemctl enable %s.service\n" "$service" >> install_notes.txt
|
||||
printf "sudo systemctl start %s.service\n" "$service" >> install_notes.txt
|
||||
|
||||
+291
-290
@@ -81,7 +81,7 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
"readrss": lambda: get_rss_feed(message),
|
||||
"riverflow": lambda: handle_riverFlow(message, message_from_id, deviceID),
|
||||
"rlist": lambda: handle_repeaterQuery(message_from_id, deviceID, channel_number),
|
||||
"satpass": lambda: handle_satpass(message_from_id, deviceID, channel_number, message),
|
||||
"satpass": lambda: handle_satpass(message_from_id, deviceID, message),
|
||||
"setemail": lambda: handle_email(message_from_id, message),
|
||||
"setsms": lambda: handle_sms( message_from_id, message),
|
||||
"sitrep": lambda: handle_lheard(message, message_from_id, deviceID, isDM),
|
||||
@@ -144,16 +144,17 @@ def auto_response(message, snr, rssi, hop, pkiStatus, message_from_id, channel_n
|
||||
cmds = sorted(cmds, key=lambda k: k['index'])
|
||||
|
||||
# Check if user is already playing a game
|
||||
playing, game = isPlayingGame(message_from_id)
|
||||
playing, game = isPlayingGame(message_from_id)[0], isPlayingGame(message_from_id)[1]
|
||||
|
||||
# Block restricted commands if not DM, or if already playing a game
|
||||
if (cmds[0]['cmd'] in restrictedCommands and not isDM) or (cmds[0]['cmd'] in restrictedCommands and playing):
|
||||
# Block restricted commands if not DM
|
||||
if (cmds[0]['cmd'] in restrictedCommands and not isDM) or (cmds[0]['cmd'] in restrictedCommands and playing) or playing:
|
||||
logger.debug(f"System: Bot restricted Command:{cmds[0]['cmd']} From: {get_name_from_number(message_from_id)} isDM:{isDM} playing:{playing}")
|
||||
if playing:
|
||||
bot_response = f"🤖You are already playing {game}, finish that first."
|
||||
else:
|
||||
bot_response = restrictedResponse
|
||||
else:
|
||||
logger.debug(f"System: Bot detected Commands:{cmds} From: {get_name_from_number(message_from_id)}")
|
||||
logger.debug(f"System: Bot detected Commands:{cmds} From: {get_name_from_number(message_from_id)} isDM:{isDM} playing:{playing}")
|
||||
# run the first command after sorting
|
||||
bot_response = command_handler[cmds[0]['cmd']]()
|
||||
# append the command to the cmdHistory list for lheard and history
|
||||
@@ -168,6 +169,59 @@ def handle_cmd(message, message_from_id, deviceID):
|
||||
if " " in message and message.split(" ")[1] in trap_list:
|
||||
return "🤖 just use the commands directly in chat"
|
||||
return help_message
|
||||
|
||||
def isPlayingGame(message_from_id):
|
||||
global gameTrackers
|
||||
trackers = gameTrackers.copy()
|
||||
playingGame = False
|
||||
game = "None"
|
||||
|
||||
trackers = [tracker for tracker in trackers if tracker is not None]
|
||||
|
||||
for tracker, game_name, handle_game_func in trackers:
|
||||
for i in range(len(tracker)-1, -1, -1): # iterate backwards for safe removal
|
||||
id_key = 'userID' if game_name == "DopeWars" else 'nodeID'
|
||||
id_key = 'id' if game_name == "Survey" else id_key
|
||||
if tracker[i].get(id_key) == message_from_id:
|
||||
last_played_key = 'last_played' if 'last_played' in tracker[i] else 'time'
|
||||
if tracker[i].get(last_played_key, 0) > (time.time() - GAMEDELAY):
|
||||
playingGame = True
|
||||
game = game_name
|
||||
break
|
||||
if playingGame:
|
||||
break
|
||||
|
||||
return playingGame, game
|
||||
|
||||
def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
|
||||
global gameTrackers
|
||||
trackers = gameTrackers.copy()
|
||||
playingGame = False
|
||||
game = "None"
|
||||
|
||||
trackers = [tracker for tracker in trackers if tracker is not None]
|
||||
|
||||
for tracker, game_name, handle_game_func in trackers:
|
||||
playingGame, game = check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func)
|
||||
if playingGame:
|
||||
break
|
||||
return playingGame
|
||||
|
||||
def check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func):
|
||||
global llm_enabled
|
||||
|
||||
for i in range(len(tracker)):
|
||||
# Use 'userID' for DopeWars, 'nodeID' for others (including Survey)
|
||||
id_key = 'userID' if game_name == "DopeWars" else 'nodeID'
|
||||
|
||||
if tracker[i].get(id_key) == message_from_id:
|
||||
last_played_key = 'last_played' if 'last_played' in tracker[i] else 'time'
|
||||
if tracker[i].get(last_played_key) > (time.time() - GAMEDELAY):
|
||||
if llm_enabled:
|
||||
logger.debug(f"System: LLM Disabled for {message_from_id} for duration of {game_name}")
|
||||
send_message(handle_game_func(message_string, message_from_id, rxNode), channel_number, message_from_id, rxNode)
|
||||
return True, game_name
|
||||
return False, "None"
|
||||
|
||||
def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, channel_number):
|
||||
global multiPing
|
||||
@@ -240,6 +294,7 @@ def handle_ping(message_from_id, deviceID, message, hop, snr, rssi, isDM, chann
|
||||
|
||||
if pingCount > 1:
|
||||
multiPingList.append({'message_from_id': message_from_id, 'count': pingCount + 1, 'type': type, 'deviceID': deviceID, 'channel_number': channel_number, 'startCount': pingCount})
|
||||
logger.info(f"System: Starting auto-ping of type {type} for {pingCount} pings to {get_name_from_number(message_from_id, 'short', deviceID)}")
|
||||
if type == "🎙TEST":
|
||||
msg = f"🛜Initalizing BufferTest, using chunks of about {int(maxBuffer // pingCount)}, max length {maxBuffer} in {pingCount} messages"
|
||||
else:
|
||||
@@ -271,7 +326,6 @@ def handle_emergency(message_from_id, deviceID, message):
|
||||
nodeInfo = f"{get_name_from_number(message_from_id, 'short', deviceID)} detected by {get_name_from_number(myNodeNum, 'short', deviceID)} lastGPS {nodeLocation[0]}, {nodeLocation[1]}"
|
||||
msg = f"🔔🚨Intercepted Possible Emergency Assistance needed for: {nodeInfo}"
|
||||
# alert the emergency_responder_alert_channel
|
||||
time.sleep(responseDelay)
|
||||
send_message(msg, emergency_responder_alert_channel, 0, emergency_responder_alert_interface)
|
||||
logger.warning(f"System: {message_from_id} Emergency Assistance Requested in {message}")
|
||||
# send the message out via email/sms
|
||||
@@ -282,40 +336,39 @@ def handle_emergency(message_from_id, deviceID, message):
|
||||
|
||||
def handle_motd(message, message_from_id, isDM):
|
||||
global MOTD
|
||||
isAdmin = False
|
||||
msg = ""
|
||||
# check if the message_from_id is in the bbs_admin_list
|
||||
if bbs_admin_list != ['']:
|
||||
for admin in bbs_admin_list:
|
||||
if str(message_from_id) == admin:
|
||||
isAdmin = True
|
||||
break
|
||||
else:
|
||||
isAdmin = True
|
||||
|
||||
# admin help via DM
|
||||
if "?" in message and isDM and isAdmin:
|
||||
msg = ''
|
||||
isAdmin = isNodeAdmin(message_from_id)
|
||||
if "?" in message:
|
||||
msg = "Message of the day, set with 'motd $ HelloWorld!'"
|
||||
elif "?" in message and isDM and not isAdmin:
|
||||
# non-admin help via DM
|
||||
msg = "Message of the day"
|
||||
elif "$" in message and isAdmin:
|
||||
motd = message.split("$")[1]
|
||||
MOTD = motd.rstrip()
|
||||
logger.debug(f"System: {message_from_id} changed MOTD: {MOTD}")
|
||||
logger.debug(f"System: {message_from_id} temporarly changed MOTD: {MOTD}")
|
||||
msg = "MOTD changed to: " + MOTD
|
||||
else:
|
||||
msg = "MOTD: " + MOTD
|
||||
return msg
|
||||
|
||||
def handle_echo(message, message_from_id, deviceID, isDM, channel_number):
|
||||
|
||||
echoBinary = False
|
||||
if echoBinary:
|
||||
try:
|
||||
#send_raw_bytes echo the data to the channel with synch word:
|
||||
port_num = 256
|
||||
synch_word = b"echo:"
|
||||
message = message.split("echo ")[1]
|
||||
raw_bytes = synch_word + message.encode('utf-8')
|
||||
send_raw_bytes(message_from_id, raw_bytes, nodeInt=deviceID, channel=channel_number, portnum=port_num)
|
||||
except Exception as e:
|
||||
logger.error(f"System: Echo Exception {e}")
|
||||
return f"Sent binary echo message to {message_from_id} to {port_num} on channel {channel_number} device {deviceID}"
|
||||
|
||||
if "?" in message.lower():
|
||||
return "command returns your message back to you. Example:echo Hello World"
|
||||
elif "echo " in message.lower():
|
||||
parts = message.lower().split("echo ", 1)
|
||||
if len(parts) > 1 and parts[1].strip() != "":
|
||||
echo_msg = parts[1]
|
||||
if channel_number != echoChannel:
|
||||
if channel_number != echoChannel and not isDM:
|
||||
echo_msg = "@" + get_name_from_number(message_from_id, 'short', deviceID) + " " + echo_msg
|
||||
return echo_msg
|
||||
else:
|
||||
@@ -389,8 +442,8 @@ def handle_howtall(message, message_from_id, deviceID, isDM):
|
||||
lat = location[0]
|
||||
lon = location[1]
|
||||
if lat == latitudeValue and lon == longitudeValue:
|
||||
logger.debug(f"System: HowTall: No GPS location for {message_from_id}")
|
||||
return "No GPS location available"
|
||||
# add guessing tot he msg
|
||||
msg += "Guessing:"
|
||||
if use_metric:
|
||||
measure = "meters"
|
||||
else:
|
||||
@@ -405,7 +458,7 @@ def handle_howtall(message, message_from_id, deviceID, isDM):
|
||||
return f"Please provide a shadow length in {measure} example: howtall 5.5"
|
||||
|
||||
# get data
|
||||
msg = measureHeight(lat, lon, shadow_length)
|
||||
msg += measureHeight(lat, lon, shadow_length)
|
||||
|
||||
# if data has NO_ALERTS return help
|
||||
if NO_ALERTS in msg:
|
||||
@@ -436,8 +489,12 @@ llmRunCounter = 0
|
||||
llmTotalRuntime = []
|
||||
llmLocationTable = [{'nodeID': 1234567890, 'location': 'No Location'},]
|
||||
|
||||
def handle_satpass(message_from_id, deviceID, channel_number, message):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
def handle_satpass(message_from_id, deviceID, message='', vox=False):
|
||||
if vox:
|
||||
location = (latitudeValue, longitudeValue)
|
||||
message = 'satpass'
|
||||
else:
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
passes = ''
|
||||
satList = satListConfig
|
||||
message = message.lower()
|
||||
@@ -498,11 +555,9 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
|
||||
if (channel_number == publicChannel and antiSpam) or useDMForResponse:
|
||||
# send via DM
|
||||
send_message(welcome_message, channel_number, message_from_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# send via channel
|
||||
send_message(welcome_message, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
# mark the node as welcomed
|
||||
for node in seenNodes:
|
||||
if node['nodeID'] == message_from_id:
|
||||
@@ -536,7 +591,6 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
|
||||
else:
|
||||
# send via channel
|
||||
send_message(msg, channel_number, 0, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
|
||||
start = time.time()
|
||||
|
||||
@@ -549,8 +603,10 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
|
||||
llmTotalRuntime.append(end - start)
|
||||
|
||||
return response
|
||||
|
||||
def handleDopeWars(message, nodeID, rxNode):
|
||||
global dwPlayerTracker, dwHighScore
|
||||
from modules.settings import dwPlayerTracker
|
||||
global dwHighScore
|
||||
|
||||
# Find player in tracker
|
||||
player = next((p for p in dwPlayerTracker if p.get('userID') == nodeID), None)
|
||||
@@ -602,7 +658,8 @@ def handle_gTnW(chess = False):
|
||||
return response[selected_index]
|
||||
|
||||
def handleLemonade(message, nodeID, deviceID):
|
||||
global lemonadeTracker, lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore, lemon_starting_cash, lemon_total_weeks
|
||||
from modules.settings import lemonadeTracker
|
||||
global lemonadeCups, lemonadeLemons, lemonadeSugar, lemonadeWeeks, lemonadeScore, lemon_starting_cash, lemon_total_weeks
|
||||
msg = ""
|
||||
|
||||
def create_player(nodeID):
|
||||
@@ -649,7 +706,7 @@ def handleLemonade(message, nodeID, deviceID):
|
||||
return msg
|
||||
|
||||
def handleBlackJack(message, nodeID, deviceID):
|
||||
global jackTracker
|
||||
from modules.settings import jackTracker
|
||||
msg = ""
|
||||
|
||||
# Find player in tracker
|
||||
@@ -664,9 +721,24 @@ def handleBlackJack(message, nodeID, deviceID):
|
||||
|
||||
# Create new player if not found
|
||||
if not player and nodeID != 0:
|
||||
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'last_played': time.time()})
|
||||
msg += "Welcome to 🃏BlackJack!🃏\n"
|
||||
logger.debug(f"System: BlackJack: New Player {nodeID}")
|
||||
# create new player
|
||||
jackTracker.append({
|
||||
'nodeID': nodeID,
|
||||
'bet': 0,
|
||||
'cash': 100, # starting cash
|
||||
'gameStats': {'p_win': 0, 'd_win': 0, 'draw': 0},
|
||||
'p_cards': [],
|
||||
'd_cards': [],
|
||||
'p_hand': [],
|
||||
'd_hand': [],
|
||||
'next_card': [],
|
||||
'last_played': time.time(),
|
||||
'cmd': 'new'
|
||||
})
|
||||
msg += f"Welcome to 🃏BlackJack🃏!\n (H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
|
||||
# Show high score if available
|
||||
highScore = 0
|
||||
highScore = loadHSJack()
|
||||
if highScore and highScore.get('nodeID', 0) != 0:
|
||||
nodeName = get_name_from_number(highScore['nodeID'])
|
||||
@@ -679,12 +751,18 @@ def handleBlackJack(message, nodeID, deviceID):
|
||||
if player:
|
||||
player['last_played'] = time.time()
|
||||
|
||||
# get player's last command from tracker if not new player
|
||||
last_cmd = ""
|
||||
for i in range(len(jackTracker)):
|
||||
if jackTracker[i]['nodeID'] == nodeID:
|
||||
last_cmd = jackTracker[i]['cmd']
|
||||
|
||||
# Play BlackJack
|
||||
msg += playBlackJack(nodeID=nodeID, message=message)
|
||||
msg += playBlackJack(nodeID=nodeID, message=message, last_cmd=last_cmd)
|
||||
return msg
|
||||
|
||||
def handleVideoPoker(message, nodeID, deviceID):
|
||||
global vpTracker
|
||||
from modules.settings import vpTracker
|
||||
msg = ""
|
||||
|
||||
# Find player in tracker
|
||||
@@ -719,7 +797,7 @@ def handleVideoPoker(message, nodeID, deviceID):
|
||||
return msg
|
||||
|
||||
def handleMmind(message, nodeID, deviceID):
|
||||
global mindTracker
|
||||
from modules.settings import mindTracker
|
||||
msg = ''
|
||||
|
||||
if "end" in message.lower() or message.lower().startswith("e"):
|
||||
@@ -763,11 +841,30 @@ def handleMmind(message, nodeID, deviceID):
|
||||
return msg
|
||||
|
||||
def handleGolf(message, nodeID, deviceID):
|
||||
global golfTracker
|
||||
from modules.settings import golfTracker
|
||||
msg = ''
|
||||
|
||||
# get player's last command from tracker if not new player
|
||||
last_cmd = ""
|
||||
|
||||
# Ensure player exists in tracker
|
||||
if not any(entry['nodeID'] == nodeID for entry in golfTracker):
|
||||
logger.debug("System: GolfSim: New Player: " + str(nodeID))
|
||||
golfTracker.append({
|
||||
'nodeID': nodeID,
|
||||
'last_played': time.time(),
|
||||
'cmd': 'new',
|
||||
'hole': 1,
|
||||
'distance_remaining': 0,
|
||||
'hole_shots': 0,
|
||||
'hole_strokes': 0,
|
||||
'hole_to_par': 0,
|
||||
'total_strokes': 0,
|
||||
'total_to_par': 0,
|
||||
'par': 0,
|
||||
'hazard': ''
|
||||
})
|
||||
# get player's last command from tracker
|
||||
for i in range(len(golfTracker)):
|
||||
if golfTracker[i]['nodeID'] == nodeID:
|
||||
last_cmd = golfTracker[i]['cmd']
|
||||
@@ -782,18 +879,17 @@ def handleGolf(message, nodeID, deviceID):
|
||||
|
||||
logger.debug(f"System: {nodeID} PlayingGame golfsim last_cmd: {last_cmd}")
|
||||
|
||||
if last_cmd == "" and nodeID != 0:
|
||||
if last_cmd == "new" and nodeID != 0:
|
||||
# create new player
|
||||
logger.debug("System: GolfSim: New Player: " + str(nodeID))
|
||||
golfTracker.append({'nodeID': nodeID, 'last_played': time.time(), 'cmd': 'new', 'hole': 1, 'distance_remaining': 0, 'hole_shots': 0, 'hole_strokes': 0, 'hole_to_par': 0, 'total_strokes': 0, 'total_to_par': 0, 'par': 0, 'hazard': ''})
|
||||
|
||||
msg = f"Welcome to 🏌️GolfSim⛳️\n"
|
||||
msg += f"Clubs: (D)river, (L)ow Iron, (M)id Iron, (H)igh Iron, (G)ap Wedge, Lob (W)edge\n"
|
||||
msg += f"Clubs: (D)river, (L)ow Iron, (M)id Iron, (H)igh Iron, (G)ap Wedge, Lob (W)edge (C)addie\n"
|
||||
|
||||
msg += playGolf(nodeID=nodeID, message=message)
|
||||
msg += playGolf(nodeID=nodeID, message=message, last_cmd=last_cmd)
|
||||
return msg
|
||||
|
||||
def handleHangman(message, nodeID, deviceID):
|
||||
global hangmanTracker
|
||||
from modules.settings import hangmanTracker
|
||||
index = 0
|
||||
msg = ''
|
||||
for i in range(len(hangmanTracker)):
|
||||
@@ -819,7 +915,7 @@ def handleHangman(message, nodeID, deviceID):
|
||||
return msg
|
||||
|
||||
def handleHamtest(message, nodeID, deviceID):
|
||||
global hamtestTracker
|
||||
from modules.settings import hamtestTracker
|
||||
index = 0
|
||||
msg = ''
|
||||
response = message.split(' ')
|
||||
@@ -852,7 +948,7 @@ def handleHamtest(message, nodeID, deviceID):
|
||||
return msg
|
||||
|
||||
def handleTicTacToe(message, nodeID, deviceID):
|
||||
global tictactoeTracker
|
||||
from modules.settings import tictactoeTracker
|
||||
index = 0
|
||||
msg = ''
|
||||
|
||||
@@ -933,7 +1029,6 @@ def quizHandler(message, nodeID, deviceID):
|
||||
if isinstance(msg, dict) and str(nodeID) in bbs_admin_list and 'message' in msg:
|
||||
for player_id in quizGamePlayer.players:
|
||||
send_message(msg['message'], 0, player_id, deviceID)
|
||||
time.sleep(responseDelay)
|
||||
msg = f"Message sent to {len(quizGamePlayer.players)} players"
|
||||
|
||||
return msg
|
||||
@@ -941,7 +1036,8 @@ def quizHandler(message, nodeID, deviceID):
|
||||
return "🧠Please provide an answer or command, or send q: ?"
|
||||
|
||||
def surveyHandler(message, nodeID, deviceID):
|
||||
global surveyTracker
|
||||
from modules.settings import surveyTracker
|
||||
user_id = nodeID
|
||||
location = get_node_location(nodeID, deviceID)
|
||||
msg = ''
|
||||
# Normalize and parse the command
|
||||
@@ -983,8 +1079,13 @@ def surveyHandler(message, nodeID, deviceID):
|
||||
|
||||
return msg
|
||||
|
||||
def handle_riverFlow(message, message_from_id, deviceID):
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
def handle_riverFlow(message, message_from_id, deviceID, vox=False):
|
||||
# River Flow from NOAA or Open-Meteo
|
||||
if vox:
|
||||
location = (latitudeValue, longitudeValue)
|
||||
message = "riverflow"
|
||||
else:
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
msg_lower = message.lower()
|
||||
if "riverflow " in msg_lower:
|
||||
user_input = msg_lower.split("riverflow ", 1)[1].strip()
|
||||
@@ -1010,7 +1111,14 @@ def handle_mwx(message_from_id, deviceID, cmd):
|
||||
return NO_ALERTS
|
||||
return get_nws_marine(zone=myCoastalZone, days=coastalForecastDays)
|
||||
|
||||
def handle_wxc(message_from_id, deviceID, cmd):
|
||||
def handle_wxc(message_from_id, deviceID, cmd, vox=False):
|
||||
# Weather from NOAA or Open-Meteo
|
||||
if vox:
|
||||
# return a default message if vox is enabled
|
||||
if use_meteo_wxApi:
|
||||
return get_wx_meteo(latitudeValue, longitudeValue)
|
||||
else:
|
||||
return get_NOAAweather(latitudeValue, longitudeValue)
|
||||
location = get_node_location(message_from_id, deviceID)
|
||||
if use_meteo_wxApi and not "wxc" in cmd and not use_metric:
|
||||
#logger.debug("System: Bot Returning Open-Meteo API for weather imperial")
|
||||
@@ -1100,49 +1208,51 @@ def handle_messages(message, deviceID, channel_number, msg_history, publicChanne
|
||||
if "?" in message and isDM:
|
||||
return message.split("?")[0].title() + " command returns the last " + str(storeFlimit) + " messages sent on a channel."
|
||||
else:
|
||||
response = ""
|
||||
header = f"📨Messages:\n"
|
||||
# Calculate safe byte limit (account for header and some overhead)
|
||||
header_bytes = len(header.encode('utf-8'))
|
||||
available_bytes = max_bytes - header_bytes
|
||||
# Filter messages for this device/channel
|
||||
filtered_msgs = [
|
||||
msgH for msgH in msg_history
|
||||
if msgH[4] == deviceID and (msgH[2] == channel_number or msgH[2] == publicChannel)
|
||||
]
|
||||
|
||||
# Reverse the message history to show most recent first
|
||||
for msgH in reversed(msg_history):
|
||||
# number of messages to return +1 for the header line
|
||||
if len(response.split("\n")) >= storeFlimit + 1:
|
||||
break
|
||||
# if the message is for this deviceID and channel or publicChannel
|
||||
if msgH[4] == deviceID:
|
||||
if msgH[2] == channel_number or msgH[2] == publicChannel:
|
||||
new_line = f"\n{msgH[0]}: {msgH[1]}"
|
||||
# Check if adding this line would exceed byte limit
|
||||
test_response = response + new_line
|
||||
if len(test_response.encode('utf-8')) > available_bytes:
|
||||
# Try to add truncated version of the message
|
||||
msg_text = msgH[1]
|
||||
truncated = False
|
||||
while len(msg_text) > 0 and len((response + f"\n{msgH[0]}: {msg_text}").encode('utf-8')) > available_bytes:
|
||||
# Remove one character at a time from the end
|
||||
msg_text = msg_text[:-1]
|
||||
truncated = True
|
||||
if len(msg_text) > 10: # Only add if we have at least 10 chars left
|
||||
response += f"\n{msgH[0]}: {msg_text}" + ("..." if truncated else "")
|
||||
break # Stop adding more messages
|
||||
else:
|
||||
response += new_line
|
||||
|
||||
# Choose order and slice
|
||||
# Oldest first, take first N
|
||||
filtered_msgs = filtered_msgs[-storeFlimit:][::-1]
|
||||
if reverseSF:
|
||||
# segassem reverse the order of the messages
|
||||
response_lines = response.split("\n")
|
||||
response_lines.reverse()
|
||||
response = "\n".join(response_lines)
|
||||
|
||||
# reverse that
|
||||
filtered_msgs = filtered_msgs[::-1]
|
||||
|
||||
response = ""
|
||||
header = f"📨Msgs:\n"
|
||||
for msgH in filtered_msgs:
|
||||
new_line = f"\n{msgH[0]}: {msgH[1]}"
|
||||
test_response = response + new_line
|
||||
if len(test_response.encode('utf-8')) > maxBuffer:
|
||||
# Truncate message if needed
|
||||
msg_text = msgH[1]
|
||||
truncated = False
|
||||
trunc_marker = "..."
|
||||
while len(msg_text) > 0 and len((response + f"\n{msgH[0]}: {msg_text}{trunc_marker}").encode('utf-8')) > maxBuffer:
|
||||
msg_text = msg_text[:-1]
|
||||
truncated = True
|
||||
if len(msg_text) > 10:
|
||||
if truncated:
|
||||
response += f"\n{msgH[0]}: {msg_text}{trunc_marker}"
|
||||
else:
|
||||
response += f"\n{msgH[0]}: {msg_text}"
|
||||
break
|
||||
continue
|
||||
else:
|
||||
response += new_line
|
||||
|
||||
if len(response) > 0:
|
||||
return header + response
|
||||
else:
|
||||
return "No 📭messages in history"
|
||||
|
||||
def handle_sun(message_from_id, deviceID, channel_number):
|
||||
def handle_sun(message_from_id, deviceID, channel_number, vox=False):
|
||||
if vox:
|
||||
# return a default message if vox is enabled
|
||||
return get_sun(str(latitudeValue), str(longitudeValue))
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_sun(str(location[0]), str(location[1]))
|
||||
|
||||
@@ -1153,7 +1263,7 @@ def sysinfo(message, message_from_id, deviceID):
|
||||
if enable_runShellCmd and file_monitor_enabled:
|
||||
# get the system information from the shell script
|
||||
# this is an example of how to run a shell script and return the data
|
||||
shellData = call_external_script(None, "script/sysEnv.sh")
|
||||
shellData = call_external_script('', "script/sysEnv.sh")
|
||||
# check if the script returned data
|
||||
if shellData == "" or shellData == None:
|
||||
# no data returned from the script
|
||||
@@ -1250,11 +1360,15 @@ def handle_repeaterQuery(message_from_id, deviceID, channel_number):
|
||||
else:
|
||||
return "Repeater lookup not enabled"
|
||||
|
||||
def handle_tide(message_from_id, deviceID, channel_number):
|
||||
def handle_tide(message_from_id, deviceID, channel_number, vox=False):
|
||||
if vox:
|
||||
return get_NOAAtide(str(latitudeValue), str(longitudeValue))
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_NOAAtide(str(location[0]), str(location[1]))
|
||||
|
||||
def handle_moon(message_from_id, deviceID, channel_number):
|
||||
def handle_moon(message_from_id, deviceID, channel_number, vox=False):
|
||||
if vox:
|
||||
return get_moon(str(latitudeValue), str(longitudeValue))
|
||||
location = get_node_location(message_from_id, deviceID, channel_number)
|
||||
return get_moon(str(location[0]), str(location[1]))
|
||||
|
||||
@@ -1322,74 +1436,6 @@ def handle_whois(message, deviceID, channel_number, message_from_id):
|
||||
msg += f"Loc: {where_am_i(str(location[0]), str(location[1]))}"
|
||||
return msg
|
||||
|
||||
def check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func):
|
||||
global llm_enabled
|
||||
|
||||
for i in range(len(tracker)):
|
||||
# Use 'userID'
|
||||
id_key = 'userID' if game_name == "DopeWars" else 'nodeID' # DopeWars uses 'userID'
|
||||
id_key = 'id' if game_name == "Survey" else id_key # Survey uses 'id'
|
||||
|
||||
if tracker[i].get(id_key) == message_from_id:
|
||||
last_played_key = 'last_played' if 'last_played' in tracker[i] else 'time'
|
||||
if tracker[i].get(last_played_key) > (time.time() - GAMEDELAY):
|
||||
if llm_enabled:
|
||||
logger.debug(f"System: LLM Disabled for {message_from_id} for duration of {game_name}")
|
||||
send_message(handle_game_func(message_string, message_from_id, rxNode), channel_number, message_from_id, rxNode)
|
||||
return True, game_name
|
||||
return False, "None"
|
||||
|
||||
gameTrackers = [
|
||||
(dwPlayerTracker, "DopeWars", handleDopeWars) if 'dwPlayerTracker' in globals() else None,
|
||||
(lemonadeTracker, "LemonadeStand", handleLemonade) if 'lemonadeTracker' in globals() else None,
|
||||
(vpTracker, "VideoPoker", handleVideoPoker) if 'vpTracker' in globals() else None,
|
||||
(jackTracker, "BlackJack", handleBlackJack) if 'jackTracker' in globals() else None,
|
||||
(mindTracker, "MasterMind", handleMmind) if 'mindTracker' in globals() else None,
|
||||
(golfTracker, "GolfSim", handleGolf) if 'golfTracker' in globals() else None,
|
||||
(hangmanTracker, "Hangman", handleHangman) if 'hangmanTracker' in globals() else None,
|
||||
(hamtestTracker, "HamTest", handleHamtest) if 'hamtestTracker' in globals() else None,
|
||||
(tictactoeTracker, "TicTacToe", handleTicTacToe) if 'tictactoeTracker' in globals() else None,
|
||||
(surveyTracker, "Survey", surveyHandler) if 'surveyTracker' in globals() else None,
|
||||
#quiz does not use a tracker (quizGamePlayer) always active
|
||||
]
|
||||
|
||||
def isPlayingGame(message_from_id):
|
||||
global gameTrackers
|
||||
trackers = gameTrackers.copy()
|
||||
playingGame = False
|
||||
game = "None"
|
||||
|
||||
trackers = [tracker for tracker in trackers if tracker is not None]
|
||||
|
||||
for tracker, game_name, handle_game_func in trackers:
|
||||
for i in range(len(tracker)-1, -1, -1): # iterate backwards for safe removal
|
||||
id_key = 'userID' if game_name == "DopeWars" else 'nodeID'
|
||||
id_key = 'id' if game_name == "Survey" else id_key
|
||||
if tracker[i].get(id_key) == message_from_id:
|
||||
last_played_key = 'last_played' if 'last_played' in tracker[i] else 'time'
|
||||
if tracker[i].get(last_played_key, 0) > (time.time() - GAMEDELAY):
|
||||
playingGame = True
|
||||
game = game_name
|
||||
break
|
||||
if playingGame:
|
||||
break
|
||||
|
||||
return playingGame, game
|
||||
|
||||
def checkPlayingGame(message_from_id, message_string, rxNode, channel_number):
|
||||
global gameTrackers
|
||||
trackers = gameTrackers.copy()
|
||||
playingGame = False
|
||||
game = "None"
|
||||
|
||||
trackers = [tracker for tracker in trackers if tracker is not None]
|
||||
|
||||
for tracker, game_name, handle_game_func in trackers:
|
||||
playingGame, game = check_and_play_game(tracker, message_from_id, message_string, rxNode, channel_number, game_name, handle_game_func)
|
||||
if playingGame:
|
||||
break
|
||||
return playingGame
|
||||
|
||||
def onReceive(packet, interface):
|
||||
global seenNodes, msg_history, cmdHistory
|
||||
# Priocess the incoming packet, handles the responses to the packet with auto_response()
|
||||
@@ -1403,7 +1449,13 @@ def onReceive(packet, interface):
|
||||
pkiStatus = (False, 'ABC')
|
||||
replyIDset = False
|
||||
emojiSeen = False
|
||||
simulator_flag = False
|
||||
isDM = False
|
||||
channel_number = 0
|
||||
hop_away = 0
|
||||
hop_start = 0
|
||||
hop_count = 0
|
||||
channel_name = "unknown"
|
||||
playingGame = False
|
||||
|
||||
if DEBUGpacket:
|
||||
@@ -1450,7 +1502,23 @@ def onReceive(packet, interface):
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
channel_number = packet.get('channel')
|
||||
channel_name = "unknown"
|
||||
# get channel hashes for the interface
|
||||
device = next((d for d in channel_list if d["interface_id"] == rxNode), None)
|
||||
if device:
|
||||
# Find the channel name whose hash matches channel_number
|
||||
for chan_name, info in device['channels'].items():
|
||||
if info['hash'] == channel_number:
|
||||
print(f"Matched channel hash {info['hash']} to channel name {chan_name}")
|
||||
channel_name = chan_name
|
||||
break
|
||||
|
||||
# check if the packet has a simulator flag
|
||||
simulator_flag = packet.get('decoded', {}).get('simulator', False)
|
||||
if isinstance(simulator_flag, dict):
|
||||
# assume Software Simulator
|
||||
simulator_flag = True
|
||||
|
||||
# set the message_from_id
|
||||
message_from_id = packet['from']
|
||||
@@ -1464,8 +1532,6 @@ def onReceive(packet, interface):
|
||||
|
||||
msg = bbs_check_dm(message_from_id)
|
||||
if msg:
|
||||
# wait a responseDelay to avoid message collision from lora-ack.
|
||||
time.sleep(responseDelay)
|
||||
logger.info(f"System: BBS DM Delivery: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
message = "Mail: " + msg[1] + " From: " + get_name_from_number(msg[2], 'long', rxNode)
|
||||
bbs_delete_dm(msg[0], msg[1])
|
||||
@@ -1477,6 +1543,13 @@ def onReceive(packet, interface):
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
via_mqtt = packet['decoded'].get('viaMqtt', False)
|
||||
transport_mechanism = (
|
||||
packet.get('transport_mechanism')
|
||||
or packet.get('transportMechanism')
|
||||
or (packet.get('decoded', {}).get('transport_mechanism'))
|
||||
or (packet.get('decoded', {}).get('transportMechanism'))
|
||||
or 'unknown'
|
||||
)
|
||||
rx_time = packet['decoded'].get('rxTime', time.time())
|
||||
|
||||
# check if the packet is from us
|
||||
@@ -1503,40 +1576,36 @@ def onReceive(packet, interface):
|
||||
# check if the packet has a hop count flag use it
|
||||
if packet.get('hopsAway'):
|
||||
hop_away = packet.get('hopsAway', 0)
|
||||
|
||||
if packet.get('hopStart'):
|
||||
hop_start = packet.get('hopStart', 0)
|
||||
|
||||
if packet.get('hopLimit'):
|
||||
hop_limit = packet.get('hopLimit', 0)
|
||||
|
||||
# calculate hop count
|
||||
hop = ""
|
||||
if hop_limit > 0 and hop_start >= hop_limit:
|
||||
hop_count = hop_away + (hop_start - hop_limit)
|
||||
elif hop_limit > 0 and hop_start < hop_limit:
|
||||
hop_count = hop_away + (hop_limit - hop_start)
|
||||
else:
|
||||
# if the packet does not have a hop count try other methods
|
||||
if packet.get('hopLimit'):
|
||||
hop_limit = packet.get('hopLimit', 0)
|
||||
else:
|
||||
hop_limit = 0
|
||||
|
||||
if packet.get('hopStart'):
|
||||
hop_start = packet.get('hopStart', 0)
|
||||
else:
|
||||
hop_start = 0
|
||||
|
||||
if enableHopLogs:
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start}")
|
||||
|
||||
hop_count = hop_away
|
||||
|
||||
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
|
||||
hop = "Last Hop"
|
||||
hop_count = 0
|
||||
|
||||
if hop_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
elif hop_start == 0 and hop_limit > 0 or via_mqtt:
|
||||
hop = "MQTT"
|
||||
hop_count = 0
|
||||
else:
|
||||
# set hop to Direct if the message was sent directly otherwise set the hop count
|
||||
if hop_away > 0:
|
||||
hop_count = hop_away
|
||||
else:
|
||||
hop_count = hop_start - hop_limit
|
||||
#print (f"calculated hop count: {hop_start} - {hop_limit} = {hop_count}")
|
||||
|
||||
hop = f"{hop_count} hops"
|
||||
if hop_start == hop_limit and "lora" in str(transport_mechanism).lower():
|
||||
hop = "Direct"
|
||||
|
||||
if ((hop_start == 0 and hop_limit >= 0) or via_mqtt or ("mqtt" in str(transport_mechanism).lower())):
|
||||
hop = "MQTT"
|
||||
|
||||
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}")
|
||||
|
||||
# check with stringSafeChecker if the message is safe
|
||||
if stringSafeCheck(message_string) is False:
|
||||
@@ -1566,7 +1635,6 @@ def onReceive(packet, interface):
|
||||
if games_enabled:
|
||||
logger.warning(f"Device:{rxNode} Ignoring Request to Play Game: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)} with hop count: {hop}")
|
||||
send_message(f"Your hop count exceeds safe playable distance at {hop_count} hops", channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
playingGame = False
|
||||
else:
|
||||
@@ -1577,7 +1645,6 @@ def onReceive(packet, interface):
|
||||
# respond with LLM
|
||||
llm = handle_llm(message_from_id, channel_number, rxNode, message_string, publicChannel)
|
||||
send_message(llm, channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# respond with welcome message on DM
|
||||
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
@@ -1586,7 +1653,6 @@ def onReceive(packet, interface):
|
||||
if not any(node['nodeID'] == message_from_id and node['welcome'] == True for node in seenNodes):
|
||||
# send welcome message
|
||||
send_message(welcome_message, channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
# mark the node as welcomed
|
||||
for node in seenNodes:
|
||||
if node['nodeID'] == message_from_id:
|
||||
@@ -1598,9 +1664,7 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
# respond with help message on DM
|
||||
send_message(help_message, channel_number, message_from_id, rxNode)
|
||||
|
||||
time.sleep(responseDelay)
|
||||
|
||||
|
||||
# log the message to the message log
|
||||
if log_messages_to_file:
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_string.replace('\n', '-nl-'))
|
||||
@@ -1685,9 +1749,7 @@ def onReceive(packet, interface):
|
||||
hello(message_from_id, name)
|
||||
# send a hello message as a DM
|
||||
if not train_qrz:
|
||||
time.sleep(responseDelay)
|
||||
send_message(f"Hello {name} {qrz_hello_string}", channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode, channel_number)
|
||||
@@ -1750,7 +1812,7 @@ async def start_rx():
|
||||
|
||||
if wikipedia_enabled:
|
||||
if use_kiwix_server:
|
||||
logger.debug(f"System: Wikipedia search Enabled using Kiwix server at {kiwix_server_address}")
|
||||
logger.debug(f"System: Wikipedia search Enabled using Kiwix server at {kiwix_url}")
|
||||
else:
|
||||
logger.debug("System: Wikipedia search Enabled")
|
||||
|
||||
@@ -1761,13 +1823,13 @@ async def start_rx():
|
||||
logger.debug(f"System: MOTD Enabled using {MOTD} scheduler:{schedulerMotd}")
|
||||
|
||||
if sentry_enabled:
|
||||
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel}")
|
||||
logger.debug(f"System: Sentry Mode Enabled {sentry_radius}m radius reporting to channel:{secure_channel} requestLOC:{reqLocationEnabled}")
|
||||
|
||||
if highfly_enabled:
|
||||
logger.debug(f"System: HighFly Enabled using {highfly_altitude}m limit reporting to channel:{highfly_channel}")
|
||||
|
||||
if store_forward_enabled:
|
||||
logger.debug(f"System: S&F(messages command) Enabled using limit: {storeFlimit}")
|
||||
logger.debug(f"System: S&F(messages command) Enabled using limit: {storeFlimit} and reverse queue:{reverseSF}")
|
||||
|
||||
if enableEcho:
|
||||
logger.debug("System: Echo command Enabled")
|
||||
@@ -1829,95 +1891,34 @@ async def start_rx():
|
||||
logger.warning("System: SMTP Email Alerting Enabled")
|
||||
|
||||
if scheduler_enabled:
|
||||
# basic scheduler
|
||||
if schedulerMotd:
|
||||
schedulerMessage = MOTD
|
||||
if schedulerValue != '':
|
||||
if schedulerValue.lower() == 'day':
|
||||
if schedulerTime != '':
|
||||
# Send a message every day at the time set in schedulerTime
|
||||
schedule.every().day.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
else:
|
||||
# Send a message every day at the time set in schedulerInterval
|
||||
schedule.every(int(schedulerInterval)).days.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'mon' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Monday at the time set in schedulerTime
|
||||
schedule.every().monday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'tue' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Tuesday at the time set in schedulerTime
|
||||
schedule.every().tuesday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'wed' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Wednesday at the time set in schedulerTime
|
||||
schedule.every().wednesday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'thu' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Thursday at the time set in schedulerTime
|
||||
schedule.every().thursday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'fri' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Friday at the time set in schedulerTime
|
||||
schedule.every().friday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sat' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Saturday at the time set in schedulerTime
|
||||
schedule.every().saturday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sun' in schedulerValue.lower() and schedulerTime != '':
|
||||
# Send a message every Sunday at the time set in schedulerTime
|
||||
schedule.every().sunday.at(schedulerTime).do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'hour' in schedulerValue.lower():
|
||||
# Send a message every hour at the time set in schedulerTime
|
||||
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'min' in schedulerValue.lower():
|
||||
# Send a message every minute at the time set in schedulerTime
|
||||
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(schedulerMessage, schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the scheduler to send '{schedulerMessage}' every {schedulerValue} at {schedulerTime} on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
else:
|
||||
logger.warning("System: No schedule.Value set edit the .py file to do more. See examples in the code.")
|
||||
# Reminder Scheduler is enabled every Monday at noon send a log message
|
||||
schedule.every().monday.at("12:00").do(lambda: logger.info("System: Scheduled Broadcast Enabled Reminder"))
|
||||
# example scheduler message
|
||||
logger.debug(f"System: Starting the scheduler to send '{schedulerMessage}' every Monday at noon on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
|
||||
# Enhanced Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
|
||||
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
|
||||
|
||||
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
|
||||
|
||||
# Send Weather Channel Notice Wed. Noon on channel 2, device 1
|
||||
#schedule.every().wednesday.at("12:00").do(lambda: send_message("Weather alerts available on 'Alerts' channel with default 'AQ==' key.", 2, 0, 1))
|
||||
|
||||
# Send config URL for Medium Fast Network Use every other day at 10:00 to default channel 2 on device 1
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("Join us on Medium Fast https://meshtastic.org/e/#CgcSAQE6AggNEg4IARAEOAFAA0gBUB5oAQ", 2, 0, 1))
|
||||
|
||||
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
|
||||
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", 2, 0, 1)).day(15, 25)
|
||||
|
||||
# Send a joke every 6 hours using tell_joke function to channel 2 on device 1
|
||||
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), 2, 0, 1))
|
||||
|
||||
# Send a joke every 2 minutes using tell_joke function to channel 2 on device 1
|
||||
#schedule.every(2).minutes.do(lambda: send_message(tell_joke(), 2, 0, 1))
|
||||
|
||||
# Send the Welcome Message every other day at 08:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, 2, 0, 1))
|
||||
|
||||
# Send the MOTD every day at 13:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, 2, 0, 1))
|
||||
|
||||
# Send bbslink looking for peers every other day at 10:00 using send_message function to channel 3 on device 1
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", 3, 0, 1))
|
||||
# show schedual details
|
||||
await BroadcastScheduler()
|
||||
# setup the scheduler
|
||||
from modules.scheduler import setup_scheduler
|
||||
await setup_scheduler(
|
||||
schedulerMotd, MOTD, schedulerMessage, schedulerChannel, schedulerInterface,
|
||||
schedulerValue, schedulerTime, schedulerInterval, logger, BroadcastScheduler
|
||||
)
|
||||
|
||||
# here we go loopty loo
|
||||
while True:
|
||||
await asyncio.sleep(0.5)
|
||||
pass
|
||||
|
||||
|
||||
# Initialize game trackers
|
||||
gameTrackers = [
|
||||
(dwPlayerTracker, "DopeWars", handleDopeWars) if 'dwPlayerTracker' in globals() else None,
|
||||
(lemonadeTracker, "LemonadeStand", handleLemonade) if 'lemonadeTracker' in globals() else None,
|
||||
(vpTracker, "VideoPoker", handleVideoPoker) if 'vpTracker' in globals() else None,
|
||||
(jackTracker, "BlackJack", handleBlackJack) if 'jackTracker' in globals() else None,
|
||||
(mindTracker, "MasterMind", handleMmind) if 'mindTracker' in globals() else None,
|
||||
(golfTracker, "GolfSim", handleGolf) if 'golfTracker' in globals() else None,
|
||||
(hangmanTracker, "Hangman", handleHangman) if 'hangmanTracker' in globals() else None,
|
||||
(hamtestTracker, "HamTest", handleHamtest) if 'hamtestTracker' in globals() else None,
|
||||
(tictactoeTracker, "TicTacToe", handleTicTacToe) if 'tictactoeTracker' in globals() else None,
|
||||
(surveyTracker, "Survey", surveyHandler) if 'surveyTracker' in globals() else None,
|
||||
#quiz does not use a tracker (quizGamePlayer) always active
|
||||
]
|
||||
|
||||
# Hello World
|
||||
async def main():
|
||||
tasks = []
|
||||
|
||||
+40
-1
@@ -5,12 +5,19 @@ import pickle # pip install pickle
|
||||
from modules.log import *
|
||||
import time
|
||||
|
||||
useSynchCompression = False
|
||||
|
||||
if useSynchCompression:
|
||||
import zlib
|
||||
from modules.system import send_raw_bytes
|
||||
|
||||
trap_list_bbs = ("bbslist", "bbspost", "bbsread", "bbsdelete", "bbshelp", "bbsinfo", "bbslink", "bbsack")
|
||||
|
||||
# global message list, later we will use a pickle on disk
|
||||
bbs_messages = []
|
||||
bbs_dm = []
|
||||
|
||||
|
||||
def load_bbsdb():
|
||||
global bbs_messages
|
||||
# load the bbs messages from the database file
|
||||
@@ -201,6 +208,32 @@ def bbs_delete_dm(toNode, message):
|
||||
return "System: cleared mail for" + str(toNode)
|
||||
return "System: No DM found for node " + str(toNode)
|
||||
|
||||
def compress_data(data_to_compress):
|
||||
# Prepare message as bytes
|
||||
compressed = zlib.compress(data_to_compress.encode('utf-8'))
|
||||
return compressed
|
||||
|
||||
def decompress_data(data_bytes):
|
||||
try:
|
||||
decompressed = zlib.decompress(data_bytes)
|
||||
msg = decompressed.decode('utf-8')
|
||||
return msg
|
||||
except Exception as e:
|
||||
logger.warning(f"Error decompressing data: {e}")
|
||||
return False
|
||||
|
||||
def bbs_receive_compressed(data_bytes, fromNode, RxNode):
|
||||
try:
|
||||
decompressed = zlib.decompress(data_bytes)
|
||||
msg = decompressed.decode('utf-8')
|
||||
|
||||
bbs_sync_posts(msg, fromNode, RxNode)
|
||||
|
||||
return msg
|
||||
except Exception as e:
|
||||
logger.error(f"Error decompressing BBS message: {e}")
|
||||
return None
|
||||
|
||||
def bbs_sync_posts(input, peerNode, RxNode):
|
||||
messageID = 0
|
||||
|
||||
@@ -245,7 +278,13 @@ def bbs_sync_posts(input, peerNode, RxNode):
|
||||
if messageID % 5 == 0:
|
||||
time.sleep(10 + responseDelay)
|
||||
logger.debug(f"System: Sending bbslink message {messageID} of {len(bbs_messages)} to peer " + str(peerNode))
|
||||
return f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]} @{fromNodeHex}"
|
||||
msg = f"bbslink {messageID} ${bbs_messages[messageID][1]} #{bbs_messages[messageID][2]} @{fromNodeHex}"
|
||||
if useSynchCompression:
|
||||
compressed = compress_data(msg)
|
||||
send_raw_bytes(peerNode, compressed)
|
||||
logger.debug("System: Sent compressed bbslink message to peer " + str(peerNode))
|
||||
else:
|
||||
return msg
|
||||
else:
|
||||
logger.debug("System: bbslink sync complete with peer " + str(peerNode))
|
||||
|
||||
|
||||
+9
-3
@@ -72,7 +72,6 @@ async def watch_file():
|
||||
def call_external_script(message, script="script/runShell.sh"):
|
||||
# Call an external script with the message as an argument this is a example only
|
||||
try:
|
||||
# Debugging: Print the current working directory and resolved script path
|
||||
current_working_directory = os.getcwd()
|
||||
script_path = os.path.join(current_working_directory, script)
|
||||
|
||||
@@ -82,8 +81,15 @@ def call_external_script(message, script="script/runShell.sh"):
|
||||
if not os.path.exists(script_path):
|
||||
logger.warning(f"FileMon: Script not found: {script_path}")
|
||||
return "sorry I can't do that"
|
||||
|
||||
output = os.popen(f"bash {script_path} {message}").read().encode('utf-8').decode('utf-8')
|
||||
|
||||
# Use subprocess.run for better resource management
|
||||
result = subprocess.run(
|
||||
["bash", script_path, message],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
output = result.stdout.strip()
|
||||
return output
|
||||
except Exception as e:
|
||||
logger.warning(f"FileMon: Error calling external script: {e}")
|
||||
|
||||
+56
-40
@@ -7,8 +7,7 @@ import time
|
||||
import pickle
|
||||
|
||||
jack_starting_cash = 100 # Replace 100 with your desired starting cash value
|
||||
jackTracker= [{'nodeID': 0, 'cmd': 'new', 'cash': jack_starting_cash,\
|
||||
'bet': 0, 'gameStats': {'p_win': 0, 'd_win': 0, 'draw': 0}, 'p_cards':[], 'd_cards':[], 'p_hand':[], 'd_hand':[], 'next_card':[],'last_played': time.time()}]
|
||||
from modules.settings import jackTracker
|
||||
|
||||
SUITS = ("♥️", "♦️", "♠️", "♣️")
|
||||
RANKS = (
|
||||
@@ -114,22 +113,35 @@ class jackChips:
|
||||
self.total -= self.bet
|
||||
self.winnings -= 1
|
||||
|
||||
def success_rate(card, obj_h):
|
||||
""" Calculate Success rate of 'HIT' new cards """
|
||||
msg = ""
|
||||
rate = 0
|
||||
diff = 21 - obj_h.value
|
||||
if diff != 0:
|
||||
rate = (VALUES[card[0][1]] / diff) * 100
|
||||
def success_rate(next_card, player_hand):
|
||||
# Estimate the chance of a successful 'HIT' (not busting) in blackjack.
|
||||
|
||||
if rate < 100:
|
||||
msg += f"If Hit, chance {int(rate)}% failure, {100-int(rate)}% success."
|
||||
else:
|
||||
l_rate = int(rate - (rate - 99)) # Round to 99
|
||||
if card[0][1] == "A":
|
||||
l_rate -= 99
|
||||
msg += f"If Hit, chance {100-l_rate}% failure, and {l_rate}% success"
|
||||
return msg
|
||||
# If player already has 21 or more, hitting will always bust
|
||||
if player_hand.value >= 21:
|
||||
return "\n🧠 What do you think?"
|
||||
|
||||
# Calculate how much more the player can add without busting
|
||||
max_safe = 21 - player_hand.value
|
||||
|
||||
safe_cards = 0
|
||||
total_cards = 0
|
||||
for rank in VALUES:
|
||||
# 4 cards of each rank in a standard deck
|
||||
count = 4
|
||||
card_value = VALUES[rank]
|
||||
# Ace can be 1 or 11, but here we treat it as 1 if 11 would bust
|
||||
if rank == "A":
|
||||
card_value = 1 if player_hand.value + 11 > 21 else 11
|
||||
# Count as safe if it won't bust the player
|
||||
if card_value <= max_safe:
|
||||
safe_cards += count
|
||||
total_cards += count
|
||||
|
||||
# Calculate probability
|
||||
success_chance = int((safe_cards / total_cards) * 100)
|
||||
fail_chance = 100 - success_chance
|
||||
|
||||
return f"\n🧠Hit: {fail_chance}% 👎, {success_chance}% 👍"
|
||||
|
||||
def hits(obj_de):
|
||||
new_card = [obj_de.deal_cards()[0][0]]
|
||||
@@ -147,12 +159,12 @@ def display_hand(hand):
|
||||
|
||||
def show_some(player_cards, dealer_cards, obj_h):
|
||||
msg = f"Player[{obj_h.value}] {display_hand(player_cards)} "
|
||||
msg += f"Dealer[{VALUES[dealer_cards[1][1]]}] {dealer_cards[1][1]}{dealer_cards[1][0]} "
|
||||
msg += f"\nDealer[{VALUES[dealer_cards[1][1]]}] {dealer_cards[1][1]}{dealer_cards[1][0]} "
|
||||
return msg
|
||||
|
||||
def show_all(player_cards, dealer_cards, obj_h, obj_d):
|
||||
msg = f"Player[{obj_h.value}] {display_hand(player_cards)} "
|
||||
msg += f"Dealer[{obj_d.value}] {display_hand(dealer_cards)}"
|
||||
msg += f"\nDealer[{obj_d.value}] {display_hand(dealer_cards)}"
|
||||
return msg
|
||||
|
||||
def player_bust(obj_h, obj_c):
|
||||
@@ -229,7 +241,7 @@ def loadHSJack():
|
||||
pickle.dump(highScore, file)
|
||||
return 0
|
||||
|
||||
def playBlackJack(nodeID, message):
|
||||
def playBlackJack(nodeID, message, last_cmd=None):
|
||||
# Initalize the Game
|
||||
msg, last_cmd = '', None
|
||||
blackJack = False
|
||||
@@ -267,10 +279,12 @@ def playBlackJack(nodeID, message):
|
||||
|
||||
if last_cmd is None:
|
||||
# create new player if not in tracker
|
||||
logger.debug(f"System: BlackJack: New Player {nodeID}")
|
||||
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'last_played': time.time(), 'cash': jack_starting_cash,\
|
||||
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
|
||||
return f"Welcome to ♠️♥️BlackJack♣️♦️ you have {p_chips.total} chips. Whats your bet?"
|
||||
if nodeID != 0:
|
||||
#logger.debug(f"System: BlackJack: New Player {nodeID}")
|
||||
jackTracker.append({'nodeID': nodeID, 'cmd': 'new', 'last_played': time.time(), 'cash': jack_starting_cash,\
|
||||
'bet': 0, 'gameStats': {'p_win': p_win, 'd_win': d_win, 'draw': draw}, 'p_cards':p_cards, 'd_cards':d_cards, 'p_hand':p_hand.cards, 'd_hand':d_hand.cards, 'next_card':next_card})
|
||||
return f"You have {p_chips.total} chips. Whats your bet?"
|
||||
return "Error: Player not found."
|
||||
|
||||
if getLastCmdJack(nodeID) == "new":
|
||||
# Place Bet
|
||||
@@ -283,24 +297,26 @@ def playBlackJack(nodeID, message):
|
||||
#resend the hand
|
||||
msg += show_some(p_cards, d_cards, p_hand)
|
||||
return msg
|
||||
elif "blackjack" in message.lower():
|
||||
return f"\nTo place a bet, enter the amount you wish to wager."
|
||||
else:
|
||||
try:
|
||||
bet_money = int(message)
|
||||
except ValueError:
|
||||
return "Invalid Bet, please enter a valid number."
|
||||
return f"\nInvalid Bet, please enter a valid number."
|
||||
|
||||
if bet_money <= p_chips.total and bet_money >= 1:
|
||||
p_chips.bet = bet_money
|
||||
else:
|
||||
return f"Invalid Bet, the maximum bet you can place is {p_chips.total} and the minimum bet is 1."
|
||||
return f"\nInvalid Bet, the maximum bet you can place is {p_chips.total} and the minimum bet is 1."
|
||||
except ValueError:
|
||||
return f"Invalid Bet, the maximum bet, {p_chips.total}"
|
||||
return f"\nInvalid Bet, the maximum bet, {p_chips.total}"
|
||||
|
||||
# Show the cards
|
||||
msg += show_some(p_cards, d_cards, p_hand)
|
||||
# check for blackjack 21 and only two cards
|
||||
if p_hand.value == 21 and len(p_hand.cards) == 2:
|
||||
msg += "Player 🎰 BLAAAACKJACKKKK 💰"
|
||||
msg += f"\n🎰 BLAAAACKJACKKKK 💰"
|
||||
p_chips.total += round(p_chips.bet * 1.5)
|
||||
setLastCmdJack(nodeID, "dealerTurn")
|
||||
blackJack = True
|
||||
@@ -317,7 +333,7 @@ def playBlackJack(nodeID, message):
|
||||
|
||||
if getLastCmdJack(nodeID) == "betPlaced":
|
||||
setLastCmdJack(nodeID, "playing")
|
||||
msg += "(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
|
||||
msg += f"\n(H)it,(S)tand,(F)orfit,(D)ouble,(R)esend,(L)eave table"
|
||||
|
||||
# save the game state
|
||||
for i in range(len(jackTracker)):
|
||||
@@ -367,7 +383,7 @@ def playBlackJack(nodeID, message):
|
||||
# Check if player bust
|
||||
if player_bust(p_hand, p_chips):
|
||||
d_win += 1
|
||||
msg += "💥PlayerBUST💥"
|
||||
msg += f"\n💥PlayerBUST💥"
|
||||
setLastCmdJack(nodeID, "dealerTurn")
|
||||
|
||||
if getLastCmdJack(nodeID) == "playing":
|
||||
@@ -419,7 +435,7 @@ def playBlackJack(nodeID, message):
|
||||
d_hand.add_cards(d_card)
|
||||
if dealer_bust(d_hand, p_hand, p_chips):
|
||||
p_win += 1
|
||||
msg += "💰DealerBUST💥"
|
||||
msg += f"\n💰DealerBUST💥"
|
||||
break
|
||||
# Show all cards
|
||||
msg += show_all(p_hand.cards, d_hand.cards, p_hand, d_hand)
|
||||
@@ -427,15 +443,15 @@ def playBlackJack(nodeID, message):
|
||||
# Check who wins
|
||||
if push(p_hand, d_hand):
|
||||
draw += 1
|
||||
msg += "👌PUSH"
|
||||
msg += f"\n👌PUSH"
|
||||
elif player_wins(p_hand, d_hand, p_chips):
|
||||
p_win += 1
|
||||
msg += "🎉PLAYER WINS🎰"
|
||||
msg += f"\n🎉PLAYER WINS🎰"
|
||||
elif dealer_wins(p_hand, d_hand, p_chips):
|
||||
d_win += 1
|
||||
msg += "👎DEALER WINS"
|
||||
msg += f"\n👎DEALER WINS"
|
||||
else:
|
||||
msg += "👎DEALER WINS"
|
||||
msg += f"\n👎DEALER WINS"
|
||||
|
||||
# Display the Game Stats
|
||||
msg += gameStats(str(p_win), str(d_win), str(draw))
|
||||
@@ -443,20 +459,20 @@ def playBlackJack(nodeID, message):
|
||||
# Display the chips left
|
||||
if p_chips.total < 1:
|
||||
if p_chips.total > 0:
|
||||
msg += "🪙Keep the change you filthy animal!"
|
||||
msg += f"\n🪙Keep the change you filthy animal!"
|
||||
else:
|
||||
msg += "💸NO MORE CHIPS!🏧💳"
|
||||
msg += f"\n💸NO MORE CHIPS!🏧💳"
|
||||
p_chips.total = jack_starting_cash
|
||||
else:
|
||||
# check high score
|
||||
highScore = loadHSJack()
|
||||
if highScore != 0 and p_chips.total > highScore['highScore']:
|
||||
msg += f"💰HighScore💰{p_chips.total} "
|
||||
msg += f"\n💰HighScore💰{p_chips.total} "
|
||||
saveHSJack(nodeID, p_chips.total)
|
||||
else:
|
||||
msg += f"💰You have {p_chips.total} chips "
|
||||
msg += f"\n💰You have {p_chips.total} chips "
|
||||
|
||||
msg += " Bet or Leave?"
|
||||
msg += f"\nBet or Leave?"
|
||||
|
||||
# Reset the game
|
||||
setLastCmdJack(nodeID, "new")
|
||||
|
||||
@@ -14,7 +14,7 @@ dwInventoryDb = [{'userID': 1234567890, 'inventory': 0, 'priceList': [], 'amount
|
||||
dwCashDb = [{'userID': 1234567890, 'cash': starting_cash},]
|
||||
dwGameDayDb = [{'userID': 1234567890, 'day': 0},]
|
||||
dwLocationDb = [{'userID': 1234567890, 'location': 'USA', 'loc_choice': 0},]
|
||||
dwPlayerTracker = [{'userID': 1234567890, 'last_played': time.time(), 'cmd': 'start'},]
|
||||
from modules.settings import dwPlayerTracker
|
||||
# high score is saved in a pickle file
|
||||
dwHighScore = {}
|
||||
|
||||
|
||||
+12
-11
@@ -26,7 +26,7 @@ par4_5_range = par4_range + par5_range
|
||||
|
||||
# Player setup
|
||||
playingHole = False
|
||||
golfTracker = [{'nodeID': 0, 'last_played': time.time(), 'cmd': '', 'hole': 0, 'distance_remaining': 0, 'hole_shots': 0, 'hole_strokes': 0, 'hole_to_par': 0, 'total_strokes': 0, 'total_to_par': 0, 'par': 0, 'hazard': ''}]
|
||||
from modules.settings import golfTracker
|
||||
|
||||
# Club functions
|
||||
def hit_driver():
|
||||
@@ -122,9 +122,8 @@ def getHighScoreGolf(nodeID, strokes, par):
|
||||
return 0
|
||||
|
||||
# Main game loop
|
||||
def playGolf(nodeID, message, finishedHole=False):
|
||||
def playGolf(nodeID, message, finishedHole=False, last_cmd=''):
|
||||
msg = ''
|
||||
global golfTracker
|
||||
# Course setup
|
||||
par3_count = 0
|
||||
par4_count = 0
|
||||
@@ -150,8 +149,8 @@ def playGolf(nodeID, message, finishedHole=False):
|
||||
for i in range(len(golfTracker)):
|
||||
if golfTracker[i]['nodeID'] == nodeID:
|
||||
golfTracker[i]['last_played'] = time.time()
|
||||
|
||||
if last_cmd == "" or last_cmd == "new":
|
||||
|
||||
if last_cmd == "new":
|
||||
# Start a new hole
|
||||
if hole <= 9:
|
||||
# Set up hole count restrictions on par
|
||||
@@ -198,17 +197,19 @@ def playGolf(nodeID, message, finishedHole=False):
|
||||
# Set initial parameters before starting a hole
|
||||
distance_remaining = hole_length
|
||||
hole_shots = 0
|
||||
last_cmd = 'stroking'
|
||||
|
||||
# save player's current game state
|
||||
for i in range(len(golfTracker)):
|
||||
if golfTracker[i]['nodeID'] == nodeID:
|
||||
golfTracker[i]['cmd'] = last_cmd
|
||||
golfTracker[i]['hole'] = hole
|
||||
golfTracker[i]['distance_remaining'] = distance_remaining
|
||||
golfTracker[i]['cmd'] = 'stroking'
|
||||
golfTracker[i]['par'] = par
|
||||
golfTracker[i]['total_strokes'] = total_strokes
|
||||
golfTracker[i]['total_to_par'] = total_to_par
|
||||
golfTracker[i]['hazard'] = hazard
|
||||
golfTracker[i]['hole'] = hole
|
||||
golfTracker[i]['last_played'] = time.time()
|
||||
golfTracker[i]['hole_shots'] = hole_shots
|
||||
|
||||
@@ -325,8 +326,8 @@ def playGolf(nodeID, message, finishedHole=False):
|
||||
else:
|
||||
last_cmd = 'stroking'
|
||||
else:
|
||||
msg += "\nYou have " + str(distance_remaining) + "yd. ⛳️"
|
||||
msg += "\nClub?[D, L, M, H, G, W]🏌️"
|
||||
msg += f"\nYou have " + str(distance_remaining) + "yd. ⛳️"
|
||||
msg += f"\nClub?[D, L, M, H, G, W]🏌️"
|
||||
|
||||
|
||||
# save player's current game state, keep stroking
|
||||
@@ -370,7 +371,7 @@ def playGolf(nodeID, message, finishedHole=False):
|
||||
|
||||
if hole not in [1, 10]:
|
||||
# Show player total scoring info for the round, except hole 1 and 10
|
||||
msg += "\nYou've hit a total of " + str(total_strokes) + " strokes today, for"
|
||||
msg += f"\nYou've hit a total of " + str(total_strokes) + " strokes today, for"
|
||||
msg += getScorecardGolf(total_to_par)
|
||||
|
||||
# Move to next hole
|
||||
@@ -408,7 +409,7 @@ def playGolf(nodeID, message, finishedHole=False):
|
||||
logger.debug("System: GolfSim: Player " + str(nodeID) + " has finished their round.")
|
||||
else:
|
||||
# Show player the next hole
|
||||
msg += playGolf(nodeID, 'new', True)
|
||||
msg += "\n🏌️[D, L, M, H, G, W, End]🏌️"
|
||||
msg += playGolf(nodeID, '', True, last_cmd='new')
|
||||
msg += f"\n🏌️[D, L, M, H, G, W, End]🏌️"
|
||||
|
||||
return msg
|
||||
|
||||
@@ -168,10 +168,10 @@ def sendWithEmoji(message):
|
||||
i += 1
|
||||
return ' '.join(words)
|
||||
|
||||
def tell_joke(nodeID=0):
|
||||
def tell_joke(nodeID=0, vox=False):
|
||||
dadjoke = Dadjoke()
|
||||
try:
|
||||
if dad_jokes_emojiJokes:
|
||||
if dad_jokes_emojiJokes or vox:
|
||||
renderedLaugh = sendWithEmoji(dadjoke.joke)
|
||||
else:
|
||||
renderedLaugh = dadjoke.joke
|
||||
|
||||
+21
-21
@@ -23,6 +23,7 @@ lemonadeLemons = [{'nodeID': 0, 'cost': 4.00, 'count': 8, 'min': 2.00, 'unit': 0
|
||||
lemonadeSugar = [{'nodeID': 0, 'cost': 3.00, 'count': 15, 'min': 1.50, 'unit': 0.00}]
|
||||
lemonadeWeeks = [{'nodeID': 0, 'current': 1, 'total': lemon_total_weeks, 'sales': 99, 'potential': 0, 'unit': 0.00, 'price': 0.00, 'total_sales': 0}]
|
||||
lemonadeScore = [{'nodeID': 0, 'value': 0.00, 'total': 0.00}]
|
||||
from modules.settings import lemonadeTracker
|
||||
|
||||
def get_sales_amount(potential, unit, price):
|
||||
"""Gets the sales amount.
|
||||
@@ -258,7 +259,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
|
||||
buffer += ". " + \
|
||||
formatted + temperature.units + " " + \
|
||||
forecastd[list(forecastd)[temperature.forecast]][2] + \
|
||||
" " + glyph
|
||||
" " + glyph + f"\n"
|
||||
|
||||
# Calculate the potential sales as a percentage of the maximum value
|
||||
# (lower temperature = fewer sales, severe weather = fewer sales)
|
||||
@@ -287,23 +288,23 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
|
||||
|
||||
# Calculate the unit cost and display the estimated sales from the forecast potential
|
||||
unit = max(0.01, min(cups.unit + lemons.unit + sugar.unit, 4.0)) # limit the unit cost between $0.01 and $4.00
|
||||
buffer += " SupplyCost" + locale.currency(round(unit, 2), grouping=True) + " a cup."
|
||||
buffer += " Sales Potential:" + str(potential) + " cups."
|
||||
buffer += f"\nSupplyCost" + locale.currency(round(unit, 2), grouping=True) + " a cup."
|
||||
buffer += f"\nSales Potential:" + str(potential) + " cups."
|
||||
|
||||
# Display the current inventory
|
||||
buffer += " Inventory:"
|
||||
buffer += f"\nInventory:"
|
||||
buffer += "🥤:" + str(inventory.cups)
|
||||
buffer += "🍋:" + str(inventory.lemons)
|
||||
buffer += "🍚:" + str(inventory.sugar)
|
||||
|
||||
# Display the updated item prices
|
||||
buffer += f"\nPrices: "
|
||||
buffer += "🥤:" + locale.currency(round(cups.cost, 2), grouping=True) + " 📦 of " + str(cups.count) + "."
|
||||
buffer += " 🍋:" + locale.currency(round(lemons.cost, 2), grouping=True) + " 🧺 of " + str(lemons.count) + "."
|
||||
buffer += " 🍚:" + locale.currency(round(sugar.cost, 2), grouping=True) + " bag for " + str(sugar.count) + "🥤."
|
||||
buffer += f"\nPrices:\n"
|
||||
buffer += f"\n🥤:" + locale.currency(round(cups.cost, 2), grouping=True) + " 📦 of " + str(cups.count) + "."
|
||||
buffer += f"\n🍋:" + locale.currency(round(lemons.cost, 2), grouping=True) + " 🧺 of " + str(lemons.count) + "."
|
||||
buffer += f"\n🍚:" + locale.currency(round(sugar.cost, 2), grouping=True) + " bag for " + str(sugar.count) + "🥤."
|
||||
# Display the current cash
|
||||
gainloss = inventory.cash - inventory.start
|
||||
buffer += " 💵:" + locale.currency(round(inventory.cash, 2), grouping=True)
|
||||
buffer += f"\n💵:" + locale.currency(round(inventory.cash, 2), grouping=True)
|
||||
|
||||
|
||||
# if the player is in the red
|
||||
@@ -314,7 +315,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
|
||||
else:
|
||||
buffer += "📊P&L📈" + pnl
|
||||
|
||||
buffer += f"\n🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
|
||||
buffer += f"\n🥤 to buy?\nHave {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
|
||||
saveValues(nodeID, inventory, cups, lemons, sugar, weeks, score)
|
||||
return buffer
|
||||
|
||||
@@ -338,7 +339,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
|
||||
except Exception as e:
|
||||
return "invalid input, enter the number of 🥤 to purchase or (N)one"
|
||||
|
||||
msg += f"\n 🍋 to buy? Have {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
|
||||
msg += f"\n 🍋 to buy?\nHave {inventory.lemons}🥤 of 🍋 Cost {locale.currency(lemons.cost, grouping=True)} a 🧺 for {str(lemons.count)}🥤"
|
||||
# set the last command to lemons in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
@@ -368,7 +369,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
|
||||
newlemons = -1
|
||||
return "⛔️invalid input, enter the number of 🍋 to purchase"
|
||||
|
||||
msg += f"\n 🍚 to buy? You have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
|
||||
msg += f"\n 🍚 to buy?\nYou have {inventory.sugar}🥤 of 🍚, Cost {locale.currency(sugar.cost, grouping=True)} a bag for {str(sugar.count)}🥤"
|
||||
# set the last command to sugar in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
@@ -414,7 +415,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
|
||||
lemonadeTracker[i]['cmd'] = "sales"
|
||||
if "g" in message.lower():
|
||||
lemonadeTracker[i]['cmd'] = "cups"
|
||||
msg = f"#of🥤 to buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
|
||||
msg = f"#of🥤\nto buy? Have {inventory.cups} Cost {locale.currency(cups.cost, grouping=True)} a 📦 of {str(cups.count)}"
|
||||
return msg
|
||||
else:
|
||||
lemonsLastCmd = "sales"
|
||||
@@ -467,7 +468,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
|
||||
msg += " N.Profit:" + locale.currency(net, grouping=True)
|
||||
|
||||
# Display the updated inventory levels
|
||||
msg += "\nRemaining"
|
||||
msg += f"\nRemaining"
|
||||
msg += " 🥤:" + str(inventory.cups)
|
||||
msg += " 🍋:" + str(inventory.lemons)
|
||||
msg += " 🍚:" + str(inventory.sugar)
|
||||
@@ -484,7 +485,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
|
||||
pad_week = len(str(weeks.total))
|
||||
pad_sale = len(str(weeks.sales))
|
||||
total = 0
|
||||
msg += "\nWeekly📊"
|
||||
msg += f"\nWeekly📊"
|
||||
for i in range(len(weeks.summary)):
|
||||
msg += "#" + str(weeks.current).rjust(pad_week) + ". " + str(weeks.summary[i]['sales']).rjust(pad_sale) + \
|
||||
" sold x " + locale.currency(weeks.summary[i]['price'], grouping=True) + "ea. "
|
||||
@@ -524,7 +525,7 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
|
||||
if (inventory.sugar <= 0):
|
||||
msg += " You ran out of sugar.🍚"
|
||||
else:
|
||||
msg += "\nCongratulations 🍋🍋 your sales were perfect!🎉"
|
||||
msg += f"\nCongratulations 🍋🍋 your sales were perfect!🎉"
|
||||
|
||||
# Increment the score counters
|
||||
score.value = score.value + minnet
|
||||
@@ -535,27 +536,26 @@ def playLemonstand(nodeID, message, celsius=False, newgame=False):
|
||||
if (weeks.current == weeks.total):
|
||||
# end of the game
|
||||
success = round((score.value / score.total) * 100)
|
||||
msg += "\nYou've made " + locale.currency(score.value, grouping=True) + " out of a possible " + \
|
||||
msg += f"\nYou've made " + locale.currency(score.value, grouping=True) + " out of a possible " + \
|
||||
locale.currency(score.total, grouping=True) + " for a score of " + str(success) + "% "
|
||||
msg += "You've sold " + str(weeks.total_sales) + " total 🥤🍋"
|
||||
msg += f"\nYou've sold " + str(weeks.total_sales) + " total 🥤🍋"
|
||||
|
||||
# check for high score
|
||||
high_score = getHighScoreLemon()
|
||||
if (inventory.cash > int(high_score['cash'])):
|
||||
msg += "\nCongratulations! You've set a new high score!🎉💰🍋"
|
||||
msg += f"\nCongratulations! You've set a new high score!🎉💰🍋"
|
||||
high_score['cash'] = inventory.cash
|
||||
high_score['success'] = success
|
||||
high_score['userID'] = nodeID
|
||||
with open('data/lemonstand.pkl', 'wb') as file:
|
||||
pickle.dump(high_score, file)
|
||||
endGame(nodeID)
|
||||
|
||||
else:
|
||||
# keep playing
|
||||
|
||||
weeks.current = weeks.current + 1
|
||||
|
||||
msg += f"Play another week🥤? or (E)nd Game"
|
||||
msg += f"\nPlay another week🥤? or (E)nd Game"
|
||||
# set the last command to new in the inventory db
|
||||
for i in range(len(lemonadeTracker)):
|
||||
if lemonadeTracker[i]['nodeID'] == nodeID:
|
||||
|
||||
+73
-105
@@ -5,9 +5,7 @@ import random
|
||||
import time
|
||||
import pickle
|
||||
from modules.log import *
|
||||
|
||||
mindTracker = [{'nodeID': 0, 'last_played': time.time(), 'cmd': '', 'secret_code': '', 'diff': 'n', 'turns': 1}]
|
||||
|
||||
from modules.settings import mindTracker
|
||||
def chooseDifficultyMMind(message):
|
||||
usrInput = message.lower()
|
||||
msg = ''
|
||||
@@ -62,98 +60,63 @@ def makeCodeMMind(diff):
|
||||
return secret_code
|
||||
|
||||
#get guess from user
|
||||
def getGuessMMind(diff, guess):
|
||||
msg = ''
|
||||
if diff == "n":
|
||||
valid_colorsMMind = "RYGB"
|
||||
elif diff == "h":
|
||||
valid_colorsMMind = "RYGBOP"
|
||||
elif diff == "x":
|
||||
valid_colorsMMind = "RYGBOPWK"
|
||||
|
||||
user_guess = guess.upper()
|
||||
valid_guess = True
|
||||
if len(user_guess) != 4:
|
||||
valid_guess = False
|
||||
for i in range(len(user_guess)):
|
||||
if user_guess[i] not in valid_colorsMMind:
|
||||
valid_guess = False
|
||||
if valid_guess == False:
|
||||
user_guess = "XXXX"
|
||||
def getGuessMMind(diff, guess, nodeID):
|
||||
valid_colors = {
|
||||
"n": "RYGB",
|
||||
"h": "RYGBOP",
|
||||
"x": "RYGBOPWK"
|
||||
}
|
||||
user_guess = guess.strip().upper()
|
||||
if len(user_guess) != 4 or any(c not in valid_colors.get(diff, "RYGB") for c in user_guess):
|
||||
return "XXXX"
|
||||
|
||||
#increase the turn count and store in tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['turns'] += 1
|
||||
mindTracker[i]['last_played'] = time.time()
|
||||
mindTracker[i]['diff'] = diff
|
||||
return user_guess
|
||||
|
||||
def getHighScoreMMind(nodeID, turns, diff):
|
||||
# check if player is in high score list and pick the lowest score
|
||||
try:
|
||||
with open('mmind_hs.pkl', 'rb') as f:
|
||||
mindHighScore = pickle.load(f)
|
||||
except:
|
||||
logger.debug("System: MasterMind: High Score file not found.")
|
||||
mindHighScore = [{'nodeID': nodeID, 'turns': turns, 'diff': diff}]
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
import os
|
||||
hs_file = 'data/mmind_hs.pkl'
|
||||
# Try to load existing high scores
|
||||
if os.path.exists(hs_file):
|
||||
try:
|
||||
with open(hs_file, 'rb') as f:
|
||||
mindHighScore = pickle.load(f)
|
||||
except Exception as e:
|
||||
logger.debug(f"System: MasterMind: Error loading high score file: {e}")
|
||||
mindHighScore = []
|
||||
else:
|
||||
mindHighScore = []
|
||||
|
||||
# If nodeID==0, just return 0
|
||||
if nodeID == 0:
|
||||
# just return the high score
|
||||
mindHighScore = [{'nodeID': 0, 'turns': 0, 'diff': 'n'}]
|
||||
return mindHighScore
|
||||
|
||||
# calculate lowest score
|
||||
lowest_score = mindHighScore[0]['turns']
|
||||
# If no high score, add this one
|
||||
if not mindHighScore:
|
||||
mindHighScore = [{'nodeID': nodeID, 'turns': turns, 'diff': diff}]
|
||||
with open(hs_file, 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
|
||||
if mindHighScore[0]['diff'] == "n" and diff == "n":
|
||||
if lowest_score > turns:
|
||||
# update the high score for normal if new score is lower
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
# If the diff matches, compare and update if better
|
||||
if mindHighScore[0]['diff'] == diff:
|
||||
if turns < mindHighScore[0]['turns']:
|
||||
mindHighScore[0] = {'nodeID': nodeID, 'turns': turns, 'diff': diff}
|
||||
with open(hs_file, 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
elif mindHighScore[0]['diff'] == "n" and diff == "h":
|
||||
# update the high score for hard if normal is the only high score
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
elif mindHighScore[0]['diff'] == "h" and diff == "h":
|
||||
if lowest_score > turns:
|
||||
# update the high score for hard if new score is lower
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
elif mindHighScore[0]['diff'] == "n" or mindHighScore[0]['diff'] == "h" and diff == "x":
|
||||
# update the high score for expert if normal or high is the only high score
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
elif mindHighScore[0]['diff'] == "x" and diff == "x":
|
||||
if lowest_score > turns:
|
||||
# update the high score for expert if new score is lower
|
||||
mindHighScore[0]['nodeID'] = nodeID
|
||||
mindHighScore[0]['turns'] = turns
|
||||
mindHighScore[0]['diff'] = diff
|
||||
|
||||
# write new high score to file
|
||||
with open('mmind_hs.pkl', 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
return 0
|
||||
|
||||
# If the diff is different, replace with new high score for new diff
|
||||
mindHighScore[0] = {'nodeID': nodeID, 'turns': turns, 'diff': diff}
|
||||
with open(hs_file, 'wb') as f:
|
||||
pickle.dump(mindHighScore, f)
|
||||
return mindHighScore
|
||||
|
||||
|
||||
def getEmojiMMind(secret_code):
|
||||
@@ -182,7 +145,7 @@ def getEmojiMMind(secret_code):
|
||||
return secret_code_emoji
|
||||
|
||||
#compare userGuess with secret code and provide feedback
|
||||
def compareCodeMMind(secret_code, user_guess):
|
||||
def compareCodeMMind(secret_code, user_guess, nodeID):
|
||||
game_won = False
|
||||
perfect_pins = 0
|
||||
wrong_position = 0
|
||||
@@ -210,9 +173,26 @@ def compareCodeMMind(secret_code, user_guess):
|
||||
temp_code.remove(guess) # Remove the first occurrence of the matched color
|
||||
# display feedback
|
||||
if game_won:
|
||||
msg += f"Correct{getEmojiMMind(user_guess)}\n"
|
||||
msg += f"\n🏆Correct{getEmojiMMind(user_guess)}\nYou are the master mind!🤯"
|
||||
# get turn count from tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
turns = mindTracker[i]['turns'] - 2 # subtract 2 to account for increment after last guess and starting at 1
|
||||
diff = mindTracker[i]['diff']
|
||||
# get high score
|
||||
high_score = getHighScoreMMind(nodeID, turns, diff)
|
||||
if high_score[0]['turns'] != 0:
|
||||
msg += f"\n🏆 High Score:{turns} turns, Difficulty:{diff}"
|
||||
# reset turn count in tracker
|
||||
msg += f"\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
|
||||
# reset turn count in tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['turns'] = 0
|
||||
mindTracker[i]['secret_code'] = ''
|
||||
mindTracker[i]['cmd'] = 'new'
|
||||
else:
|
||||
msg += f"Guess{getEmojiMMind(user_guess)}\n"
|
||||
msg += f"\nGuess{getEmojiMMind(user_guess)}\n"
|
||||
|
||||
if perfect_pins > 0 and game_won == False:
|
||||
msg += "✅ color ✅ position: {}".format(perfect_pins)
|
||||
@@ -231,11 +211,11 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
|
||||
msg = ''
|
||||
won = False
|
||||
if turn_count <= 10:
|
||||
user_guess = getGuessMMind(diff, message)
|
||||
user_guess = getGuessMMind(diff, message, nodeID)
|
||||
if user_guess == "XXXX":
|
||||
msg += f"⛔️Invalid guess. Please enter 4 valid colors letters.\n🔴🟢🔵🔴 is RGBR"
|
||||
return msg
|
||||
check_guess = compareCodeMMind(secret_code, user_guess)
|
||||
check_guess = compareCodeMMind(secret_code, user_guess, nodeID)
|
||||
|
||||
# display turn count and feedback
|
||||
msg += "Turn {}:".format(turn_count)
|
||||
@@ -245,18 +225,6 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
|
||||
|
||||
if won == True:
|
||||
msg += f"\n🎉🧠 you win 🥷🤯"
|
||||
# get high score
|
||||
high_score = getHighScoreMMind(nodeID, turn_count, diff)
|
||||
if high_score != 0:
|
||||
msg += f"\n🏆 High Score:{high_score[0]['turns']} turns, Difficulty:{high_score[0]['diff'].upper()}"
|
||||
|
||||
msg += "\nWould you like to play again?\n(N)ormal, (H)ard, e(X)pert (E)nd?"
|
||||
# reset turn count in tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['turns'] = 1
|
||||
mindTracker[i]['secret_code'] = ''
|
||||
mindTracker[i]['cmd'] = 'new'
|
||||
else:
|
||||
# increment turn count and keep playing
|
||||
turn_count += 1
|
||||
@@ -266,12 +234,12 @@ def playGameMMind(diff, secret_code, turn_count, nodeID, message):
|
||||
mindTracker[i]['turns'] = turn_count
|
||||
elif won == False:
|
||||
msg += f"🙉Game Over🙈\nThe code was: {getEmojiMMind(secret_code)}"
|
||||
msg += "\nYou have run out of turns.😿"
|
||||
msg += "\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
|
||||
msg += f"\nYou have run out of turns.😿"
|
||||
msg += f"\nWould you like to play again? (N)ormal, (H)ard, or e(X)pert?"
|
||||
# reset turn count in tracker
|
||||
for i in range(len(mindTracker)):
|
||||
if mindTracker[i]['nodeID'] == nodeID:
|
||||
mindTracker[i]['turns'] = 1
|
||||
mindTracker[i]['turns'] = 0
|
||||
mindTracker[i]['secret_code'] = ''
|
||||
mindTracker[i]['cmd'] = 'new'
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ import pickle
|
||||
from modules.log import *
|
||||
|
||||
vpStartingCash = 20
|
||||
vpTracker= [{'nodeID': 0, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0}]
|
||||
|
||||
from modules.settings import vpTracker
|
||||
# Define the Card class
|
||||
class CardVP:
|
||||
|
||||
@@ -304,7 +303,7 @@ def playVideoPoker(nodeID, message):
|
||||
# create new player if not in tracker
|
||||
logger.debug(f"System: VideoPoker: New Player {nodeID}")
|
||||
vpTracker.append({'nodeID': nodeID, 'cmd': 'new', 'time': time.time(), 'cash': vpStartingCash, 'player': None, 'deck': None, 'highScore': 0, 'drawCount': 0})
|
||||
return f"Welcome to 🎰VideoPoker♥️ you have {vpStartingCash} coins, Whats your bet?"
|
||||
return f"You have {vpStartingCash} coins, \nWhats your bet?"
|
||||
|
||||
# Gather the player's bet
|
||||
if getLastCmdVp(nodeID) == "new" or getLastCmdVp(nodeID) == "gameOver":
|
||||
@@ -426,7 +425,7 @@ def playVideoPoker(nodeID, message):
|
||||
|
||||
if player.bankroll < 1:
|
||||
player.bankroll = vpStartingCash
|
||||
msg += "\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
|
||||
msg += f"\nLooks 💸 like you're out of money. 💳 resetting ballance 🏧"
|
||||
elif player.bankroll > vpTracker[i]['highScore']:
|
||||
vpTracker[i]['highScore'] = player.bankroll
|
||||
msg += " 🎉HighScore!"
|
||||
|
||||
+141
-37
@@ -48,7 +48,7 @@ meshBotAI = """
|
||||
PROMPT
|
||||
{input}
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
if llmContext_fromGoogle:
|
||||
meshBotAI = meshBotAI + """
|
||||
@@ -76,6 +76,142 @@ if llmEnableHistory:
|
||||
|
||||
"""
|
||||
|
||||
# Tooling Functions Defined Here
|
||||
# Example: current_time function
|
||||
def llmTool_current_time():
|
||||
"""
|
||||
Example tool function to get the current time.
|
||||
:return: Current time string.
|
||||
"""
|
||||
return datetime.now().strftime('%Y-%m-%d %H:%M:%S %Z')
|
||||
|
||||
def llmTool_math_calculator(expression):
|
||||
"""
|
||||
Example tool function to perform basic math calculations.
|
||||
:param expression: A string containing a math expression (e.g., "2 + 2").
|
||||
:return: The result of the calculation as a string.
|
||||
"""
|
||||
try:
|
||||
# WARNING: Using eval can be dangerous if not controlled properly.
|
||||
# This is a simple example; in production, consider using a safe math parser.
|
||||
result = eval(expression, {"__builtins__": None}, {})
|
||||
return str(result)
|
||||
except Exception as e:
|
||||
return f"Error in calculation: {e}"
|
||||
|
||||
def llmTool_get_google(query, num_results=3):
|
||||
"""
|
||||
Example tool function to perform a Google search and return results.
|
||||
:param query: The search query string.
|
||||
:param num_results: Number of search results to return.
|
||||
:return: A list of search result titles and descriptions.
|
||||
"""
|
||||
results = []
|
||||
try:
|
||||
googleSearch = search(query, advanced=True, num_results=num_results)
|
||||
for result in googleSearch:
|
||||
results.append(f"{result.title}: {result.description}")
|
||||
return results
|
||||
except Exception as e:
|
||||
return [f"Error in Google search: {e}"]
|
||||
|
||||
llmFunctions = [
|
||||
|
||||
{
|
||||
"name": "llmTool_current_time",
|
||||
"description": "Get the current time.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "llmTool_math_calculator",
|
||||
"description": "Perform basic math calculations.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expression": {
|
||||
"type": "string",
|
||||
"description": "A math expression to evaluate, e.g., '2 + 2'."
|
||||
}
|
||||
},
|
||||
"required": ["expression"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "llmTool_get_google",
|
||||
"description": "Perform a Google search and return results.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query string."
|
||||
},
|
||||
"num_results": {
|
||||
"type": "integer",
|
||||
"description": "Number of search results to return.",
|
||||
"default": 3
|
||||
}
|
||||
},
|
||||
"required": ["query"]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
def get_google_context(input, num_results):
|
||||
# Get context from Google search results
|
||||
googleResults = []
|
||||
try:
|
||||
googleSearch = search(input, advanced=True, num_results=num_results)
|
||||
if googleSearch:
|
||||
for result in googleSearch:
|
||||
googleResults.append(f"{result.title} {result.description}")
|
||||
else:
|
||||
googleResults = ['no other context provided']
|
||||
except Exception as e:
|
||||
logger.debug(f"System: LLM Query: context gathering failed, likely due to network issues")
|
||||
googleResults = ['no other context provided']
|
||||
return googleResults
|
||||
|
||||
def send_ollama_query(llmQuery):
|
||||
# Send the query to the Ollama API and return the response
|
||||
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
|
||||
if result.status_code == 200:
|
||||
result_json = result.json()
|
||||
result = result_json.get("response", "")
|
||||
# deepseek has added <think> </think> tags to the response
|
||||
if "<think>" in result:
|
||||
result = result.split("</think>")[1]
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code}")
|
||||
return result
|
||||
|
||||
def send_ollama_tooling_query(prompt, functions, model=None, max_tokens=450):
|
||||
"""
|
||||
Send a prompt and function/tool definitions to Ollama API for function calling.
|
||||
:param prompt: The user prompt string.
|
||||
:param functions: List of function/tool definitions (see Ollama API docs).
|
||||
:param model: Model name (optional, defaults to llmModel).
|
||||
:param max_tokens: Max tokens for response.
|
||||
:return: Ollama API response JSON.
|
||||
"""
|
||||
if model is None:
|
||||
model = llmModel
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"functions": functions,
|
||||
"stream": False,
|
||||
"max_tokens": max_tokens
|
||||
}
|
||||
result = requests.post(ollamaAPI, data=json.dumps(payload))
|
||||
if result.status_code == 200:
|
||||
return result.json()
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code} - {result.text}")
|
||||
|
||||
def llm_query(input, nodeID=0, location_name=None):
|
||||
global antiFloodLLM, llmChat_history
|
||||
googleResults = []
|
||||
@@ -109,23 +245,7 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
antiFloodLLM.append(nodeID)
|
||||
|
||||
if llmContext_fromGoogle and not rawLLMQuery:
|
||||
# grab some context from the internet using google search hits (if available)
|
||||
# localization details at https://pypi.org/project/googlesearch-python/
|
||||
|
||||
# remove common words from the search query
|
||||
# commonWordsList = ["is", "for", "the", "of", "and", "in", "on", "at", "to", "with", "by", "from", "as", "a", "an", "that", "this", "these", "those", "there", "here", "where", "when", "why", "how", "what", "which", "who", "whom", "whose", "whom"]
|
||||
# sanitizedSearch = ' '.join([word for word in input.split() if word.lower() not in commonWordsList])
|
||||
try:
|
||||
googleSearch = search(input, advanced=True, num_results=googleSearchResults)
|
||||
if googleSearch:
|
||||
for result in googleSearch:
|
||||
# SearchResult object has url= title= description= just grab title and description
|
||||
googleResults.append(f"{result.title} {result.description}")
|
||||
else:
|
||||
googleResults = ['no other context provided']
|
||||
except Exception as e:
|
||||
logger.debug(f"System: LLM Query: context gathering failed, likely due to network issues")
|
||||
googleResults = ['no other context provided']
|
||||
googleResults = get_google_context(input, googleSearchResults)
|
||||
|
||||
history = llmChat_history.get(nodeID, ["", ""])
|
||||
|
||||
@@ -151,20 +271,11 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
|
||||
llmQuery = {"model": llmModel, "prompt": modelPrompt, "stream": False, "max_tokens": tokens}
|
||||
# Query the model via Ollama web API
|
||||
result = requests.post(ollamaAPI, data=json.dumps(llmQuery))
|
||||
# Condense the result to just needed
|
||||
if result.status_code == 200:
|
||||
result_json = result.json()
|
||||
result = result_json.get("response", "")
|
||||
|
||||
# deepseek-r1 has added <think> </think> tags to the response
|
||||
if "<think>" in result:
|
||||
result = result.split("</think>")[1]
|
||||
else:
|
||||
raise Exception(f"HTTP Error: {result.status_code}")
|
||||
result = send_ollama_query(llmQuery)
|
||||
|
||||
#logger.debug(f"System: LLM Response: " + result.strip().replace('\n', ' '))
|
||||
except Exception as e:
|
||||
antiFloodLLM.remove(nodeID) # Ensure removal on error
|
||||
logger.warning(f"System: LLM failure: {e}")
|
||||
return "⛔️I am having trouble processing your request, please try again later."
|
||||
|
||||
@@ -175,15 +286,8 @@ def llm_query(input, nodeID=0, location_name=None):
|
||||
#retryy loop to truncate the response
|
||||
logger.warning(f"System: LLM Query: Response exceeded {tokens} characters, requesting truncation")
|
||||
truncateQuery = {"model": llmModel, "prompt": truncatePrompt + response, "stream": False, "max_tokens": tokens}
|
||||
truncateResult = requests.post(ollamaAPI, data=json.dumps(truncateQuery))
|
||||
if truncateResult.status_code == 200:
|
||||
truncate_json = truncateResult.json()
|
||||
result = truncate_json.get("response", "")
|
||||
truncateResult = send_ollama_query(truncateQuery)
|
||||
|
||||
else:
|
||||
#use the original result if truncation fails
|
||||
logger.warning("System: LLM Query: Truncation failed, using original response")
|
||||
|
||||
# cleanup for message output
|
||||
response = result.strip().replace('\n', ' ')
|
||||
|
||||
|
||||
+50
-12
@@ -295,9 +295,36 @@ def get_NOAAweather(lat=0, lon=0, unit=0):
|
||||
|
||||
return weather
|
||||
|
||||
def abbreviate_noaa(row):
|
||||
# replace long strings with shorter ones for display
|
||||
replacements = {
|
||||
def case_insensitive_replace(text, old, new):
|
||||
"""Replace all occurrences of old (any case) in text with new."""
|
||||
idx = 0
|
||||
old_lower = old.lower()
|
||||
text_lower = text.lower()
|
||||
while True:
|
||||
idx = text_lower.find(old_lower, idx)
|
||||
if idx == -1:
|
||||
break
|
||||
text = text[:idx] + new + text[idx+len(old):]
|
||||
text_lower = text.lower()
|
||||
idx += len(new)
|
||||
return text
|
||||
|
||||
def abbreviate_noaa(data=""):
|
||||
# Long phrases (with spaces)
|
||||
phrase_replacements = {
|
||||
"less than a tenth of an inch possible": "< 0.1in",
|
||||
"between a tenth and quarter of an inch possible": "0.1-0.25in",
|
||||
"between a quarter and half an inch possible": "0.25-0.5in",
|
||||
"between a half and three quarters of an inch possible": "0.5-0.75in",
|
||||
"between one and two inches possible": "1-2in",
|
||||
"between two and three inches possible": "2-3in",
|
||||
"between three and four inches possible": "3-4in",
|
||||
"between four and five inches possible": "4-5in",
|
||||
"between five and six inches possible": "5-6in",
|
||||
"between six and eight inches possible": "6-8in",
|
||||
}
|
||||
# Single words (no spaces)
|
||||
word_replacements = {
|
||||
"monday": "Mon",
|
||||
"tuesday": "Tue",
|
||||
"wednesday": "Wed",
|
||||
@@ -313,6 +340,8 @@ def abbreviate_noaa(row):
|
||||
"south": "S",
|
||||
"east": "E",
|
||||
"west": "W",
|
||||
"accumulation": "accum",
|
||||
"visibility": "vis",
|
||||
"precipitation": "precip",
|
||||
"showers": "shwrs",
|
||||
"thunderstorms": "t-storms",
|
||||
@@ -334,17 +363,26 @@ def abbreviate_noaa(row):
|
||||
"degrees": "°",
|
||||
"percent": "%",
|
||||
"department": "Dept.",
|
||||
"amounts less than a tenth of an inch possible.": "< 0.1in",
|
||||
"temperatures": "temps.",
|
||||
"temperature": "temp.",
|
||||
"temperatures": "temps:",
|
||||
"temperature": "temp:",
|
||||
"amounts": "amts:",
|
||||
"afternoon": "Aftn",
|
||||
"evening": "Eve",
|
||||
}
|
||||
|
||||
line = row
|
||||
for key, value in replacements.items():
|
||||
for variant in (key, key.capitalize(), key.upper()):
|
||||
if variant != value:
|
||||
line = line.replace(variant, value)
|
||||
return line
|
||||
text = data
|
||||
|
||||
# Replace long phrases (case-insensitive)
|
||||
for key in sorted(phrase_replacements, key=len, reverse=True):
|
||||
value = phrase_replacements[key]
|
||||
text = case_insensitive_replace(text, key, value)
|
||||
|
||||
# Replace single words (case-insensitive)
|
||||
for key in word_replacements:
|
||||
value = word_replacements[key]
|
||||
text = case_insensitive_replace(text, key, value)
|
||||
|
||||
return text
|
||||
|
||||
def getWeatherAlertsNOAA(lat=0, lon=0, useDefaultLatLon=False):
|
||||
# get weather alerts from NOAA limited to ALERT_COUNT with the total number of alerts found
|
||||
|
||||
+68
-32
@@ -17,6 +17,16 @@ if radio_detection_enabled:
|
||||
import socket
|
||||
|
||||
if voxDetectionEnabled:
|
||||
# methods available for trap word processing, these can be called by VOX detection when trap words are detected
|
||||
from mesh_bot import tell_joke, handle_wxc, handle_moon, handle_sun, handle_riverFlow, handle_tide, handle_satpass
|
||||
botMethods = {
|
||||
"joke": tell_joke,
|
||||
"weather": handle_wxc,
|
||||
"moon": handle_moon,
|
||||
"daylight": handle_sun,
|
||||
"river": handle_riverFlow,
|
||||
"tide": handle_tide,
|
||||
"satellite": handle_satpass}
|
||||
# module global variables
|
||||
previousVoxState = False
|
||||
voxHoldTime = signalHoldTime
|
||||
@@ -25,7 +35,7 @@ if voxDetectionEnabled:
|
||||
import sounddevice as sd # pip install sounddevice sudo apt install portaudio19-dev
|
||||
from vosk import Model, KaldiRecognizer # pip install vosk
|
||||
import json
|
||||
q = asyncio.Queue(maxsize=10) # what is a reasonable limit?
|
||||
q = asyncio.Queue(maxsize=32) # queue for audio data
|
||||
|
||||
if useLocalVoxModel:
|
||||
voxModel = Model(lang=localVoxModelPath) # use built in model for specified language
|
||||
@@ -116,11 +126,41 @@ def get_sig_strength():
|
||||
strength = get_hamlib('l STRENGTH')
|
||||
return strength
|
||||
|
||||
def vox_callback(indata, frames, time, status):
|
||||
if status:
|
||||
logger.warning(f"RadioMon: VOX input status: {status}")
|
||||
q.put(bytes(indata))
|
||||
|
||||
def checkVoxTrapWords(text):
|
||||
try:
|
||||
if not voxOnTrapList:
|
||||
logger.debug(f"RadioMon: VOX detected: {text}")
|
||||
return text
|
||||
if text:
|
||||
traps = [voxTrapList] if isinstance(voxTrapList, str) else voxTrapList
|
||||
text_lower = text.lower()
|
||||
for trap in traps:
|
||||
trap_clean = trap.strip()
|
||||
trap_lower = trap_clean.lower()
|
||||
idx = text_lower.find(trap_lower)
|
||||
if debugVoxTmsg:
|
||||
logger.debug(f"RadioMon: VOX checking for trap word '{trap_lower}' in: '{text}' (index: {idx})")
|
||||
if idx != -1:
|
||||
new_text = text[idx + len(trap_clean):].strip()
|
||||
if debugVoxTmsg:
|
||||
logger.debug(f"RadioMon: VOX detected trap word '{trap_lower}' in: '{text}' (remaining: '{new_text}')")
|
||||
new_words = new_text.split()
|
||||
if voxEnableCmd:
|
||||
for word in new_words:
|
||||
if word in botMethods:
|
||||
logger.info(f"RadioMon: VOX action '{word}' with '{new_text}'")
|
||||
if word == "joke":
|
||||
return botMethods[word](vox=True)
|
||||
else:
|
||||
return botMethods[word](None, None, None, vox=True)
|
||||
logger.debug(f"RadioMon: VOX returning text after trap word '{trap_lower}': '{new_text}'")
|
||||
return new_text
|
||||
if debugVoxTmsg:
|
||||
logger.debug(f"RadioMon: VOX no trap word found in: '{text}'")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.debug(f"RadioMon: Error in checkVoxTrapWords: {e}")
|
||||
return None
|
||||
|
||||
async def signalWatcher():
|
||||
global previousStrength
|
||||
@@ -146,15 +186,23 @@ async def signalWatcher():
|
||||
signalCycle = 0
|
||||
previousStrength = -40
|
||||
|
||||
def make_vox_callback(loop, q):
|
||||
async def make_vox_callback(loop, q):
|
||||
def vox_callback(indata, frames, time, status):
|
||||
if status:
|
||||
logger.warning(f"RadioMon: VOX input status: {status}")
|
||||
try:
|
||||
loop.call_soon_threadsafe(q.put_nowait, bytes(indata))
|
||||
except asyncio.QueueFull:
|
||||
# Optionally log or just drop the oldest
|
||||
logger.debug("RadioMon: VOX queue full, dropping audio frame")
|
||||
# Drop the oldest item and add the new one
|
||||
try:
|
||||
q.get_nowait() # Remove oldest
|
||||
except asyncio.QueueEmpty:
|
||||
pass
|
||||
try:
|
||||
loop.call_soon_threadsafe(q.put_nowait, bytes(indata))
|
||||
except asyncio.QueueFull:
|
||||
# If still full, just drop this frame
|
||||
logger.debug("RadioMon: VOX queue full, dropping audio frame")
|
||||
except RuntimeError:
|
||||
# Loop may be closed
|
||||
pass
|
||||
@@ -169,11 +217,11 @@ async def voxMonitor():
|
||||
logger.debug(f"RadioMon: VOX monitor started on device {device_info['name']} with samplerate {samplerate} using trap words: {voxTrapList if voxOnTrapList else 'none'}")
|
||||
rec = KaldiRecognizer(model, samplerate)
|
||||
loop = asyncio.get_running_loop()
|
||||
callback = make_vox_callback(loop, q)
|
||||
callback = await make_vox_callback(loop, q)
|
||||
with sd.RawInputStream(
|
||||
device=voxInputDevice,
|
||||
samplerate=samplerate,
|
||||
blocksize=8000,
|
||||
blocksize=4000,
|
||||
dtype='int16',
|
||||
channels=1,
|
||||
callback=callback
|
||||
@@ -183,28 +231,16 @@ async def voxMonitor():
|
||||
if rec.AcceptWaveform(data):
|
||||
result = rec.Result()
|
||||
text = json.loads(result).get("text", "")
|
||||
# check for trap words
|
||||
# process text
|
||||
if text and text != 'huh':
|
||||
if voxOnTrapList:
|
||||
if isinstance(voxTrapList, str):
|
||||
traps = [voxTrapList]
|
||||
else:
|
||||
traps = voxTrapList
|
||||
if any(trap.lower() in text.lower() for trap in traps):
|
||||
#remove the trap words from the text
|
||||
for trap in traps:
|
||||
text = text.replace(trap, '')
|
||||
text = text.strip()
|
||||
if text:
|
||||
logger.debug(f"RadioMon: VOX 🎙️Trapped {voxTrapList} in: {text}")
|
||||
voxMsgQueue.append(f"🎙️Trapped {voxDescription}: {text}")
|
||||
else:
|
||||
if debugVoxTmsg:
|
||||
logger.debug(f"RadioMon: VOX ignored text not on trap list: {text}")
|
||||
else:
|
||||
voxMsgQueue.append(f"🎙️Detected {voxDescription}: {text}")
|
||||
await asyncio.sleep(0.5)
|
||||
result = checkVoxTrapWords(text)
|
||||
if result:
|
||||
# If result is a function return, handle it (send to mesh, log, etc.)
|
||||
# If it's just text, handle as a normal message
|
||||
voxMsgQueue.append(result)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception as e:
|
||||
logger.error(f"RadioMon: Error in VOX monitor: {e}")
|
||||
|
||||
# end of file
|
||||
# end of file
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# modules/scheduler.py 2025 meshing-around
|
||||
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 = schedulerValue.lower().strip()
|
||||
schedulerTime = schedulerTime.strip()
|
||||
schedulerInterval = schedulerInterval.strip()
|
||||
schedulerChannel = int(schedulerChannel)
|
||||
schedulerInterface = int(schedulerInterface)
|
||||
# Setup the scheduler based on configuration
|
||||
try:
|
||||
if schedulerMotd:
|
||||
scheduler_message = MOTD
|
||||
else:
|
||||
scheduler_message = schedulerMessage
|
||||
|
||||
# Basic Scheduler Options
|
||||
if 'custom' not in schedulerValue:
|
||||
# Basic scheduler job to run the schedule see examples below for custom schedules
|
||||
if schedulerValue.lower() == 'day':
|
||||
if schedulerTime != '':
|
||||
schedule.every().day.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
else:
|
||||
schedule.every(int(schedulerInterval)).days.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'mon' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().monday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'tue' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().tuesday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'wed' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().wednesday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'thu' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().thursday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'fri' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().friday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sat' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().saturday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'sun' in schedulerValue.lower() and schedulerTime != '':
|
||||
schedule.every().sunday.at(schedulerTime).do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'hour' in schedulerValue.lower():
|
||||
schedule.every(int(schedulerInterval)).hours.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
elif 'min' in schedulerValue.lower():
|
||||
schedule.every(int(schedulerInterval)).minutes.do(lambda: send_message(scheduler_message, schedulerChannel, 0, schedulerInterface))
|
||||
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {schedulerInterval} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
else:
|
||||
# Default schedule if no valid configuration is provided
|
||||
|
||||
# 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}")
|
||||
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))
|
||||
|
||||
# Start the Broadcast Scheduler
|
||||
await BroadcastScheduler()
|
||||
except Exception as e:
|
||||
logger.error(f"System: Scheduler Error {e}")
|
||||
|
||||
# Enhanced Examples of using the scheduler, Times here are in 24hr format
|
||||
# https://schedule.readthedocs.io/en/stable/
|
||||
|
||||
# Good Morning Every day at 09:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("09:00").do(lambda: send_message("Good Morning", 2, 0, 1))
|
||||
|
||||
# Send WX every Morning at 08:00 using handle_wxc function to channel 2 on device 1
|
||||
#schedule.every().day.at("08:00").do(lambda: send_message(handle_wxc(0, 1, 'wx'), 2, 0, 1))
|
||||
|
||||
# Send Weather Channel Notice Wed. Noon on channel 2, device 1
|
||||
#schedule.every().wednesday.at("12:00").do(lambda: send_message("Weather alerts available on 'Alerts' channel with default 'AQ==' key.", 2, 0, 1))
|
||||
|
||||
# Send config URL for Medium Fast Network Use every other day at 10:00 to default channel 2 on device 1
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("Join us on Medium Fast https://meshtastic.org/e/#CgcSAQE6AggNEg4IARAEOAFAA0gBUB5oAQ", 2, 0, 1))
|
||||
|
||||
# Send a Net Starting Now Message Every Wednesday at 19:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().wednesday.at("19:00").do(lambda: send_message("Net Starting Now", 2, 0, 1))
|
||||
|
||||
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00 using send_message function to channel 2 on device 1
|
||||
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", 2, 0, 1)).day(15, 25)
|
||||
|
||||
# Send a Welcome Notice for group on the 15th and 25th of the month at 12:00
|
||||
#schedule.every().day.at("12:00").do(lambda: send_message("Welcome to the group", schedulerChannel, 0, schedulerInterface)).day(15, 25)
|
||||
|
||||
# Send a joke every 6 hours
|
||||
#schedule.every(6).hours.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
|
||||
|
||||
# Send a joke every 2 minutes
|
||||
#schedule.every(2).minutes.do(lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface))
|
||||
|
||||
# Send the Welcome Message every other day at 08:00
|
||||
#schedule.every(2).days.at("08:00").do(lambda: send_message(welcome_message, schedulerChannel, 0, schedulerInterface))
|
||||
|
||||
# Send the MOTD every day at 13:00
|
||||
#schedule.every().day.at("13:00").do(lambda: send_message(MOTD, schedulerChannel, 0, schedulerInterface))
|
||||
|
||||
# Send bbslink looking for peers every other day at 10:00
|
||||
#schedule.every(2).days.at("10:00").do(lambda: send_message("bbslink MeshBot looking for peers", schedulerChannel, 0, schedulerInterface))
|
||||
+17
-2
@@ -28,11 +28,23 @@ wiki_return_limit = 3 # limit the number of sentences returned off the first par
|
||||
GAMEDELAY = 28800 # 8 hours in seconds for game mode holdoff
|
||||
cmdHistory = [] # list to hold the last commands
|
||||
seenNodes = [] # list to hold the last seen nodes
|
||||
surveyTracker, tictactoeTracker, hamtestTracker, hangmanTracker, golfTracker, mastermindTracker, vpTracker, blackjackTracker, lemonadeTracker, dwPlayerTracker, jackTracker = [], [], [], [], [], [], [], [], [], [], [] # game trackers
|
||||
cmdHistory = [] # list to hold the command history for lheard and history commands
|
||||
msg_history = [] # list to hold the message history for the messages command
|
||||
max_bytes = 200 # Meshtastic has ~237 byte limit, use conservative 200 bytes for message content
|
||||
voxMsgQueue = [] # queue for VOX detected messages
|
||||
# Game trackers
|
||||
surveyTracker = [] # Survey game tracker
|
||||
tictactoeTracker = [] # TicTacToe game tracker
|
||||
hamtestTracker = [] # Ham radio test tracker
|
||||
hangmanTracker = [] # Hangman game tracker
|
||||
golfTracker = [] # GolfSim game tracker
|
||||
mastermindTracker = [] # Mastermind game tracker
|
||||
vpTracker = [] # Video Poker game tracker
|
||||
jackTracker = [] # Blackjack game tracker
|
||||
lemonadeTracker = [] # Lemonade Stand game tracker
|
||||
dwPlayerTracker = [] # DopeWars player tracker
|
||||
jackTracker = [] # Jack game tracker
|
||||
mindTracker = [] # Mastermind (mmind) game tracker
|
||||
|
||||
# Read the config file, if it does not exist, create basic config file
|
||||
config = configparser.ConfigParser()
|
||||
@@ -268,6 +280,7 @@ try:
|
||||
highfly_ignoreList = config['sentry'].get('highFlyingIgnoreList', '').split(',') # default empty
|
||||
highfly_check_openskynetwork = config['sentry'].getboolean('highflyOpenskynetwork', True) # default True check with OpenSkyNetwork if highfly detected
|
||||
detctionSensorAlert = config['sentry'].getboolean('detectionSensorAlert', False) # default False
|
||||
reqLocationEnabled = config['sentry'].getboolean('reqLocationEnabled', False) # default False
|
||||
|
||||
# location
|
||||
location_enabled = config['location'].getboolean('enabled', True)
|
||||
@@ -376,6 +389,7 @@ try:
|
||||
voxInputDevice = config['radioMon'].get('voxInputDevice', 'default') # default default
|
||||
voxOnTrapList = config['radioMon'].getboolean('voxOnTrapList', False) # default False
|
||||
voxTrapList = config['radioMon'].get('voxTrapList', 'chirpy').split(',') # default chirpy
|
||||
voxEnableCmd = config['radioMon'].getboolean('voxEnableCmd', True) # default True
|
||||
|
||||
# file monitor
|
||||
file_monitor_enabled = config['fileMon'].getboolean('filemon_enabled', False)
|
||||
@@ -403,6 +417,7 @@ try:
|
||||
tictactoe_enabled = config['games'].getboolean('tictactoe', True)
|
||||
quiz_enabled = config['games'].getboolean('quiz', False)
|
||||
survey_enabled = config['games'].getboolean('survey', False)
|
||||
default_survey = config['games'].get('defaultSurvey', 'example') # default example
|
||||
surveyRecordID = config['games'].getboolean('surveyRecordID', True)
|
||||
surveyRecordLocation = config['games'].getboolean('surveyRecordLocation', True)
|
||||
|
||||
@@ -411,7 +426,7 @@ try:
|
||||
splitDelay = config['messagingSettings'].getfloat('splitDelay', 0) # default 0
|
||||
MESSAGE_CHUNK_SIZE = config['messagingSettings'].getint('MESSAGE_CHUNK_SIZE', 160) # default 160 chars
|
||||
wantAck = config['messagingSettings'].getboolean('wantAck', False) # default False
|
||||
maxBuffer = config['messagingSettings'].getint('maxBuffer', 200) # default 200
|
||||
maxBuffer = config['messagingSettings'].getint('maxBuffer', 200) # default 200 bytes
|
||||
enableHopLogs = config['messagingSettings'].getboolean('enableHopLogs', False) # default False
|
||||
debugMetadata = config['messagingSettings'].getboolean('debugMetadata', False) # default False
|
||||
metadataFilter = config['messagingSettings'].get('metadataFilter', '').split(',') # default empty
|
||||
|
||||
+18
-14
@@ -49,20 +49,24 @@ class SurveyModule:
|
||||
logger.error(f"Survey: Error loading surveys: {e}")
|
||||
|
||||
def start_survey(self, user_id, survey_name='example', location=None):
|
||||
"""Begin a new survey session for a user."""
|
||||
if not survey_name:
|
||||
survey_name = 'example'
|
||||
if survey_name not in allowedSurveys:
|
||||
return f"error: survey '{survey_name}' is not allowed."
|
||||
self.responses[user_id] = {
|
||||
'survey_name': survey_name,
|
||||
'current_question': 0,
|
||||
'answers': [],
|
||||
'location': location if surveyRecordLocation and location is not None else 'N/A'
|
||||
}
|
||||
msg = f"'{survey_name}'📝survey\nSend answer' or 'end'\n"
|
||||
msg += self.show_question(user_id)
|
||||
return msg
|
||||
try:
|
||||
"""Begin a new survey session for a user."""
|
||||
if not survey_name:
|
||||
survey_name = default_survey
|
||||
if survey_name not in allowedSurveys:
|
||||
return f"error: survey '{survey_name}' is not allowed."
|
||||
self.responses[user_id] = {
|
||||
'survey_name': survey_name,
|
||||
'current_question': 0,
|
||||
'answers': [],
|
||||
'location': location if surveyRecordLocation and location is not None else 'N/A'
|
||||
}
|
||||
msg = f"'{survey_name}'📝survey\nSend answer' or 'end'\n"
|
||||
msg += self.show_question(user_id)
|
||||
return msg
|
||||
except Exception as e:
|
||||
logger.error(f"Error starting survey for user {user_id}: {e}")
|
||||
return "An error occurred while starting the survey. Please try again later."
|
||||
|
||||
def show_question(self, user_id):
|
||||
"""Show the current question for the user, or end the survey."""
|
||||
|
||||
+251
-119
@@ -7,6 +7,7 @@ import meshtastic.ble_interface
|
||||
import time
|
||||
import asyncio
|
||||
import random
|
||||
import base64
|
||||
# not ideal but needed?
|
||||
import contextlib # for suppressing output on watchdog
|
||||
import io # for suppressing output on watchdog
|
||||
@@ -315,6 +316,24 @@ if ble_count > 1:
|
||||
logger.critical(f"System: Multiple BLE interfaces detected. Only one BLE interface is allowed. Exiting")
|
||||
exit()
|
||||
|
||||
def xor_hash(data: bytes) -> int:
|
||||
"""Compute an XOR hash from bytes."""
|
||||
result = 0
|
||||
for char in data:
|
||||
result ^= char
|
||||
return result
|
||||
|
||||
def generate_hash(name: str, key: str) -> int:
|
||||
"""generate the channel number by hashing the channel name and psk"""
|
||||
if key == "AQ==":
|
||||
key = "1PG7OiApB1nwvP+rz05pAQ=="
|
||||
replaced_key = key.replace("-", "+").replace("_", "/")
|
||||
key_bytes = base64.b64decode(replaced_key.encode("utf-8"))
|
||||
h_name = xor_hash(bytes(name, "utf-8"))
|
||||
h_key = xor_hash(key_bytes)
|
||||
result: int = h_name ^ h_key
|
||||
return result
|
||||
|
||||
# Initialize interfaces
|
||||
logger.debug(f"System: Initializing Interfaces")
|
||||
interface1 = interface2 = interface3 = interface4 = interface5 = interface6 = interface7 = interface8 = interface9 = None
|
||||
@@ -331,7 +350,20 @@ for i in range(1, 10):
|
||||
if interface_type == 'serial':
|
||||
globals()[f'interface{i}'] = meshtastic.serial_interface.SerialInterface(globals().get(f'port{i}'))
|
||||
elif interface_type == 'tcp':
|
||||
globals()[f'interface{i}'] = meshtastic.tcp_interface.TCPInterface(globals().get(f'hostname{i}'))
|
||||
host = globals().get(f'hostname{i}', '127.0.0.1')
|
||||
port = 4403
|
||||
|
||||
# Allow host:port format
|
||||
if isinstance(host, str) and ':' in host:
|
||||
maybe_host, maybe_port = host.rsplit(':', 1)
|
||||
if maybe_port.isdigit():
|
||||
host = maybe_host
|
||||
try:
|
||||
port = int(maybe_port)
|
||||
except ValueError:
|
||||
port = 4403
|
||||
|
||||
globals()[f'interface{i}'] = meshtastic.tcp_interface.TCPInterface(hostname=host, portNumber=port)
|
||||
elif interface_type == 'ble':
|
||||
globals()[f'interface{i}'] = meshtastic.ble_interface.BLEInterface(globals().get(f'mac{i}'))
|
||||
else:
|
||||
@@ -352,6 +384,43 @@ for i in range(1, 10):
|
||||
else:
|
||||
globals()[f'myNodeNum{i}'] = 777
|
||||
|
||||
# Fetch channel list from each device
|
||||
channel_list = []
|
||||
for i in range(1, 10):
|
||||
if globals().get(f'interface{i}') and globals().get(f'interface{i}_enabled'):
|
||||
try:
|
||||
node = globals()[f'interface{i}'].getNode('^local')
|
||||
channels = node.channels
|
||||
channel_dict = {}
|
||||
for channel in channels:
|
||||
if hasattr(channel, 'role') and channel.role:
|
||||
channel_name = getattr(channel.settings, 'name', '').strip()
|
||||
channel_number = getattr(channel, 'index', 0)
|
||||
# Only add channels with a non-empty name
|
||||
if channel_name:
|
||||
channel_dict[channel_name] = channel_number
|
||||
channel_list.append({
|
||||
"interface_id": i,
|
||||
"channels": channel_dict
|
||||
})
|
||||
logger.debug(f"System: Fetched Channel List from Device{i}")
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error fetching channel list from Device{i}: {e}")
|
||||
|
||||
# add channel hash to channel_list
|
||||
for device in channel_list:
|
||||
interface_id = device["interface_id"]
|
||||
for channel_name, channel_number in device["channels"].items():
|
||||
psk_base64 = base64.b64encode(channel.settings.psk).decode('utf-8')
|
||||
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:
|
||||
if entry["interface_id"] == interface_id:
|
||||
entry["channels"][channel_name] = {
|
||||
"number": channel_number,
|
||||
"hash": channel_hash
|
||||
}
|
||||
|
||||
#### FUN-ctions ####
|
||||
|
||||
def cleanup_memory():
|
||||
@@ -386,11 +455,11 @@ def cleanup_memory():
|
||||
# Clean up stale game tracker entries
|
||||
cleanup_game_trackers(current_time)
|
||||
|
||||
# Clean up multiPingList of completed or stale entries
|
||||
if 'multiPingList' in globals():
|
||||
multiPingList[:] = [ping for ping in multiPingList
|
||||
if ping.get('message_from_id', 0) != 0 and
|
||||
ping.get('count', 0) > 0]
|
||||
# # Clean up multiPingList of completed or stale entries
|
||||
# if 'multiPingList' in globals():
|
||||
# multiPingList[:] = [ping for ping in multiPingList
|
||||
# if ping.get('message_from_id', 0) != 0 and
|
||||
# ping.get('count', 0) > 0]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error during memory cleanup: {e}")
|
||||
@@ -546,7 +615,8 @@ def get_node_location(nodeID, nodeInt=1, channel=0, round_digits=2):
|
||||
if fuzzItAll:
|
||||
latitude = round(latitude, round_digits)
|
||||
longitude = round(longitude, round_digits)
|
||||
logger.debug(f"System: Fuzzed location data for {nodeID}")
|
||||
logger.debug(f"System: Fuzzed location data for {nodeID} is {latitude}, {longitude}")
|
||||
logger.debug(f"System: Location data for {nodeID} is {latitude}, {longitude}")
|
||||
return [latitude, longitude]
|
||||
except Exception as e:
|
||||
logger.warning(f"System: Error processing position for node {nodeID}: {e}")
|
||||
@@ -557,57 +627,59 @@ def get_node_location(nodeID, nodeInt=1, channel=0, round_digits=2):
|
||||
else:
|
||||
return config_position
|
||||
|
||||
def get_closest_nodes(nodeInt=1,returnCount=3, channel=publicChannel):
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
node_list = []
|
||||
async def get_closest_nodes(nodeInt=1,returnCount=3, channel=publicChannel):
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
node_list = []
|
||||
|
||||
if interface.nodes:
|
||||
for node in interface.nodes.values():
|
||||
if 'position' in node:
|
||||
try:
|
||||
nodeID = node['num']
|
||||
latitude = node['position']['latitude']
|
||||
longitude = node['position']['longitude']
|
||||
|
||||
#lastheard time in unix time
|
||||
lastheard = node.get('lastHeard', 0)
|
||||
#if last heard is over 24 hours ago, ignore the node
|
||||
if lastheard < (time.time() - 86400):
|
||||
continue
|
||||
|
||||
# Calculate distance to node from config.ini location
|
||||
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
|
||||
|
||||
if (distance < sentry_radius):
|
||||
if (nodeID not in [globals().get(f'myNodeNum{i}') for i in range(1, 10)]) and str(nodeID) not in sentryIgnoreList:
|
||||
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
else:
|
||||
# request location data
|
||||
reqLocationEnabled = False
|
||||
if reqLocationEnabled:
|
||||
if interface.nodes:
|
||||
for node in interface.nodes.values():
|
||||
if 'position' in node:
|
||||
try:
|
||||
logger.debug(f"System: Requesting location data for {node['id']}, lastHeard: {node.get('lastHeard', 'N/A')}")
|
||||
# one idea is to send a ping to the node to request location data for if or when, ask again later
|
||||
interface.sendPosition(destinationId=node['id'], wantResponse=False, channelIndex=channel)
|
||||
# wait a bit
|
||||
time.sleep(3)
|
||||
# send a traceroute request
|
||||
interface.sendTraceRoute(destinationId=node['id'], channelIndex=channel, wantResponse=False)
|
||||
# wait a bit
|
||||
time.sleep(1)
|
||||
nodeID = node['num']
|
||||
latitude = node['position']['latitude']
|
||||
longitude = node['position']['longitude']
|
||||
|
||||
#lastheard time in unix time
|
||||
lastheard = node.get('lastHeard', 0)
|
||||
#if last heard is over 24 hours ago, ignore the node
|
||||
if lastheard < (time.time() - 86400):
|
||||
continue
|
||||
|
||||
# Calculate distance to node from config.ini location
|
||||
distance = round(geopy.distance.geodesic((latitudeValue, longitudeValue), (latitude, longitude)).m, 2)
|
||||
|
||||
if (distance < sentry_radius):
|
||||
if (nodeID not in [globals().get(f'myNodeNum{i}') for i in range(1, 10)]) and str(nodeID) not in sentryIgnoreList:
|
||||
node_list.append({'id': nodeID, 'latitude': latitude, 'longitude': longitude, 'distance': distance})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error requesting location data for {node['id']}. Error: {e}")
|
||||
# sort by distance closest
|
||||
#node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2)
|
||||
node_list.sort(key=lambda x: x['distance'])
|
||||
# return the first 3 closest nodes by default
|
||||
return node_list[:returnCount]
|
||||
else:
|
||||
logger.warning(f"System: No nodes found in closest_nodes on interface {nodeInt}")
|
||||
return ERROR_FETCHING_DATA
|
||||
pass
|
||||
else:
|
||||
# request location data currently blocking needs to be async
|
||||
reqLocationEnabled = False
|
||||
if reqLocationEnabled:
|
||||
try:
|
||||
logger.debug(f"System: Requesting location data for {node['id']}, lastHeard: {node.get('lastHeard', 'N/A')}")
|
||||
# if not a interface node
|
||||
if node['num'] in [globals().get(f'myNodeNum{i}') for i in range(1, 10)]:
|
||||
ignore = True
|
||||
else:
|
||||
# one idea is to send a ping to the node to request location data for if or when, ask again later
|
||||
interface.sendPosition(destinationId=node['id'], wantResponse=False, channelIndex=channel)
|
||||
# wayyy too fast async wait
|
||||
|
||||
# send a traceroute request
|
||||
interface.sendTraceRoute(destinationId=node['id'], channelIndex=channel, wantResponse=False)
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error requesting location data for {node['id']}. Error: {e}")
|
||||
# sort by distance closest
|
||||
#node_list.sort(key=lambda x: (x['latitude']-latitudeValue)**2 + (x['longitude']-longitudeValue)**2)
|
||||
node_list.sort(key=lambda x: x['distance'])
|
||||
# return the first 3 closest nodes by default
|
||||
return node_list[:returnCount]
|
||||
else:
|
||||
logger.warning(f"System: No nodes found in closest_nodes on interface {nodeInt}")
|
||||
return ERROR_FETCHING_DATA
|
||||
|
||||
def handleFavoriteNode(nodeInt=1, nodeID=0, aor=False):
|
||||
# Add or remove a favorite node for the given interface. aor: True to add, False to remove.
|
||||
@@ -797,11 +869,43 @@ def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
interface.sendText(text=message, channelIndex=ch, destinationId=nodeid)
|
||||
# Throttle the message sending to prevent spamming the device
|
||||
time.sleep(responseDelay)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"System: Exception during send_message: {e} (message length: {len(message)})")
|
||||
return False
|
||||
|
||||
def send_raw_bytes(nodeid, raw_bytes, nodeInt=1, channel=0, portnum=256, want_ack=True):
|
||||
# Send raw bytes to a node using the Meshtastic interface.
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
try:
|
||||
interface.sendData(
|
||||
raw_bytes,
|
||||
destinationId=nodeid,
|
||||
portNum=portnum,
|
||||
channelIndex=channel,
|
||||
wantAck=want_ack
|
||||
)
|
||||
# Throttle the message sending to prevent spamming the device
|
||||
logger.debug(f"System: Sent raw bytes to {nodeid} on portnum {portnum} via Device{nodeInt}")
|
||||
time.sleep(responseDelay)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error sending raw bytes to {nodeid} via Device{nodeInt}: {e} bytes: {raw_bytes}")
|
||||
return False
|
||||
|
||||
def decode_raw_bytes(raw_bytes):
|
||||
# Decode raw bytes received from a Meshtastic device.
|
||||
try:
|
||||
decoded_message = raw_bytes.decode('utf-8', errors='ignore')
|
||||
# reminder for a synch word check or crc check if needed later
|
||||
logger.debug(f"Decoded raw bytes: {decoded_message}")
|
||||
return decoded_message
|
||||
except Exception as e:
|
||||
logger.debug(f"System: Error decoding raw bytes: {e} bytes: {raw_bytes}")
|
||||
return ""
|
||||
|
||||
def messageTrap(msg):
|
||||
# Check if the message contains a trap word, this is the first filter for listning to messages
|
||||
# after this the message is passed to the command_handler in the bot.py which is switch case filter for applying word to function
|
||||
@@ -862,17 +966,23 @@ def save_bbsBanList():
|
||||
|
||||
def load_bbsBanList():
|
||||
global bbs_ban_list
|
||||
# load the bbs_ban_list from file
|
||||
loaded_list = []
|
||||
try:
|
||||
with open('data/bbs_ban_list.txt', 'r') as f:
|
||||
bbs_ban_list = [line.strip() for line in f.readlines() if line.strip()]
|
||||
logger.debug("System: BBS ban list loaded")
|
||||
loaded_list = [line.strip() for line in f if line.strip()]
|
||||
logger.debug("System: BBS ban list loaded from file")
|
||||
except FileNotFoundError:
|
||||
bbs_ban_list = config['bbs'].get('bbs_ban_list', '').split(',')
|
||||
logger.debug("System: No BBS ban list found, starting with default")
|
||||
config_val = config['bbs'].get('bbs_ban_list', '')
|
||||
if config_val:
|
||||
loaded_list = [x.strip() for x in config_val.split(',') if x.strip()]
|
||||
logger.debug("System: No BBS ban list file found, loaded from config or started empty")
|
||||
except Exception as e:
|
||||
logger.error(f"System: Error loading BBS ban list: {e}")
|
||||
bbs_ban_list = []
|
||||
|
||||
# Merge loaded_list into bbs_ban_list, only adding new entries
|
||||
for node in loaded_list:
|
||||
if node not in bbs_ban_list:
|
||||
bbs_ban_list.append(node)
|
||||
|
||||
def isNodeAdmin(nodeID):
|
||||
# check if the nodeID is in the bbs_admin_list
|
||||
@@ -907,6 +1017,7 @@ def handle_bbsban(message, message_from_id, isDM):
|
||||
action = parts[1]
|
||||
|
||||
if action == "list":
|
||||
load_bbsBanList() # Always reload from file for latest list
|
||||
if bbs_ban_list:
|
||||
return "BBS Ban List:\n" + "\n".join(bbs_ban_list)
|
||||
else:
|
||||
@@ -980,7 +1091,6 @@ def handleMultiPing(nodeID=0, deviceID=1):
|
||||
|
||||
# send the DM
|
||||
send_message(f"🔂{count} {type}", channel_number, message_id_from, deviceID, bypassChuncking=True)
|
||||
time.sleep(responseDelay + 1)
|
||||
if count < 2:
|
||||
# remove the item from the list
|
||||
for j in range(len(multiPingList)):
|
||||
@@ -1065,9 +1175,6 @@ def handleAlertBroadcast(deviceID=1):
|
||||
else:
|
||||
send_message(deAlert, emergencyAlertBroadcastCh, 0, deviceID)
|
||||
return True
|
||||
|
||||
# pause for traffic
|
||||
time.sleep(5)
|
||||
|
||||
if wxAlertBroadcastEnabled:
|
||||
if wxAlert:
|
||||
@@ -1081,9 +1188,6 @@ def handleAlertBroadcast(deviceID=1):
|
||||
else:
|
||||
send_message(wxAlert, wxAlertBroadcastChannel, 0, deviceID)
|
||||
return True
|
||||
|
||||
# pause for traffic
|
||||
time.sleep(5)
|
||||
|
||||
if volcanoAlertBroadcastEnabled:
|
||||
volcanoAlert = get_volcano_usgs(latitudeValue, longitudeValue)
|
||||
@@ -1104,12 +1208,13 @@ def onDisconnect(interface):
|
||||
interface.close()
|
||||
|
||||
# Telemetry Functions
|
||||
telemetryData = {}
|
||||
localTelemetryData = {}
|
||||
def initialize_telemetryData():
|
||||
telemetryData[0] = {f'interface{i}': 0 for i in range(1, 10)}
|
||||
telemetryData[0].update({f'lastAlert{i}': '' for i in range(1, 10)})
|
||||
global localTelemetryData
|
||||
localTelemetryData[0] = {f'interface{i}': 0 for i in range(1, 10)}
|
||||
localTelemetryData[0].update({f'lastAlert{i}': '' for i in range(1, 10)})
|
||||
for i in range(1, 10):
|
||||
telemetryData[i] = {'numPacketsTx': 0, 'numPacketsRx': 0, 'numOnlineNodes': 0, 'numPacketsTxErr': 0, 'numPacketsRxErr': 0, 'numTotalNodes': 0}
|
||||
localTelemetryData[i] = {'numPacketsTx': 0, 'numPacketsRx': 0, 'numOnlineNodes': 0, 'numPacketsTxErr': 0, 'numPacketsRxErr': 0, 'numTotalNodes': 0}
|
||||
|
||||
# indented to be called from the main loop
|
||||
initialize_telemetryData()
|
||||
@@ -1162,23 +1267,26 @@ def compileFavoriteList(getInterfaceIDs=True):
|
||||
def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
|
||||
interface = globals()[f'interface{rxNode}']
|
||||
myNodeNum = globals().get(f'myNodeNum{rxNode}')
|
||||
global telemetryData
|
||||
|
||||
global localTelemetryData
|
||||
|
||||
# throttle the telemetry requests to prevent spamming the device
|
||||
if 1 <= rxNode <= 9:
|
||||
if time.time() - telemetryData[0][f'interface{rxNode}'] < 600 and not userRequested:
|
||||
if time.time() - localTelemetryData[0][f'interface{rxNode}'] < 600 and not userRequested:
|
||||
return -1
|
||||
telemetryData[0][f'interface{rxNode}'] = time.time()
|
||||
localTelemetryData[0][f'interface{rxNode}'] = time.time()
|
||||
|
||||
# some telemetry data is not available in python-meshtastic?
|
||||
# bring in values from the last telemetry dump for the node
|
||||
numPacketsTx = telemetryData[rxNode]['numPacketsTx']
|
||||
numPacketsRx = telemetryData[rxNode]['numPacketsRx']
|
||||
numPacketsTxErr = telemetryData[rxNode]['numPacketsTxErr']
|
||||
numPacketsRxErr = telemetryData[rxNode]['numPacketsRxErr']
|
||||
numTotalNodes = telemetryData[rxNode]['numTotalNodes']
|
||||
totalOnlineNodes = telemetryData[rxNode]['numOnlineNodes']
|
||||
|
||||
numPacketsTx = localTelemetryData[rxNode].get('numPacketsTx', 0)
|
||||
numPacketsRx = localTelemetryData[rxNode].get('numPacketsRx', 0)
|
||||
numPacketsTxErr = localTelemetryData[rxNode].get('numPacketsTxErr', 0)
|
||||
numPacketsRxErr = localTelemetryData[rxNode].get('numPacketsRxErr', 0)
|
||||
numTotalNodes = localTelemetryData[rxNode].get('numTotalNodes', 0)
|
||||
totalOnlineNodes = localTelemetryData[rxNode].get('numOnlineNodes', 0)
|
||||
numRXDupes = localTelemetryData[rxNode].get('numRXDupes', 0)
|
||||
numTxRelays = localTelemetryData[rxNode].get('numTxRelays', 0)
|
||||
heapFreeBytes = localTelemetryData[rxNode].get('heapFreeBytes', 0)
|
||||
heapTotalBytes = localTelemetryData[rxNode].get('heapTotalBytes', 0)
|
||||
# get the telemetry data for a node
|
||||
chutil = round(interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("channelUtilization", 0), 1)
|
||||
airUtilTx = round(interface.nodes.get(decimal_to_hex(myNodeNum), {}).get("deviceMetrics", {}).get("airUtilTx", 0), 1)
|
||||
@@ -1219,6 +1327,18 @@ def displayNodeTelemetry(nodeID=0, rxNode=0, userRequested=False):
|
||||
send_message(f"Low Battery Level: {batteryLevel}{emji} on Device: {rxNode}", {secure_channel}, 0, {secure_interface})
|
||||
elif batteryLevel < 10:
|
||||
logger.critical(f"System: Critical Battery Level: {batteryLevel}{emji} on Device: {rxNode}")
|
||||
|
||||
# if numRXDupes,numTxRelays,heapFreeBytes,heapTotalBytes are available loge them
|
||||
# if numRXDupes != 0:
|
||||
# dataResponse += f" RXDupes:{numRXDupes}"
|
||||
# logger.debug(f"System: Device {rxNode} RX Dupes:{numRXDupes}")
|
||||
# if numTxRelays != 0:
|
||||
# dataResponse += f" TxRelays:{numTxRelays}"
|
||||
# logger.debug(f"System: Device {rxNode} TX Relays:{numTxRelays}")
|
||||
# if heapFreeBytes != 0 and heapTotalBytes != 0:
|
||||
# logger.debug(f"System: Device {rxNode} Heap Memory Free:{heapFreeBytes} Total:{heapTotalBytes}")
|
||||
#dataResponse += f" Heap:{heapFreeBytes}/{heapTotalBytes}"
|
||||
|
||||
return dataResponse
|
||||
|
||||
positionMetadata = {}
|
||||
@@ -1248,7 +1368,7 @@ def initializeMeshLeaderboard():
|
||||
|
||||
initializeMeshLeaderboard()
|
||||
def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
global positionMetadata, telemetryData, meshLeaderboard
|
||||
global positionMetadata, localTelemetryData, meshLeaderboard
|
||||
uptime = battery = temp = iaq = nodeID = 0
|
||||
deviceMetrics, envMetrics, localStats = {}, {}, {}
|
||||
|
||||
@@ -1342,31 +1462,26 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
except Exception as e:
|
||||
logger.debug(f"System: TELEMETRY_APP iaq error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
|
||||
# Collect localStats for telemetryData
|
||||
# Update localStats in telemetryData
|
||||
if telemetry_packet.get('localStats'):
|
||||
localStats = telemetry_packet['localStats']
|
||||
try:
|
||||
# Check if 'numPacketsTx' and 'numPacketsRx' exist and are not zero
|
||||
if localStats.get('numPacketsTx') is not None and localStats.get('numPacketsRx') is not None and localStats['numPacketsTx'] != 0:
|
||||
# Assign the values to the telemetry dictionary
|
||||
keys = [
|
||||
'numPacketsTx', 'numPacketsRx', 'numOnlineNodes',
|
||||
'numOfflineNodes', 'numPacketsTxErr', 'numPacketsRxErr', 'numTotalNodes']
|
||||
for key in keys:
|
||||
if localStats.get(key) is not None:
|
||||
telemetryData[rxNode][key] = localStats.get(key)
|
||||
# Only store keys where value is not 0
|
||||
filtered_stats = {k: v for k, v in localStats.items() if v != 0}
|
||||
localTelemetryData[rxNode].update(filtered_stats)
|
||||
except Exception as e:
|
||||
logger.debug(f"System: TELEMETRY_APP localStats error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
|
||||
#POSITION_APP packets
|
||||
if packet_type == 'POSITION_APP':
|
||||
try:
|
||||
if debugMetadata and 'POSITION_APP' not in metadataFilter:
|
||||
print(f"DEBUG POSITION_APP: {packet}\n\n")
|
||||
keys = ['altitude', 'groundSpeed', 'precisionBits']
|
||||
position_stats_keys = ['altitude', 'groundSpeed', 'precisionBits']
|
||||
position_data = packet['decoded']['position']
|
||||
if nodeID not in positionMetadata:
|
||||
positionMetadata[nodeID] = {}
|
||||
for key in keys:
|
||||
for key in position_stats_keys:
|
||||
positionMetadata[nodeID][key] = position_data.get(key, 0)
|
||||
# Track fastest speed 🚓
|
||||
if position_data.get('groundSpeed') is not None:
|
||||
@@ -1386,7 +1501,7 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
if logMetaStats:
|
||||
logger.info(f"System: 🚀 New altitude record: {altitude}m from NodeID:{nodeID} ShortName:{get_name_from_number(nodeID, 'short', rxNode)}")
|
||||
# if altitude is over highfly_altitude send a log and message for high-flying nodes and not in highfly_ignoreList
|
||||
if position_data.get('altitude', 0) > highfly_altitude and highfly_enabled and str(nodeID) not in highfly_ignoreList:
|
||||
if position_data.get('altitude', 0) > highfly_altitude and highfly_enabled and str(nodeID) not in highfly_ignoreList and not isNodeBanned(nodeID):
|
||||
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"
|
||||
@@ -1394,12 +1509,21 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
# check get_openskynetwork to see if the node is an aircraft
|
||||
if 'latitude' in position_data and 'longitude' in position_data:
|
||||
flight_info = get_openskynetwork(position_data.get('latitude', 0), position_data.get('longitude', 0))
|
||||
if flight_info and NO_ALERTS not in flight_info and ERROR_FETCHING_DATA not in flight_info:
|
||||
msg += f"\n✈️Detected near:\n{flight_info}"
|
||||
# Only show plane if within altitude
|
||||
if (
|
||||
flight_info
|
||||
and NO_ALERTS not in flight_info
|
||||
and ERROR_FETCHING_DATA not in flight_info
|
||||
and isinstance(flight_info, dict)
|
||||
and 'altitude' in flight_info
|
||||
):
|
||||
plane_alt = flight_info['altitude']
|
||||
node_alt = position_data.get('altitude', 0)
|
||||
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)
|
||||
time.sleep(responseDelay)
|
||||
# Keep the positionMetadata dictionary at a maximum size of 20
|
||||
if len(positionMetadata) > 20:
|
||||
# Keep the positionMetadata dictionary at a maximum size
|
||||
if len(positionMetadata) > MAX_SEEN_NODES:
|
||||
# Remove the oldest entry
|
||||
oldest_nodeID = next(iter(positionMetadata))
|
||||
del positionMetadata[oldest_nodeID]
|
||||
@@ -1468,7 +1592,6 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
logger.info(f"System: Detection Sensor Data from Device: {rxNode} Channel: {channel} NodeID:{nodeID} Text:{detction_text}")
|
||||
if detctionSensorAlert:
|
||||
send_message(f"🚨Detection Sensor from Device: {rxNode} Channel: {channel} NodeID:{get_name_from_number(nodeID,'long',rxNode)} Alert:{detction_text}", secure_channel, 0, secure_interface)
|
||||
time.sleep(responseDelay)
|
||||
except Exception as e:
|
||||
logger.debug(f"System: DETECTION_SENSOR_APP decode error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
|
||||
@@ -1545,6 +1668,22 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
|
||||
# COMPRESSED_TEXT_APP
|
||||
|
||||
# ATTAK_APP
|
||||
|
||||
# SERIAL_APP
|
||||
|
||||
# NODE_DB_APP
|
||||
|
||||
# RTTTL_APP
|
||||
|
||||
# STORE_AND_FORWARD_APP
|
||||
|
||||
# DEBUG_APP
|
||||
|
||||
# RANGEREPORT_APP
|
||||
|
||||
# CENSUS_APP
|
||||
|
||||
# AUDIO_APP - Track audio/voice packets ☎️
|
||||
if packet_type == 'AUDIO_APP':
|
||||
try:
|
||||
@@ -1740,7 +1879,7 @@ def get_sysinfo(nodeID=0, deviceID=1):
|
||||
# Get the system telemetry data for return on the sysinfo command
|
||||
sysinfo = ''
|
||||
stats = str(displayNodeTelemetry(nodeID, deviceID, userRequested=True)) + " 🤖👀" + str(len(seenNodes))
|
||||
if "numPacketsRx:0" in stats or stats == -1:
|
||||
if "numPacketsTx:0" in stats or stats == -1:
|
||||
return "Gathering Telemetry try again later⏳"
|
||||
# replace Telemetry with Int in string
|
||||
stats = stats.replace("Telemetry", "Int")
|
||||
@@ -1769,13 +1908,11 @@ async def handleSignalWatcher():
|
||||
for ch in sigWatchBroadcastCh:
|
||||
if antiSpam and ch != publicChannel:
|
||||
send_message(msg, int(ch), 0, sigWatchBroadcastInterface)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
|
||||
else:
|
||||
if antiSpam and sigWatchBroadcastCh != publicChannel:
|
||||
send_message(msg, int(sigWatchBroadcastCh), 0, sigWatchBroadcastInterface)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
logger.warning(f"System: antiSpam prevented Alert from Hamlib {msg}")
|
||||
|
||||
@@ -1798,23 +1935,19 @@ async def handleFileWatcher():
|
||||
for ch in file_monitor_broadcastCh:
|
||||
if antiSpam and int(ch) != publicChannel:
|
||||
send_message(msg, int(ch), 0, 1)
|
||||
time.sleep(responseDelay)
|
||||
if multiple_interface:
|
||||
for i in range(2, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
send_message(msg, int(ch), 0, i)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
logger.warning(f"System: antiSpam prevented Alert from FileWatcher")
|
||||
else:
|
||||
if antiSpam and file_monitor_broadcastCh != publicChannel:
|
||||
send_message(msg, int(file_monitor_broadcastCh), 0, 1)
|
||||
time.sleep(responseDelay)
|
||||
if multiple_interface:
|
||||
for i in range(2, 10):
|
||||
if globals().get(f'interface{i}_enabled'):
|
||||
send_message(msg, int(file_monitor_broadcastCh), 0, i)
|
||||
time.sleep(responseDelay)
|
||||
else:
|
||||
logger.warning(f"System: antiSpam prevented Alert from FileWatcher")
|
||||
|
||||
@@ -1874,7 +2007,7 @@ async def handleSentinel(deviceID):
|
||||
global handleSentinel_spotted, handleSentinel_loop
|
||||
detectedNearby = ""
|
||||
resolution = "unknown"
|
||||
closest_nodes = get_closest_nodes(deviceID)
|
||||
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
|
||||
|
||||
@@ -1904,7 +2037,6 @@ async def handleSentinel(deviceID):
|
||||
|
||||
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)
|
||||
time.sleep(responseDelay + 1)
|
||||
if enableSMTP and email_sentry_alerts:
|
||||
for email in sysopEmails:
|
||||
send_email(email, f"Sentry{deviceID}: {detectedNearby}")
|
||||
@@ -1925,10 +2057,9 @@ async def process_vox_queue():
|
||||
for channel in sigWatchBroadcastCh:
|
||||
if antiSpam and int(channel) != publicChannel:
|
||||
send_message(message, int(channel), 0, sigWatchBroadcastInterface)
|
||||
time.sleep(responseDelay)
|
||||
|
||||
async def watchdog():
|
||||
global telemetryData, retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
|
||||
global localTelemetryData, retry_int1, retry_int2, retry_int3, retry_int4, retry_int5, retry_int6, retry_int7, retry_int8, retry_int9
|
||||
logger.debug("System: Watchdog started")
|
||||
while True:
|
||||
await asyncio.sleep(20)
|
||||
@@ -1941,14 +2072,15 @@ async def watchdog():
|
||||
for i in range(1, 10):
|
||||
interface = globals().get(f'interface{i}')
|
||||
retry_int = globals().get(f'retry_int{i}')
|
||||
if interface is not None and not retry_int and globals().get(f'interface{i}_enabled'):
|
||||
int_enabled = globals().get(f'interface{i}_enabled')
|
||||
if interface is not None and not retry_int and int_enabled:
|
||||
try:
|
||||
firmware = getNodeFirmware(0, i)
|
||||
except Exception as e:
|
||||
logger.error(f"System: communicating with interface{i}, trying to reconnect: {e}")
|
||||
globals()[f'retry_int{i}'] = True
|
||||
|
||||
if not globals()[f'retry_int{i}']:
|
||||
if not retry_int and int_enabled:
|
||||
if sentry_enabled:
|
||||
await handleSentinel(i)
|
||||
|
||||
@@ -1958,11 +2090,11 @@ async def watchdog():
|
||||
handleAlertBroadcast(i)
|
||||
|
||||
intData = displayNodeTelemetry(0, i)
|
||||
if intData != -1 and telemetryData[0][f'lastAlert{i}'] != intData:
|
||||
if intData != -1 and localTelemetryData[0][f'lastAlert{i}'] != intData:
|
||||
logger.debug(intData + f" Firmware:{firmware}")
|
||||
telemetryData[0][f'lastAlert{i}'] = intData
|
||||
localTelemetryData[0][f'lastAlert{i}'] = intData
|
||||
|
||||
if globals()[f'retry_int{i}'] and globals()[f'interface{i}_enabled']:
|
||||
if retry_int and int_enabled:
|
||||
try:
|
||||
await retry_interface(i)
|
||||
except Exception as e:
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# UDP Interface Listener
|
||||
# credit to pdxlocations for all of this core work https://github.com/pdxlocations/
|
||||
# depends on: pip install meshtastic protobuf zeroconf pubsub
|
||||
# 2025 Kelly Keeton K7MHI
|
||||
from pubsub import pub
|
||||
from meshtastic.protobuf import mesh_pb2, portnums_pb2
|
||||
from mudp import UDPPacketStream, node, conn, send_text_message, send_nodeinfo, send_device_telemetry, send_position, send_environment_metrics, send_power_metrics, send_waypoint, send_data
|
||||
from mudp.encryption import generate_hash
|
||||
import time
|
||||
from zeroconf import Zeroconf, ServiceBrowser
|
||||
import socket
|
||||
|
||||
MCAST_GRP, MCAST_PORT, CHANNEL_ID, KEY = "224.0.0.69", 4403, "LongFast", "1PG7OiApB1nwvP+rz05pAQ=="
|
||||
PUBLIC_CHANNEL_IDS = ["LongFast", "ShortSlow", "MediumFast", "MediumSlow", "ShortFast", "ShortTurbo"]
|
||||
mudpEnabled, mudpInterface = True, None
|
||||
messages = []
|
||||
|
||||
class ZeroconfListner:
|
||||
def add_service(self, zeroconf, type, name):
|
||||
info = zeroconf.get_service_info(type, name)
|
||||
if info:
|
||||
txt = info.properties
|
||||
ip = None
|
||||
if info.addresses:
|
||||
ip = socket.inet_ntoa(info.addresses[0])
|
||||
print(f"Found Meshtastic node: id={txt.get(b'id', b'').decode()} shortname={txt.get(b'shortname', b'').decode()} longname={txt.get(b'longname', b'').decode()} ip={ip}")
|
||||
|
||||
def update_service(self, zeroconf, type, name):
|
||||
# This method is required by zeroconf, but you can leave it empty if you don't need updates.
|
||||
pass
|
||||
|
||||
def initalize_mudp():
|
||||
global mudpInterface
|
||||
if mudpEnabled and mudpInterface is None:
|
||||
mudpInterface = UDPPacketStream(MCAST_GRP, MCAST_PORT, key=KEY)
|
||||
print(f"MUDP Interface initialized with multicast group", MCAST_GRP, "port", MCAST_PORT)
|
||||
node.node_id, node.long_name, node.short_name = "!deadbeef", "UDP Test", "UDP"
|
||||
node.channel, node.key = "LongFast", KEY
|
||||
conn.setup_multicast(MCAST_GRP, MCAST_PORT)
|
||||
|
||||
def on_recieve(packet: mesh_pb2.MeshPacket, addr=None):
|
||||
print(f"\n[RECV] Packet received from {addr}")
|
||||
print("from:", getattr(packet, "from", None))
|
||||
print("to:", packet.to)
|
||||
|
||||
# Check against all public channels
|
||||
matched_channel = None
|
||||
for channel_name in PUBLIC_CHANNEL_IDS:
|
||||
channel_hash = generate_hash(channel_name, KEY)
|
||||
if packet.channel == channel_hash:
|
||||
matched_channel = channel_name
|
||||
break
|
||||
|
||||
if matched_channel:
|
||||
channel_status = f"Match ({matched_channel})"
|
||||
else:
|
||||
channel_status = f"Hash: {packet.channel}"
|
||||
|
||||
print("channel:", channel_status)
|
||||
|
||||
if packet.HasField("decoded"):
|
||||
port_name = portnums_pb2.PortNum.Name(packet.decoded.portnum) if packet.decoded.portnum else "N/A"
|
||||
try:
|
||||
payload_decoded = True
|
||||
packet_payload = packet.decoded.payload.decode("utf-8", "ignore")
|
||||
except Exception:
|
||||
print(" payload (raw bytes):", packet.decoded.payload)
|
||||
else:
|
||||
print(f"encrypted: { {packet.encrypted} }")
|
||||
|
||||
|
||||
print("id:", packet.id or None)
|
||||
print("rx_time:", packet.rx_time or None)
|
||||
print("rx_snr:", packet.rx_snr or None)
|
||||
print("hop_limit:", packet.hop_limit or None)
|
||||
priority_name = mesh_pb2.MeshPacket.Priority.Name(packet.priority) if packet.priority else "N/A"
|
||||
print("priority:", priority_name or None)
|
||||
print("rx_rssi:", packet.rx_rssi or None)
|
||||
print("hop_start:", packet.hop_start or None)
|
||||
print("next_hop:", packet.next_hop or None)
|
||||
print("relay_node:", packet.relay_node or None)
|
||||
|
||||
print(f"decoded {{portnum: {port_name}, payload: {packet_payload if payload_decoded else 'N/A'}, bitfield: {packet.decoded.bitfield or None}}}" if packet.HasField("decoded") else "No decoded field")
|
||||
|
||||
pub.subscribe(on_recieve, "mesh.rx.packet")
|
||||
# pub.subscribe(on_text_message, "mesh.rx.port.1")
|
||||
# pub.subscribe(on_nodeinfo, "mesh.rx.port.4") # NODEINFO_APP
|
||||
|
||||
zeroconf = Zeroconf()
|
||||
listener = ZeroconfListner()
|
||||
browser = ServiceBrowser(zeroconf, "_meshtastic._tcp.local.", listener)
|
||||
|
||||
def main():
|
||||
initalize_mudp()
|
||||
mudpInterface.start()
|
||||
try:
|
||||
while True: time.sleep(0.05)
|
||||
except KeyboardInterrupt: pass
|
||||
finally: mudpInterface.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
# Meshtastic Port Numbers Reference:
|
||||
# | Port Number | Name | Purpose |
|
||||
# |-------------|------------------------|--------------------------------|
|
||||
# | 1 | TEXT_MESSAGE_APP | Text messages |
|
||||
# | 2 | POSITION_APP | Position updates (GPS) |
|
||||
# | 3 | ROUTING_APP | Routing info |
|
||||
# | 4 | NODEINFO_APP | Node info (name, id, etc) |
|
||||
# | 5 | TELEMETRY_APP | Telemetry (battery, sensors) |
|
||||
# | 6 | SERIAL_APP | Serial data |
|
||||
# | 7 | ENVIRONMENTAL_APP | Environmental sensors |
|
||||
# | 8 | REMOTE_HARDWARE_APP | Remote hardware control |
|
||||
# | 9 | STORE_FORWARD_APP | Store and forward |
|
||||
# | 10 | RANGE_TEST_APP | Range test |
|
||||
# | 11 | ADMIN_APP | Admin/config |
|
||||
# | 12 | WAYPOINT_APP | Waypoints |
|
||||
# | 13 | CHANNEL_NODEINFO_APP | Channel node info |
|
||||
# | 256 | PRIVATE_APP | Private app (custom use) |
|
||||
# See: https://github.com/meshtastic/protobufs/blob/main/meshtastic/protobuf/portnums.proto
|
||||
|
||||
|
||||
+57
-33
@@ -267,7 +267,23 @@ def onReceive(packet, interface):
|
||||
|
||||
# check if the packet has a channel flag use it
|
||||
if packet.get('channel'):
|
||||
channel_number = packet.get('channel', 0)
|
||||
channel_number = packet.get('channel')
|
||||
channel_name = "unknown"
|
||||
# get channel hashes for the interface
|
||||
device = next((d for d in channel_list if d["interface_id"] == rxNode), None)
|
||||
if device:
|
||||
# Find the channel name whose hash matches channel_number
|
||||
for chan_name, info in device['channels'].items():
|
||||
if info['hash'] == channel_number:
|
||||
print(f"Matched channel hash {info['hash']} to channel name {chan_name}")
|
||||
channel_name = chan_name
|
||||
break
|
||||
|
||||
# check if the packet has a simulator flag
|
||||
simulator_flag = packet.get('decoded', {}).get('simulator', False)
|
||||
if isinstance(simulator_flag, dict):
|
||||
# assume Software Simulator
|
||||
simulator_flag = True
|
||||
|
||||
# set the message_from_id
|
||||
message_from_id = packet['from']
|
||||
@@ -282,6 +298,7 @@ def onReceive(packet, interface):
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
via_mqtt = packet['decoded'].get('viaMqtt', False)
|
||||
transport_mechanism = packet['decoded'].get('transport_mechanism', 'unknown')
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id == myNodeNum1 or message_from_id == myNodeNum2:
|
||||
@@ -294,46 +311,54 @@ def onReceive(packet, interface):
|
||||
|
||||
# check if the packet has a publicKey flag use it
|
||||
if packet.get('publicKey'):
|
||||
pkiStatus = (packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC'))
|
||||
pkiStatus = packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC')
|
||||
|
||||
# check if the packet has replyId flag // currently unused in the code
|
||||
if packet.get('replyId'):
|
||||
replyIDset = packet.get('replyId', False)
|
||||
|
||||
# check if the packet has emoji flag set it // currently unused in the code
|
||||
if packet.get('emoji'):
|
||||
emojiSeen = packet.get('emoji', False)
|
||||
|
||||
# check if the packet has a hop count flag use it
|
||||
if packet.get('hopsAway'):
|
||||
hop_away = packet.get('hopsAway', 0)
|
||||
else:
|
||||
# if the packet does not have a hop count try other methods
|
||||
if packet.get('hopLimit'):
|
||||
hop_limit = packet.get('hopLimit', 0)
|
||||
else:
|
||||
hop_limit = 0
|
||||
|
||||
if packet.get('hopStart'):
|
||||
hop_start = packet.get('hopStart', 0)
|
||||
else:
|
||||
hop_start = 0
|
||||
|
||||
if enableHopLogs:
|
||||
logger.debug(f"System: Packet HopDebugger: hop_away:{hop_away} hop_limit:{hop_limit} hop_start:{hop_start}")
|
||||
if packet.get('hopStart'):
|
||||
hop_start = packet.get('hopStart', 0)
|
||||
|
||||
if packet.get('hopLimit'):
|
||||
hop_limit = packet.get('hopLimit', 0)
|
||||
|
||||
# calculate hop count
|
||||
hop = ""
|
||||
if hop_limit > 0 and hop_start >= hop_limit:
|
||||
hop_count = hop_away + (hop_start - hop_limit)
|
||||
elif hop_limit > 0 and hop_start < hop_limit:
|
||||
hop_count = hop_away + (hop_limit - hop_start)
|
||||
else:
|
||||
hop_count = hop_away
|
||||
|
||||
if hop_away == 0 and hop_limit == 0 and hop_start == 0:
|
||||
hop = "Last Hop"
|
||||
hop_count = 0
|
||||
|
||||
if hop_start == hop_limit:
|
||||
hop = "Direct"
|
||||
hop_count = 0
|
||||
elif hop_start == 0 and hop_limit > 0 or via_mqtt:
|
||||
hop = "MQTT"
|
||||
hop_count = 0
|
||||
else:
|
||||
# set hop to Direct if the message was sent directly otherwise set the hop count
|
||||
if hop_away > 0:
|
||||
hop_count = hop_away
|
||||
else:
|
||||
hop_count = hop_start - hop_limit
|
||||
#print (f"calculated hop count: {hop_start} - {hop_limit} = {hop_count}")
|
||||
|
||||
hop = f"{hop_count} hops"
|
||||
|
||||
if hop_start == hop_limit and "lora" in str(transport_mechanism).lower():
|
||||
hop = "Direct"
|
||||
|
||||
if ((hop_start == 0 and hop_limit >= 0) or via_mqtt or ("mqtt" in str(transport_mechanism).lower())):
|
||||
hop = "MQTT"
|
||||
|
||||
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}")
|
||||
|
||||
# check with stringSafeChecker if the message is safe
|
||||
if stringSafeCheck(message_string) is False:
|
||||
logger.warning(f"System: Possibly Unsafe Message from {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
|
||||
if help_message in message_string or welcome_message in message_string or "CMD?:" in message_string:
|
||||
# ignore help and welcome messages
|
||||
logger.warning(f"Got Own Welcome/Help header. From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
@@ -353,7 +378,6 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
send_message(welcome_message, channel_number, message_from_id, rxNode)
|
||||
time.sleep(responseDelay)
|
||||
|
||||
# log the message to the message log
|
||||
if log_messages_to_file:
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# Configuration Merge Script
|
||||
# Merges user configuration with default settings
|
||||
# 2025 Kelly Keeton K7MHI mesh-around and its meshtastic
|
||||
import shutil
|
||||
import configparser
|
||||
import os
|
||||
|
||||
|
||||
def merge_configs(default_config_path, user_config_path, output_config_path):
|
||||
# Load default configuration (INI)
|
||||
default_config = configparser.ConfigParser()
|
||||
default_config.read(default_config_path)
|
||||
|
||||
# Load user configuration (INI)
|
||||
user_config = configparser.ConfigParser()
|
||||
user_config.read(user_config_path)
|
||||
|
||||
# Merge configurations
|
||||
for section in user_config.sections():
|
||||
if not default_config.has_section(section):
|
||||
default_config.add_section(section)
|
||||
for key, value in user_config.items(section):
|
||||
default_config.set(section, key, value)
|
||||
|
||||
# Save merged configuration as INI
|
||||
with open(output_config_path, 'w', encoding='utf-8') as f:
|
||||
default_config.write(f)
|
||||
|
||||
def backup_config(config_path, backup_path):
|
||||
shutil.copyfile(config_path, backup_path)
|
||||
|
||||
def show_config_changes(user_config_path, merged_config_path):
|
||||
if not os.path.exists(merged_config_path) or os.path.getsize(merged_config_path) == 0:
|
||||
print(f"Error: {merged_config_path} is empty or missing!")
|
||||
return
|
||||
|
||||
# Load user config (as dict)
|
||||
user_config = configparser.ConfigParser()
|
||||
user_config.read(user_config_path)
|
||||
user_dict = {s: dict(user_config.items(s)) for s in user_config.sections()}
|
||||
|
||||
# Load merged config (as dict)
|
||||
merged_config = configparser.ConfigParser()
|
||||
merged_config.read(merged_config_path)
|
||||
merged_dict = {s: dict(merged_config.items(s)) for s in merged_config.sections()}
|
||||
|
||||
print("\n--- Changes in merged configuration ---")
|
||||
for section in merged_dict:
|
||||
if section not in user_dict:
|
||||
print(f"[{section}] (new section)")
|
||||
for k, v in merged_dict[section].items():
|
||||
print(f" {k} = {v} (added)")
|
||||
else:
|
||||
for k, v in merged_dict[section].items():
|
||||
if k not in user_dict[section]:
|
||||
print(f"[{section}] {k} = {v} (added)")
|
||||
elif user_dict[section][k] != v:
|
||||
print(f"[{section}] {k}: {user_dict[section][k]} -> {v} (changed)")
|
||||
print("--- End of changes ---\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("MESHING-AROUND: Configuration Merge Script for config.ini checking updates from config.template")
|
||||
print("---------------------------------------------------------------")
|
||||
master_config_path = 'config.template'
|
||||
user_config_path = 'config.ini'
|
||||
output_config = 'config_new.ini'
|
||||
backup_config_path = 'config.bak'
|
||||
|
||||
# Step 1: Check master config
|
||||
try:
|
||||
if not os.path.exists(master_config_path) or os.path.getsize(master_config_path) == 0:
|
||||
raise FileNotFoundError(f"Master configuration file {master_config_path} is missing or empty.")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
print("Run the tool from the meshing-around/script/ directory where the config.template is located.")
|
||||
print(" python3 script/configMerge.py")
|
||||
exit(1)
|
||||
|
||||
# Step 2: Backup user config
|
||||
try:
|
||||
backup_config(user_config_path, backup_config_path)
|
||||
print(f"Backup of user config created at {backup_config_path}")
|
||||
except Exception as e:
|
||||
print(f"Error backing up user config: {e}")
|
||||
exit(1)
|
||||
|
||||
# Step 3: Merge configs
|
||||
try:
|
||||
merge_configs(master_config_path, user_config_path, output_config)
|
||||
print(f"Merged configuration saved to {output_config}")
|
||||
except Exception as e:
|
||||
print(f"Error merging configuration: {e}")
|
||||
exit(1)
|
||||
|
||||
# Step 4: Show changes
|
||||
try:
|
||||
show_config_changes(user_config_path, output_config)
|
||||
print("Please review the new configuration and replace your existing config.ini if needed.")
|
||||
print(" cp config_new.ini config.ini")
|
||||
except Exception as e:
|
||||
print(f"Error showing configuration changes: {e}")
|
||||
exit(1)
|
||||
@@ -24,6 +24,13 @@ if systemctl is-active --quiet mesh_bot_w3.service; then
|
||||
service_stopped=true
|
||||
fi
|
||||
|
||||
# Fetch latest changes from GitHub
|
||||
echo "Fetching latest changes from GitHub..."
|
||||
if ! git fetch origin; then
|
||||
echo "Error: Failed to fetch from GitHub, check your network connection."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# git pull with rebase to avoid unnecessary merge commits
|
||||
echo "Pulling latest changes from GitHub..."
|
||||
if ! git pull origin main --rebase; then
|
||||
@@ -37,22 +44,31 @@ if ! git pull origin main --rebase; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install or update dependencies
|
||||
echo "Installing or updating dependencies..."
|
||||
if pip install -r requirements.txt --upgrade 2>&1 | grep -q "externally-managed-environment"; then
|
||||
# if venv is found ask to run with launch.sh
|
||||
if [ -d "venv" ]; then
|
||||
echo "A virtual environment (venv) was found. run from inside venv"
|
||||
# Backup the data/ directory
|
||||
echo "Backing up data/ directory..."
|
||||
#backup_file="backup_$(date +%Y%m%d_%H%M%S).tar.gz"
|
||||
backup_file="data_backup.tar.gz"
|
||||
path2backup="data/"
|
||||
tar -czf "$backup_file" "$path2backup"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Backup failed."
|
||||
else
|
||||
echo "Backup of ${path2backup} completed: ${backup_file}"
|
||||
fi
|
||||
|
||||
|
||||
# Build a config_new.ini file merging user config with new defaults
|
||||
echo "Merging configuration files..."
|
||||
python3 script/configMerge.py > ini_merge_log.txt 2>&1
|
||||
|
||||
if [ -f ini_merge_log.txt ]; then
|
||||
if grep -q "Error during configuration merge" ini_merge_log.txt; then
|
||||
echo "Configuration merge encountered errors. Please check ini_merge_log.txt for details."
|
||||
else
|
||||
read -p "Warning: You are in an externally managed environment. Do you want to continue with --break-system-packages? (y/n): " choice
|
||||
if [[ "$choice" == "y" || "$choice" == "Y" ]]; then
|
||||
pip install --break-system-packages -r requirements.txt --upgrade
|
||||
else
|
||||
echo "Update aborted due to dependency installation issue."
|
||||
fi
|
||||
echo "Configuration merge completed. Please review config_new.ini and ini_merge_log.txt."
|
||||
fi
|
||||
else
|
||||
echo "Dependencies installed or updated."
|
||||
echo "Configuration merge log (ini_merge_log.txt) not found. check out the script/configMerge.py tool!"
|
||||
fi
|
||||
|
||||
# if service was stopped earlier, restart it
|
||||
|
||||
Reference in New Issue
Block a user