Compare commits

..

112 Commits

Author SHA1 Message Date
SpudGunMan 03895248cd fixing
sorry if you saw the crashing I had dinner
2025-10-19 18:31:51 -07:00
SpudGunMan a79de8a325 cleanupBadCode 2025-10-19 17:16:53 -07:00
SpudGunMan 740b53f02f Update system.py 2025-10-19 16:25:35 -07:00
SpudGunMan 76e75551c6 Update videopoker.py 2025-10-19 16:15:49 -07:00
SpudGunMan 51752ae896 Update videopoker.py 2025-10-19 16:15:37 -07:00
SpudGunMan d81e773c0c Update blackjack.py 2025-10-19 16:02:24 -07:00
SpudGunMan 1f1ed1ca70 Update blackjack.py 2025-10-19 16:01:04 -07:00
SpudGunMan 081ccd9e2e Update blackjack.py 2025-10-19 15:55:46 -07:00
SpudGunMan d9a7dafe6e Update blackjack.py 2025-10-19 15:54:41 -07:00
SpudGunMan 921225965b Update blackjack.py 2025-10-19 15:52:20 -07:00
SpudGunMan 3659254785 refactor suggestion 2025-10-19 15:42:49 -07:00
SpudGunMan 7c502608f6 remove deps install 2025-10-19 14:51:51 -07:00
SpudGunMan 427c25f80b noHoldsHeld 2025-10-19 14:26:45 -07:00
SpudGunMan c3f15390ea Update system.py 2025-10-19 14:08:21 -07:00
SpudGunMan e1476a44c6 enhance 2025-10-19 13:46:58 -07:00
SpudGunMan 72070fef3e backup data 2025-10-19 13:43:57 -07:00
SpudGunMan b63ea677f6 Update blackjack.py 2025-10-19 13:35:00 -07:00
SpudGunMan f8389500b8 Update mesh_bot.py 2025-10-19 13:28:32 -07:00
SpudGunMan b257625a45 cleanupBlackJack 2025-10-19 13:23:40 -07:00
SpudGunMan a233d8c7b3 Update mesh_bot.py 2025-10-19 12:57:37 -07:00
SpudGunMan 11c9742ebe cleanup 2025-10-19 12:55:16 -07:00
SpudGunMan 5af28c3dc2 Update system.py
kidding
2025-10-19 12:55:08 -07:00
SpudGunMan aebb9e3c20 cleanup 2025-10-19 12:48:19 -07:00
SpudGunMan d5916f4ccc 🐞🧩
thanks meshguy
2025-10-19 12:22:09 -07:00
SpudGunMan 056159a3f3 Update mesh_bot.py 2025-10-19 09:21:25 -07:00
SpudGunMan 2f6049d94b bugfix survey game 2025-10-18 19:20:36 -07:00
SpudGunMan a2d7f664ab Update udp.py 2025-10-18 17:29:21 -07:00
SpudGunMan b26491b646 Update udp.py 2025-10-18 17:26:39 -07:00
SpudGunMan 22e97b0eec Update udp.py 2025-10-18 16:54:26 -07:00
SpudGunMan f540866d08 Update locationdata.py 2025-10-18 15:18:18 -07:00
SpudGunMan c9729c8214 Update locationdata.py 2025-10-18 15:17:34 -07:00
SpudGunMan 49901cbbee Update locationdata.py 2025-10-18 15:14:23 -07:00
SpudGunMan 2aa2b80935 Update locationdata.py 2025-10-18 15:08:40 -07:00
SpudGunMan 95695f4f58 Update locationdata.py 2025-10-18 15:02:33 -07:00
SpudGunMan b641d2b5e8 ok finslly
this looks better
2025-10-18 14:54:45 -07:00
SpudGunMan 51d8faab12 enhance 2025-10-18 14:49:26 -07:00
SpudGunMan 7a1396b99d Update locationdata.py 2025-10-18 14:40:39 -07:00
SpudGunMan 819bbbcaf4 enhance 2025-10-18 14:39:06 -07:00
SpudGunMan 0eeda96670 Update locationdata.py 2025-10-18 14:36:44 -07:00
SpudGunMan 18cca4ffdd Update locationdata.py 2025-10-18 14:35:28 -07:00
SpudGunMan d169fe2dff Update locationdata.py 2025-10-18 14:33:42 -07:00
SpudGunMan 1c732dfe17 Update install.sh 2025-10-18 12:47:17 -07:00
SpudGunMan bdad3927e5 enhance 2025-10-18 10:07:09 -07:00
SpudGunMan 0e0d6416d9 enhance config merge data 2025-10-18 09:38:00 -07:00
SpudGunMan 0da780371a enhance 2025-10-18 09:10:47 -07:00
SpudGunMan 37bf30cbc0 enhance 2025-10-18 09:05:17 -07:00
SpudGunMan 817a8601dd Update system.py 2025-10-18 08:53:30 -07:00
SpudGunMan 47cca409be lab work 2025-10-18 08:52:32 -07:00
SpudGunMan e08a82ec39 Update system.py 2025-10-18 08:42:48 -07:00
SpudGunMan 345541dfb5 Update system.py 2025-10-18 08:41:22 -07:00
SpudGunMan 6e89762f1d bbsCompression
not enabled yet
2025-10-18 08:39:43 -07:00
SpudGunMan 0fb26bc16a Update mesh_bot.py 2025-10-17 19:50:47 -07:00
SpudGunMan f1ad5966af send_raw_bytes 2025-10-17 19:50:41 -07:00
SpudGunMan ac57d4683f Update udp.py 2025-10-17 17:48:24 -07:00
SpudGunMan eab099e5ee channelID 2025-10-17 17:42:07 -07:00
SpudGunMan 685bd3491d Update udp.py 2025-10-17 17:10:44 -07:00
SpudGunMan b8d64f3a9e Update system.py 2025-10-17 13:31:47 -07:00
SpudGunMan 852d491030 Update meshview.ino 2025-10-16 18:57:17 -07:00
SpudGunMan 76565c5546 Update meshview.ino 2025-10-16 18:55:03 -07:00
SpudGunMan af1ec1630e Update udp.py 2025-10-16 16:04:06 -07:00
SpudGunMan 0c2b36a206 refactor handle_messages
@mesb1 give this one a test

https://github.com/SpudGunMan/meshing-around/issues/213
2025-10-16 15:55:12 -07:00
SpudGunMan c0934096f0 Update meshview.ino 2025-10-16 12:07:58 -07:00
SpudGunMan 819bfaba90 Update meshview.ino 2025-10-16 11:52:03 -07:00
SpudGunMan 8041a1296b tinkering
@martinbogo
2025-10-15 20:32:46 -07:00
SpudGunMan 10d93b4fd3 keyFactor 2025-10-15 20:17:44 -07:00
SpudGunMan 19dedef1e6 meshview.ino
I could use help with this I am stuck at the moment
2025-10-15 19:25:21 -07:00
SpudGunMan d4af0c7e8b Update udp.py 2025-10-15 15:58:02 -07:00
SpudGunMan 8730f0fd38 Update udp.py 2025-10-15 15:57:40 -07:00
SpudGunMan 9cda8daf65 Update udp.py 2025-10-15 15:57:24 -07:00
SpudGunMan a9223f1613 Create udp.py 2025-10-15 15:51:30 -07:00
SpudGunMan 04ca4c99b8 Update scheduler.py
sorry for that
2025-10-15 08:24:02 -07:00
SpudGunMan 3072520e63 Merge branch 'main' of https://github.com/SpudGunMan/meshing-around 2025-10-15 08:23:16 -07:00
SpudGunMan bd6603766b Update scheduler.py 2025-10-15 08:23:14 -07:00
Kelly 075a23bd2b LowerBits
https://github.com/SpudGunMan/meshing-around/issues/213
2025-10-14 22:21:21 -07:00
SpudGunMan a8e4f653ed Update radio.py 2025-10-14 21:19:00 -07:00
SpudGunMan 374a44f4a9 Update radio.py 2025-10-14 21:17:36 -07:00
SpudGunMan 3c8d2e646e Update radio.py 2025-10-14 16:32:01 -07:00
SpudGunMan e5df983244 Update mesh_bot.py 2025-10-14 16:23:27 -07:00
SpudGunMan fa5f9250c4 Update llm.py 2025-10-14 16:14:59 -07:00
SpudGunMan 3f7a831690 Update llm.py 2025-10-14 16:12:14 -07:00
SpudGunMan 89aaaddae9 Update llm.py 2025-10-14 16:02:40 -07:00
SpudGunMan e1919616c2 refactoring 2025-10-14 15:55:59 -07:00
SpudGunMan 8b9e637006 Update README.md 2025-10-14 15:08:07 -07:00
SpudGunMan 0df3e32901 Update README.md 2025-10-14 15:07:18 -07:00
SpudGunMan 1c2fa174ea Hey Chirpy
- **Voice/Command Triggers**: The following keywords can be used in messages or via voice (VOX) to trigger bot functions:
  - `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
2025-10-14 15:05:39 -07:00
SpudGunMan c97aefcef1 Update radio.py 2025-10-14 14:59:10 -07:00
SpudGunMan dfb94c3993 voxUse 2025-10-14 14:56:45 -07:00
SpudGunMan 7d62f69f12 ... 2025-10-14 14:24:42 -07:00
SpudGunMan cf896767fb Update mesh_bot.py 2025-10-14 13:41:31 -07:00
SpudGunMan 1eb4cf71ed Update mesh_bot.py 2025-10-14 13:40:27 -07:00
SpudGunMan e959124eac voxEnhance 2025-10-14 13:38:26 -07:00
SpudGunMan d787c72812 Update radio.py 2025-10-14 13:32:22 -07:00
SpudGunMan 9f0dd56d43 Update mesh_bot.py 2025-10-14 13:26:37 -07:00
SpudGunMan aa71e6045a Update radio.py
forgot to save a good
2025-10-14 12:45:41 -07:00
SpudGunMan a140ad83cd Update radio.py 2025-10-14 12:34:44 -07:00
SpudGunMan 93c2d731e8 Update radio.py 2025-10-14 12:33:18 -07:00
SpudGunMan d8da553af9 Update radio.py 2025-10-14 12:32:21 -07:00
SpudGunMan 9d9f070908 enhance 2025-10-14 12:24:42 -07:00
SpudGunMan 0f2061af55 chirpy make my lunch 2025-10-14 12:24:29 -07:00
SpudGunMan d8423584d4 Update radio.py 2025-10-14 12:19:13 -07:00
SpudGunMan 843320d268 Update radio.py 2025-10-14 12:18:11 -07:00
SpudGunMan 216128b15a Update radio.py 2025-10-14 12:07:45 -07:00
SpudGunMan f8bc574753 Update radio.py 2025-10-14 11:35:32 -07:00
SpudGunMan 6193c5933f Update radio.py 2025-10-14 11:32:34 -07:00
SpudGunMan b668965bda bufferHandler
tracking https://github.com/SpudGunMan/meshing-around/issues/213
2025-10-14 11:13:40 -07:00
SpudGunMan ae039b5baf Update radio.py 2025-10-14 11:01:24 -07:00
SpudGunMan 824d43f16e Update radio.py 2025-10-14 10:57:39 -07:00
SpudGunMan 2de76e6c5e Update radio.py 2025-10-14 10:56:14 -07:00
SpudGunMan afb02602fd Update radio.py 2025-10-14 10:53:44 -07:00
SpudGunMan 99528c2bcf Update system.py 2025-10-14 10:52:35 -07:00
SpudGunMan b53f5821f3 Update system.py 2025-10-14 10:51:34 -07:00
SpudGunMan 93fc6547b8 Update system.py 2025-10-14 10:46:30 -07:00
22 changed files with 1190 additions and 330 deletions
+9 -1
View File
@@ -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.
+5 -2
View File
@@ -308,6 +308,7 @@ voxLanguage = en-us
voxInputDevice = default
voxOnTrapList = True
voxTrapList = chirpy
voxEnableCmd = True
[fileMon]
@@ -377,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
@@ -393,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
+224
View File
@@ -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
View File
@@ -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
+221 -138
View File
@@ -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)} isDM:{isDM}")
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:
@@ -293,6 +348,20 @@ def handle_motd(message, message_from_id, isDM):
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():
@@ -373,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:
@@ -389,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:
@@ -420,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()
@@ -530,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)
@@ -583,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):
@@ -630,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
@@ -645,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'])
@@ -660,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
@@ -700,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"):
@@ -744,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']
@@ -763,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 += 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)):
@@ -800,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(' ')
@@ -833,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 = ''
@@ -921,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
@@ -963,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()
@@ -990,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")
@@ -1080,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]))
@@ -1230,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]))
@@ -1302,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()
@@ -1455,6 +1521,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')
rx_time = packet['decoded'].get('rxTime', time.time())
# check if the packet is from us
@@ -1503,7 +1570,7 @@ def onReceive(packet, interface):
if hop_start == hop_limit:
hop = "Direct"
hop_count = 0
elif hop_start == 0 and hop_limit > 0 or via_mqtt:
elif hop_start == 0 and hop_limit > 0 or via_mqtt or transport_mechanism == "TRANSPORT_MQTT":
hop = "MQTT"
hop_count = 0
else:
@@ -1738,7 +1805,7 @@ async def start_rx():
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")
@@ -1812,6 +1879,22 @@ async def start_rx():
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
View File
@@ -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))
+56 -40
View File
@@ -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 message.lower() == "blackjack":
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")
+1 -1
View File
@@ -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 = {}
+8 -7
View File
@@ -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
@@ -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 += playGolf(nodeID, '', True, last_cmd='new')
msg += "\n🏌️[D, L, M, H, G, W, End]🏌️"
return msg
+2 -2
View File
@@ -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
+1
View File
@@ -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.
+1 -3
View File
@@ -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 = ''
+2 -3
View File
@@ -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":
+141 -37
View File
@@ -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
View File
@@ -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
View File
@@ -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
+16 -2
View File
@@ -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()
@@ -377,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)
@@ -404,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)
@@ -412,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
View File
@@ -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."""
+67 -21
View File
@@ -386,11 +386,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}")
@@ -597,12 +597,10 @@ async def get_closest_nodes(nodeInt=1,returnCount=3, channel=publicChannel):
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)
# wait a bit
time.sleep(3)
# wayyy too fast async wait
# send a traceroute request
interface.sendTraceRoute(destinationId=node['id'], channelIndex=channel, wantResponse=False)
# wait a bit
time.sleep(1)
except Exception as e:
logger.error(f"System: Error requesting location data for {node['id']}. Error: {e}")
# sort by distance closest
@@ -809,6 +807,36 @@ def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
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
@@ -994,7 +1022,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)):
@@ -1079,9 +1106,6 @@ def handleAlertBroadcast(deviceID=1):
else:
send_message(deAlert, emergencyAlertBroadcastCh, 0, deviceID)
return True
# pause for traffic
time.sleep(5)
if wxAlertBroadcastEnabled:
if wxAlert:
@@ -1095,9 +1119,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)
@@ -1419,11 +1440,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)
# 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]
@@ -1568,6 +1599,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:
@@ -1921,7 +1968,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}")
+126
View File
@@ -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
+104
View 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)
+29 -13
View File
@@ -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