mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-05-10 07:14:28 +02:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 03895248cd | |||
| a79de8a325 | |||
| 740b53f02f | |||
| 76e75551c6 | |||
| 51752ae896 | |||
| d81e773c0c | |||
| 1f1ed1ca70 | |||
| 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 |
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+221
-138
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
+16
-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()
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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