mirror of
https://github.com/SpudGunMan/meshing-around.git
synced 2026-05-16 05:55:39 +02:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ca3781353 | |||
| 47e4121c4e | |||
| 8a0eb62574 | |||
| 873509b3cc | |||
| b03c5c9c2e | |||
| 2ce976ca8a | |||
| 76a5913e08 | |||
| 3819791fcd | |||
| 9fe580a3cb | |||
| 8567c3ad84 | |||
| f68f7f10ca | |||
| a02025d4a0 | |||
| 1a2225e833 | |||
| e3728a965a | |||
| 4dc6befeab | |||
| 219eea5399 | |||
| c987c1286e | |||
| 2ebf721bc9 | |||
| bdef9a1f08 | |||
| 2da56bc31f | |||
| 1e3c3b9ea0 | |||
| d01d7ae668 | |||
| b875eed9fd | |||
| e8cd85700c | |||
| 91b02fead4 | |||
| cba6fe3ba2 | |||
| 021efc8c63 | |||
| a4b67072cb | |||
| f1e1516919 | |||
| e675134d08 | |||
| 655f2bf7e5 | |||
| 46cd2a8051 | |||
| fcc4f24ea5 | |||
| 7ddf29ca06 | |||
| 372bc0c5a7 |
@@ -28,7 +28,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@db14339dbc0a1f0b184157be94b23a2138122354
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
@@ -102,7 +102,7 @@ Advanced check-in/check-out and asset tracking for people and equipment—ideal
|
||||
- **Automatic Message Chunking**: Messages over 160 characters are automatically split to ensure reliable delivery across multiple hops.
|
||||
|
||||
## Getting Started
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware, however it is **recomended to use the latest firmware code**. For pico or low-powered devices, see projects for embedding, armbian or [buildroot](https://github.com/buildroot-meshtastic/buildroot-meshtastic), also see [femtofox](https://github.com/noon92/femtofox) for running on luckfox hardware. If you need a local console consider the [firefly](https://github.com/pdxlocations/firefly) project.
|
||||
This project is developed on Linux (specifically a Raspberry Pi) but should work on any platform where the [Meshtastic protobuf API](https://meshtastic.org/docs/software/python/cli/) modules are supported, and with any compatible [Meshtastic](https://meshtastic.org/docs/getting-started/) hardware, however it is **recomended to use the latest firmware code**. For low-powered devices [mPWRD-OS](https://github.com/SpudGunMan/mPWRD-OS) for running on luckfox hardware. If you need a local console consider the [firefly](https://github.com/pdxlocations/firefly) project.
|
||||
|
||||
🥔 Please use responsibly and follow local rulings for such equipment. This project captures packets, logs them, and handles over the air communications which can include PII such as GPS locations.
|
||||
|
||||
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
cd "$BASE_DIR"
|
||||
|
||||
if [[ ! -d "$BASE_DIR/venv" ]]; then
|
||||
python3 -m venv "$BASE_DIR/venv"
|
||||
fi
|
||||
|
||||
source "$BASE_DIR/venv/bin/activate"
|
||||
"$BASE_DIR/venv/bin/pip" install -r "$BASE_DIR/requirements.txt"
|
||||
|
||||
mkdir -p "$BASE_DIR/data"
|
||||
cp -Rn "$BASE_DIR/etc/data/." "$BASE_DIR/data/"
|
||||
|
||||
if [[ ! -f "$BASE_DIR/config.ini" ]]; then
|
||||
cp "$BASE_DIR/config.template" "$BASE_DIR/config.ini"
|
||||
sleep 1
|
||||
replace="s|type = serial|type = tcp|g"
|
||||
sed -i.bak "$replace" "$BASE_DIR/config.ini"
|
||||
replace="s|# hostname = meshtastic.local|hostname = localhost|g"
|
||||
sed -i.bak "$replace" "$BASE_DIR/config.ini"
|
||||
rm -f "$BASE_DIR/config.ini.bak"
|
||||
else
|
||||
echo "config.ini already exists, leaving it unchanged."
|
||||
fi
|
||||
|
||||
deactivate
|
||||
+2
-2
@@ -326,9 +326,9 @@ schedulerMotd = False
|
||||
# 'tide' (time/day), 'solar' (time/day) for automated information broadcasts, matching module needs enabled!
|
||||
# 'custom' for module/scheduler.py custom schedule examples
|
||||
value =
|
||||
# interval to use when time is not set (e.g. every 2 days)
|
||||
# interval for recurring schedules (e.g. every 2 days, or every 2 days at a set time)
|
||||
interval =
|
||||
# time of day in 24:00 hour format when value is 'day' and interval is not set
|
||||
# time of day in 24:00 hour format when value is 'day' (optional with interval)
|
||||
# Process run :00,:20,:40 try and vary the 20 minute offsets to avoid collision
|
||||
time =
|
||||
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Install mesh_bot as a systemd service for the current user.
|
||||
# Defaults:
|
||||
# - project path: /opt/meshing-around
|
||||
# - service name: mesh_bot
|
||||
# - service user: invoking user (SUDO_USER when using sudo)
|
||||
|
||||
SERVICE_NAME="mesh_bot"
|
||||
PROJECT_PATH="/opt/meshing-around"
|
||||
SERVICE_USER="${SUDO_USER:-${USER:-}}"
|
||||
SERVICE_GROUP=""
|
||||
USE_LAUNCH_SH=1
|
||||
NEED_MESHTASTICD=1
|
||||
DRY_RUN=0
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
bash etc/install_service.sh [options]
|
||||
|
||||
Options:
|
||||
--project-path PATH Project root path (default: /opt/meshing-around)
|
||||
--user USER Linux user to run the service as (default: invoking user)
|
||||
--group GROUP Linux group to run the service as (default: user's primary group)
|
||||
--direct-python Run python3 mesh_bot.py directly (skip launch.sh)
|
||||
--no-meshtasticd Do not require meshtasticd.service to be present
|
||||
--dry-run Print actions without changing the system
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
sudo bash etc/install_service.sh
|
||||
sudo bash etc/install_service.sh --project-path /opt/meshing-around --user $USER
|
||||
EOF
|
||||
}
|
||||
|
||||
log() {
|
||||
printf '[install_service] %s\n' "$*"
|
||||
}
|
||||
|
||||
die() {
|
||||
printf '[install_service] ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--project-path)
|
||||
[[ $# -ge 2 ]] || die "Missing value for --project-path"
|
||||
PROJECT_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--user)
|
||||
[[ $# -ge 2 ]] || die "Missing value for --user"
|
||||
SERVICE_USER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--group)
|
||||
[[ $# -ge 2 ]] || die "Missing value for --group"
|
||||
SERVICE_GROUP="$2"
|
||||
shift 2
|
||||
;;
|
||||
--direct-python)
|
||||
USE_LAUNCH_SH=0
|
||||
shift
|
||||
;;
|
||||
--no-meshtasticd)
|
||||
NEED_MESHTASTICD=0
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "Unknown option: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$SERVICE_USER" ]] || die "Could not determine service user. Use --user USER."
|
||||
[[ "$SERVICE_USER" != "root" ]] || die "Refusing to install service as root. Use --user USER."
|
||||
|
||||
if ! id "$SERVICE_USER" >/dev/null 2>&1; then
|
||||
die "User '$SERVICE_USER' does not exist"
|
||||
fi
|
||||
|
||||
if [[ -z "$SERVICE_GROUP" ]]; then
|
||||
SERVICE_GROUP="$(id -gn "$SERVICE_USER")"
|
||||
fi
|
||||
|
||||
id -g "$SERVICE_USER" >/dev/null 2>&1 || die "Could not determine group for user '$SERVICE_USER'"
|
||||
[[ -d "$PROJECT_PATH" ]] || die "Project path not found: $PROJECT_PATH"
|
||||
[[ -f "$PROJECT_PATH/mesh_bot.py" ]] || die "mesh_bot.py not found in $PROJECT_PATH"
|
||||
|
||||
if [[ $USE_LAUNCH_SH -eq 1 ]]; then
|
||||
[[ -f "$PROJECT_PATH/launch.sh" ]] || die "launch.sh not found in $PROJECT_PATH"
|
||||
EXEC_START="/usr/bin/bash $PROJECT_PATH/launch.sh mesh"
|
||||
else
|
||||
EXEC_START="/usr/bin/python3 $PROJECT_PATH/mesh_bot.py"
|
||||
fi
|
||||
|
||||
if [[ $NEED_MESHTASTICD -eq 1 ]]; then
|
||||
if ! systemctl list-units --type=service --no-pager --all | grep meshtasticd.service; then
|
||||
die "meshtasticd.service dependency not found. to ignore this check, run with --no-meshtasticd flag."
|
||||
fi
|
||||
MESHTASTICD_DEPENDENCY_LINES=$'\nAfter=meshtasticd.service\nRequires=meshtasticd.service'
|
||||
else
|
||||
MESHTASTICD_DEPENDENCY_LINES=""
|
||||
fi
|
||||
|
||||
SERVICE_FILE_CONTENT="[Unit]
|
||||
Description=MESH-BOT
|
||||
After=network.target${MESHTASTICD_DEPENDENCY_LINES}
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$SERVICE_USER
|
||||
Group=$SERVICE_GROUP
|
||||
WorkingDirectory=$PROJECT_PATH
|
||||
ExecStart=$EXEC_START
|
||||
KillSignal=SIGINT
|
||||
Environment=REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"
|
||||
|
||||
TARGET_SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
|
||||
|
||||
log "Service user: $SERVICE_USER"
|
||||
log "Service group: $SERVICE_GROUP"
|
||||
log "Project path: $PROJECT_PATH"
|
||||
log "Service file: $TARGET_SERVICE_FILE"
|
||||
log "ExecStart: $EXEC_START"
|
||||
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
log "Dry run mode enabled. Service file content:"
|
||||
printf '\n%s\n' "$SERVICE_FILE_CONTENT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
die "This script needs root privileges. Re-run with: sudo bash etc/install_service.sh"
|
||||
fi
|
||||
|
||||
printf '%s' "$SERVICE_FILE_CONTENT" > "$TARGET_SERVICE_FILE"
|
||||
chmod 644 "$TARGET_SERVICE_FILE"
|
||||
|
||||
# Ensure runtime files are writable by the service account.
|
||||
mkdir -p "$PROJECT_PATH/logs" "$PROJECT_PATH/data"
|
||||
chown -R "$SERVICE_USER:$SERVICE_GROUP" "$PROJECT_PATH/logs" "$PROJECT_PATH/data"
|
||||
if [[ -f "$PROJECT_PATH/config.ini" ]]; then
|
||||
chown "$SERVICE_USER:$SERVICE_GROUP" "$PROJECT_PATH/config.ini"
|
||||
chmod 664 "$PROJECT_PATH/config.ini"
|
||||
fi
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable "$SERVICE_NAME.service"
|
||||
systemctl restart "$SERVICE_NAME.service"
|
||||
|
||||
log "Service installed and started."
|
||||
log "Check status with: sudo systemctl status $SERVICE_NAME.service"
|
||||
log "View logs with: sudo journalctl -u $SERVICE_NAME.service -f"
|
||||
@@ -19,7 +19,7 @@ fi
|
||||
|
||||
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
|
||||
export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
|
||||
|
||||
export HOME=$(pwd)
|
||||
# launch the application
|
||||
if [[ "$1" == pong* ]]; then
|
||||
python3 pong_bot.py
|
||||
|
||||
+70
-36
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/python3
|
||||
#!/usr/bin/env python3
|
||||
# Meshtastic Autoresponder MESH Bot
|
||||
# K7MHI Kelly Keeton 2025
|
||||
try:
|
||||
@@ -591,6 +591,11 @@ llmRunCounter = 0
|
||||
llmTotalRuntime = []
|
||||
llmLocationTable = [{'nodeID': 1234567890, 'location': 'No Location'},]
|
||||
|
||||
# Runtime safety caps to avoid unbounded growth on long-lived systems.
|
||||
MAX_SEEN_NODES = 5000
|
||||
MAX_LLM_LOCATION_ENTRIES = 50
|
||||
MAX_LLM_RUNTIME_SAMPLES = 50
|
||||
|
||||
def handle_satpass(message_from_id, deviceID, message='', vox=False):
|
||||
if vox:
|
||||
location = (my_settings.latitudeValue, my_settings.longitudeValue)
|
||||
@@ -656,6 +661,8 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
|
||||
# likely a DM
|
||||
user_input = message
|
||||
# consider this a command use for the cmdHistory list
|
||||
if len(cmdHistory) > 50:
|
||||
cmdHistory.pop(0)
|
||||
cmdHistory.append({'nodeID': message_from_id, 'cmd': 'llm-use', 'time': time.time()})
|
||||
|
||||
# check for a welcome message (is this redundant?)
|
||||
@@ -679,6 +686,8 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
|
||||
# if not in table add the location
|
||||
if not any(d['nodeID'] == message_from_id for d in llmLocationTable):
|
||||
llmLocationTable.append({'nodeID': message_from_id, 'location': location_name})
|
||||
if len(llmLocationTable) > MAX_LLM_LOCATION_ENTRIES:
|
||||
llmLocationTable = llmLocationTable[-MAX_LLM_LOCATION_ENTRIES:]
|
||||
|
||||
user_input = user_input.strip()
|
||||
|
||||
@@ -709,12 +718,15 @@ def handle_llm(message_from_id, channel_number, deviceID, message, publicChannel
|
||||
end = time.time()
|
||||
llmRunCounter += 1
|
||||
llmTotalRuntime.append(end - start)
|
||||
if len(llmTotalRuntime) > MAX_LLM_RUNTIME_SAMPLES:
|
||||
llmTotalRuntime = llmTotalRuntime[-MAX_LLM_RUNTIME_SAMPLES:]
|
||||
|
||||
return response
|
||||
|
||||
def handleDopeWars(message, nodeID, rxNode):
|
||||
global dwPlayerTracker
|
||||
global dwHighScore
|
||||
msg = ""
|
||||
|
||||
# Find player in tracker
|
||||
player = next((p for p in dwPlayerTracker if p.get('userID') == nodeID), None)
|
||||
@@ -725,7 +737,6 @@ def handleDopeWars(message, nodeID, rxNode):
|
||||
'userID': nodeID,
|
||||
'last_played': time.time(),
|
||||
'cmd': 'new',
|
||||
# ... add other fields as needed ...
|
||||
}
|
||||
dwPlayerTracker.append(player)
|
||||
msg = 'Welcome to 💊Dope Wars💉 You have ' + str(total_days) + ' days to make as much 💰 as possible! '
|
||||
@@ -1854,14 +1865,23 @@ def onReceive(packet, interface):
|
||||
# Priocess the incoming packet, handles the responses to the packet with auto_response()
|
||||
# Sends the packet to the correct handler for processing
|
||||
|
||||
if not isinstance(packet, dict):
|
||||
logger.warning(f"System: Ignoring malformed packet type: {type(packet).__name__}")
|
||||
return
|
||||
|
||||
decoded = packet.get('decoded')
|
||||
if not isinstance(decoded, dict):
|
||||
decoded = {}
|
||||
|
||||
# extract interface details from inbound packet
|
||||
rxType = type(interface).__name__
|
||||
|
||||
# Values assinged to the packet
|
||||
packet_id = None
|
||||
rxNode = message_from_id = snr = rssi = hop = hop_away = channel_number = hop_start = hop_count = hop_limit = 0
|
||||
pkiStatus = (False, 'ABC')
|
||||
rxNodeHostName = None
|
||||
replyIDset = False
|
||||
replyIDset = None
|
||||
emojiSeen = False
|
||||
simulator_flag = False
|
||||
isDM = False
|
||||
@@ -1899,8 +1919,8 @@ def onReceive(packet, interface):
|
||||
|
||||
if rxNode is None:
|
||||
# default to interface 1 ## FIXME needs better like a default interface setting or hash lookup
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] in ['ADMIN_APP', 'SIMULATOR_APP']:
|
||||
session_passkey = packet.get('decoded', {}).get('admin', {}).get('sessionPasskey', None)
|
||||
if decoded.get('portnum') in ['ADMIN_APP', 'SIMULATOR_APP']:
|
||||
session_passkey = decoded.get('admin', {}).get('sessionPasskey', None)
|
||||
rxNode = 1
|
||||
|
||||
# check if the packet has a channel flag use it ## FIXME needs to be channel hash lookup
|
||||
@@ -1940,17 +1960,22 @@ def onReceive(packet, interface):
|
||||
# logger.debug(f"System: Received Packet on Channel:{channel_number} Name:{channel_name} on Interface:{rxNode}")
|
||||
|
||||
# check if the packet has a simulator flag
|
||||
simulator_flag = packet.get('decoded', {}).get('simulator', False)
|
||||
simulator_flag = decoded.get('simulator', False)
|
||||
if isinstance(simulator_flag, dict):
|
||||
# assume Software Simulator
|
||||
simulator_flag = True
|
||||
|
||||
# set the message_from_id
|
||||
message_from_id = packet['from']
|
||||
message_from_id = packet.get('from')
|
||||
if message_from_id is None:
|
||||
logger.warning(f"System: Ignoring packet missing 'from' field on Device:{rxNode}")
|
||||
return
|
||||
|
||||
# if message_from_id is not in the seenNodes list add it
|
||||
if not any(node.get('nodeID') == message_from_id for node in seenNodes):
|
||||
seenNodes.append({'nodeID': message_from_id, 'rxInterface': rxNode, 'channel': channel_number, 'welcome': False, 'first_seen': time.time(), 'lastSeen': time.time()})
|
||||
if len(seenNodes) > MAX_SEEN_NODES:
|
||||
seenNodes = seenNodes[-MAX_SEEN_NODES:]
|
||||
else:
|
||||
# update lastSeen time
|
||||
for node in seenNodes:
|
||||
@@ -1958,7 +1983,7 @@ def onReceive(packet, interface):
|
||||
node['lastSeen'] = time.time()
|
||||
break
|
||||
# BBS DM MAIL CHECKER
|
||||
if bbs_enabled and 'decoded' in packet:
|
||||
if bbs_enabled and decoded:
|
||||
msg = bbs_check_dm(message_from_id)
|
||||
if msg:
|
||||
logger.info(f"System: BBS DM Delivery: {msg[1]} For: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
@@ -1973,18 +1998,25 @@ def onReceive(packet, interface):
|
||||
|
||||
# handle TEXT_MESSAGE_APP
|
||||
try:
|
||||
if 'decoded' in packet and packet['decoded']['portnum'] == 'TEXT_MESSAGE_APP':
|
||||
message_bytes = packet['decoded']['payload']
|
||||
message_string = message_bytes.decode('utf-8')
|
||||
via_mqtt = packet['decoded'].get('viaMqtt', False)
|
||||
if decoded.get('portnum') == 'TEXT_MESSAGE_APP':
|
||||
message_bytes = decoded.get('payload', b'')
|
||||
if isinstance(message_bytes, bytes):
|
||||
message_string = message_bytes.decode('utf-8', errors='replace')
|
||||
elif isinstance(message_bytes, str):
|
||||
message_string = message_bytes
|
||||
else:
|
||||
logger.warning(f"System: Ignoring TEXT_MESSAGE_APP with invalid payload type: {type(message_bytes).__name__}")
|
||||
return
|
||||
message_log_string = message_string.replace('\r', ' ').replace('\n', ' ')
|
||||
via_mqtt = decoded.get('viaMqtt', False)
|
||||
transport_mechanism = (
|
||||
packet.get('transport_mechanism')
|
||||
or packet.get('transportMechanism')
|
||||
or (packet.get('decoded', {}).get('transport_mechanism'))
|
||||
or (packet.get('decoded', {}).get('transportMechanism'))
|
||||
or decoded.get('transport_mechanism')
|
||||
or decoded.get('transportMechanism')
|
||||
or 'unknown'
|
||||
)
|
||||
rx_time = packet['decoded'].get('rxTime', time.time())
|
||||
rx_time = decoded.get('rxTime', time.time())
|
||||
|
||||
# check if the packet is from us
|
||||
if message_from_id in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
|
||||
@@ -1999,9 +2031,11 @@ def onReceive(packet, interface):
|
||||
if packet.get('publicKey'):
|
||||
pkiStatus = packet.get('pkiEncrypted', False), packet.get('publicKey', 'ABC')
|
||||
|
||||
# check if the packet has replyId flag // currently unused in the code
|
||||
if packet.get('replyId'):
|
||||
replyIDset = packet.get('replyId', False)
|
||||
# Use packet id for threaded replies;
|
||||
packet_id = packet.get('id', None)
|
||||
|
||||
# existing reply - unused for tracking
|
||||
replyIDSet = packet.get('replyIDSet', None)
|
||||
|
||||
# check if the packet has emoji flag set it // currently unused in the code
|
||||
if packet.get('emoji'):
|
||||
@@ -2066,13 +2100,13 @@ def onReceive(packet, interface):
|
||||
return
|
||||
|
||||
# If the packet is a DM (Direct Message) respond to it, otherwise validate its a message for us on the channel
|
||||
if packet['to'] in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
|
||||
if packet.get('to') in [myNodeNum1, myNodeNum2, myNodeNum3, myNodeNum4, myNodeNum5, myNodeNum6, myNodeNum7, myNodeNum8, myNodeNum9]:
|
||||
# message is DM to us
|
||||
isDM = True
|
||||
# check if the message contains a trap word, DMs are always responded to
|
||||
if (messageTrap(message_string) and not llm_enabled) or messageTrap(message_string.split()[0]):
|
||||
# log the message to stdout
|
||||
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
logger.info(f"Device:{rxNode} Channel: {channel_number} " + CustomFormatter.green + f"Received DM: " + CustomFormatter.white + f"{message_log_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
# respond with DM
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
|
||||
@@ -2082,7 +2116,7 @@ def onReceive(packet, interface):
|
||||
playingGame = checkPlayingGame(message_from_id, message_string, rxNode, channel_number)
|
||||
elif hop_count >= my_settings.game_hop_limit:
|
||||
if games_enabled:
|
||||
logger.warning(f"Device:{rxNode} Ignoring Request to Play Game: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)} with hop count: {hop}")
|
||||
logger.warning(f"Device:{rxNode} Ignoring Request to Play Game: {message_log_string} From: {get_name_from_number(message_from_id, 'long', rxNode)} with hop count: {hop}")
|
||||
send_message(f"Your hop count exceeds safe playable distance at {hop_count} hops", channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
playingGame = False
|
||||
@@ -2096,7 +2130,7 @@ def onReceive(packet, interface):
|
||||
send_message(llm, channel_number, message_from_id, rxNode)
|
||||
else:
|
||||
# respond with welcome message on DM
|
||||
logger.warning(f"Device:{rxNode} Ignoring DM: {message_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
logger.warning(f"Device:{rxNode} Ignoring DM: {message_log_string} From: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
|
||||
# if seenNodes list is not marked as welcomed send welcome message
|
||||
if not any(node['nodeID'] == message_from_id and node['welcome'] == True for node in seenNodes):
|
||||
@@ -2122,26 +2156,26 @@ def onReceive(packet, interface):
|
||||
|
||||
# log the message to the message log
|
||||
if log_messages_to_file:
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_string.replace('\n', '-nl-'))
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | DM | " + message_log_string)
|
||||
else:
|
||||
# message is on a channel
|
||||
if messageTrap(message_string):
|
||||
# message is for us to respond to, or is it...
|
||||
if my_settings.ignoreDefaultChannel and channel_number == my_settings.publicChannel:
|
||||
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Default Channel:{channel_number}")
|
||||
logger.debug(f"System: Ignoring CMD:{message_log_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Default Channel:{channel_number}")
|
||||
elif str(message_from_id) in my_settings.bbs_ban_list:
|
||||
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Cantankerous Node")
|
||||
logger.debug(f"System: Ignoring CMD:{message_log_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Cantankerous Node")
|
||||
elif str(channel_number) in my_settings.ignoreChannels:
|
||||
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Ignored Channel:{channel_number}")
|
||||
logger.debug(f"System: Ignoring CMD:{message_log_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Ignored Channel:{channel_number}")
|
||||
elif my_settings.cmdBang and not message_string.startswith("!"):
|
||||
logger.debug(f"System: Ignoring CMD:{message_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Didnt sound like they meant it")
|
||||
logger.debug(f"System: Ignoring CMD:{message_log_string} From: {get_name_from_number(message_from_id, 'short', rxNode)} Didnt sound like they meant it")
|
||||
else:
|
||||
# message is for bot to respond to, seriously this time..
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "ReceivedChannel: " + CustomFormatter.white + f"{message_string} " + CustomFormatter.purple +\
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "ReceivedChannel: " + CustomFormatter.white + f"{message_log_string} " + CustomFormatter.purple +\
|
||||
"From: " + CustomFormatter.white + f"{get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
if my_settings.useDMForResponse:
|
||||
# respond to channel message via direct message
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode, reply_id=packet_id)
|
||||
else:
|
||||
# or respond to channel message on the channel itself
|
||||
if channel_number == my_settings.publicChannel and my_settings.antiSpam:
|
||||
@@ -2149,10 +2183,10 @@ def onReceive(packet, interface):
|
||||
logger.warning(f"System: AntiSpam protection, sending DM to: {get_name_from_number(message_from_id, 'long', rxNode)}")
|
||||
|
||||
# respond to channel message via direct message
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode)
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, message_from_id, rxNode, reply_id=packet_id)
|
||||
else:
|
||||
# respond to channel message on the channel itself
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, 0, rxNode)
|
||||
send_message(auto_response(message_string, snr, rssi, hop, pkiStatus, message_from_id, channel_number, rxNode, isDM), channel_number, 0, rxNode, reply_id=packet_id)
|
||||
|
||||
else:
|
||||
# message is not for us to respond to
|
||||
@@ -2172,9 +2206,9 @@ def onReceive(packet, interface):
|
||||
|
||||
# print the message to the log and sdout
|
||||
logger.info(f"Device:{rxNode} Channel:{channel_number} " + CustomFormatter.green + "Ignoring Message:" + CustomFormatter.white +\
|
||||
f" {message_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
|
||||
f" {message_log_string} " + CustomFormatter.purple + "From:" + CustomFormatter.white + f" {get_name_from_number(message_from_id)}")
|
||||
if my_settings.log_messages_to_file:
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_string.replace('\n', '-nl-'))
|
||||
msgLogger.info(f"Device:{rxNode} Channel:{channel_number} | {get_name_from_number(message_from_id, 'long', rxNode)} | " + message_log_string)
|
||||
|
||||
# repeat the message on the other device
|
||||
if my_settings.repeater_enabled and my_settings.multiple_interface:
|
||||
@@ -2204,7 +2238,7 @@ def onReceive(packet, interface):
|
||||
hello(message_from_id, name)
|
||||
# send a hello message as a DM
|
||||
if not my_settings.train_qrz:
|
||||
send_message(f"Hello {name} {qrz_hello_string}", channel_number, message_from_id, rxNode)
|
||||
send_message(f"Hello {name} {qrz_hello_string}", channel_number, message_from_id, rxNode, reply_id=packet_id)
|
||||
|
||||
# handle mini games
|
||||
if my_settings.wordOfTheDay:
|
||||
@@ -2232,8 +2266,8 @@ def onReceive(packet, interface):
|
||||
else:
|
||||
# Evaluate non TEXT_MESSAGE_APP packets
|
||||
consumeMetadata(packet, rxNode, channel_number)
|
||||
except KeyError as e:
|
||||
logger.critical(f"System: Error processing packet: {e} Device:{rxNode}")
|
||||
except Exception as e:
|
||||
logger.exception(f"System: Error processing packet: {e} Device:{rxNode}")
|
||||
logger.debug(f"System: Error Packet = {packet}")
|
||||
|
||||
async def start_rx():
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ def save_bbsdb():
|
||||
|
||||
def bbs_help():
|
||||
# help message
|
||||
return "BBS Commands:\n'bbslist'\n'bbspost $subject #message'\n'bbsread #'\n'bbsdelete #'\n'cmd'"
|
||||
return "BBS Commands:\n'bbslist'\n'bbspost $subject #message'\n'bbspost @node #message' (DM)\n'bbsread #'\n'bbsdelete #'\n'cmd'"
|
||||
|
||||
def bbs_list_messages():
|
||||
#print (f"System: raw bbs_messages: {bbs_messages}")
|
||||
|
||||
@@ -383,14 +383,11 @@ def endGameDw(nodeID):
|
||||
with open('data/dopewar_hs.pkl', 'wb') as file:
|
||||
pickle.dump(dwHighScore, file)
|
||||
msg = "You finished with $" + "{:,}".format(cash) + " and beat the high score!🎉💰"
|
||||
return msg
|
||||
if cash > starting_cash:
|
||||
elif cash > starting_cash:
|
||||
msg = 'You made money! 💵 Up ' + str((cash/starting_cash).__round__()) + 'x! Well done.'
|
||||
return msg
|
||||
if cash == starting_cash:
|
||||
elif cash == starting_cash:
|
||||
msg = 'You broke even... hope you at least had fun 💉💊'
|
||||
return msg
|
||||
if cash < starting_cash:
|
||||
else:
|
||||
msg = "You lost money, better go get a real job.💸"
|
||||
|
||||
# remove player from all trackers and databases
|
||||
@@ -503,6 +500,11 @@ def playDopeWars(nodeID, cmd):
|
||||
if dwGameDayDb[i].get('userID') == nodeID:
|
||||
inGame = True
|
||||
|
||||
# Allow ending the game from any state while a session is active.
|
||||
cmd_normalized = str(cmd).strip().lower()
|
||||
if inGame and cmd_normalized in ['e', 'end', 'quit', 'exit']:
|
||||
return endGameDw(nodeID)
|
||||
|
||||
if not inGame:
|
||||
# initalize player in the database
|
||||
loc = generatelocations()
|
||||
@@ -613,9 +615,6 @@ def playDopeWars(nodeID, cmd):
|
||||
# render_game_screen
|
||||
msg = render_game_screen(nodeID, game_day, total_days, loc_choice, -1, price_list, 0, 'nothing')
|
||||
return msg
|
||||
elif 'e' in menu_choice:
|
||||
msg = endGameDw(nodeID)
|
||||
return msg
|
||||
else:
|
||||
msg = f'example buy:\nb,drug#,qty# or Sell: s,1,10 qty can be (m)ax\n f,p or end'
|
||||
return msg
|
||||
|
||||
@@ -145,10 +145,9 @@ def tableOfContents():
|
||||
'file': '📁', 'folder': '📂', 'sports': '🏅', 'athlete': '🏃', 'competition': '🏆', 'race': '🏁', 'tournament': '🏆', 'champion': '🏆', 'medal': '🏅', 'victory': '🏆', 'win': '🏆', 'lose': '😞',
|
||||
'draw': '🤝', 'team': '👥', 'player': '👤', 'coach': '👨🏫', 'referee': '🧑⚖️', 'stadium': '🏟️', 'arena': '🏟️', 'field': '🏟️', 'court': '🏟️', 'track': '🏟️', 'gym': '🏋️', 'fitness': '🏋️', 'exercise': '🏋️',
|
||||
'workout': '🏋️', 'training': '🏋️', 'practice': '🏋️', 'game': '🎮', 'match': '🎮', 'score': '🏅', 'goal': '🥅', 'point': '🏅', 'basket': '🏀', 'home run': '⚾️', 'strike': '🎳', 'spare': '🎳', 'frame': '🎳',
|
||||
'inning': '⚾️', 'quarter': '🏈', 'half': '🏈', 'overtime': '🏈', 'penalty': '⚽️', 'foul': '⚽️', 'timeout': '⏱️', 'substitute': '🔄', 'bench': '🪑', 'sideline': '🏟️', 'dugout': '⚾️', 'locker room': '🚪', 'shower': '🚿',
|
||||
'uniform': '👕', 'jersey': '👕', 'cleats': '👟', 'helmet': '⛑️', 'pads': '🛡️', 'gloves': '🧤', 'bat': '⚾️', 'ball': '⚽️', 'puck': '🏒', 'stick': '🏒', 'net': '🥅', 'hoop': '🏀', 'goalpost': '🥅', 'whistle': '🔔',
|
||||
'scoreboard': '📊', 'fans': '👥', 'crowd': '👥', 'cheer': '📣', 'boo': '😠', 'applause': '👏', 'celebration': '🎉', 'parade': '🎉', 'trophy': '🏆', 'medal': '🏅', 'ribbon': '🎀', 'cup': '🏆', 'championship': '🏆',
|
||||
'league': '🏆', 'season': '🏆', 'playoffs': '🏆', 'finals': '🏆', 'runner-up': '🥈', 'third place': '🥉', 'snowman': '☃️', 'snowmen': '⛄️'
|
||||
'inning': '⚾️', 'shower': '🚿', 'uniform': '👕', 'jersey': '👕', 'cleats': '👟', 'helmet': '⛑️', 'pads': '🛡️', 'gloves': '🧤', 'bat': '⚾️', 'ball': '⚽️', 'puck': '🏒', 'stick': '🏒', 'net': '🥅', 'goalpost': '🥅',
|
||||
'scoreboard': '📊', 'fans': '👥', 'crowd': '👥', 'cheer': '📣', 'boo': '😠', 'applause': '👏', 'celebration': '🎉', 'parade': '🎉', 'trophy': '🏆', 'medal': '🏅', 'ribbon': '🎀',
|
||||
'third place': '🥉', 'snowman': '☃️', 'snowmen': '⛄️'
|
||||
}
|
||||
|
||||
return wordToEmojiMap
|
||||
|
||||
+13
-4
@@ -104,14 +104,23 @@ def setup_scheduler(
|
||||
|
||||
# Basic Scheduler Options
|
||||
basicOptions = ['day', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun', 'hour', 'min']
|
||||
effective_interval = schedulerIntervalInt
|
||||
if any(option in schedulerValue for option in basicOptions):
|
||||
if schedulerValue == 'day':
|
||||
day_interval = safe_int(schedulerInterval, 1, type="interval")
|
||||
if day_interval < 1:
|
||||
logger.debug(f"System: Scheduler config interval '{schedulerInterval}' invalid for day schedule, using default 1")
|
||||
day_interval = 1
|
||||
effective_interval = day_interval
|
||||
if schedulerTime:
|
||||
# Specific time each day
|
||||
schedule.every().day.at(schedulerTime).do(send_sched_msg)
|
||||
# Specific time at a daily or multi-day interval
|
||||
if day_interval == 1:
|
||||
schedule.every().day.at(schedulerTime).do(send_sched_msg)
|
||||
else:
|
||||
schedule.every(day_interval).days.at(schedulerTime).do(send_sched_msg)
|
||||
else:
|
||||
# Every N days
|
||||
schedule.every(schedulerIntervalInt).days.do(send_sched_msg)
|
||||
schedule.every(day_interval).days.do(send_sched_msg)
|
||||
elif 'mon' in schedulerValue and schedulerTime:
|
||||
schedule.every().monday.at(schedulerTime).do(send_sched_msg)
|
||||
elif 'tue' in schedulerValue and schedulerTime:
|
||||
@@ -130,7 +139,7 @@ def setup_scheduler(
|
||||
schedule.every(schedulerIntervalInt).hours.do(send_sched_msg)
|
||||
elif 'min' in schedulerValue:
|
||||
schedule.every(schedulerIntervalInt).minutes.do(send_sched_msg)
|
||||
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {schedulerIntervalInt} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
logger.debug(f"System: Starting the basic scheduler to send '{scheduler_message}' on schedule '{schedulerValue}' every {effective_interval} interval at time '{schedulerTime}' on Device:{schedulerInterface} Channel:{schedulerChannel}")
|
||||
elif 'joke' in schedulerValue:
|
||||
schedule.every(schedulerIntervalInt).minutes.do(
|
||||
lambda: send_message(tell_joke(), schedulerChannel, 0, schedulerInterface)
|
||||
|
||||
+7
-1
@@ -37,6 +37,12 @@ def hf_band_conditions():
|
||||
def solar_conditions():
|
||||
# radio related solar conditions from hamsql.com
|
||||
solar_cond = ""
|
||||
solar_a_index = ""
|
||||
solar_k_index = ""
|
||||
solar_xray = ""
|
||||
solar_flux = ""
|
||||
sunspots = ""
|
||||
signalnoise = ""
|
||||
try:
|
||||
solar_cond = requests.get("https://www.hamqsl.com/solarxml.php", timeout=urlTimeoutSeconds)
|
||||
if solar_cond.ok:
|
||||
@@ -52,7 +58,7 @@ def solar_conditions():
|
||||
solar_flux = i.getElementsByTagName("solarflux")[0].childNodes[0].data
|
||||
sunspots = i.getElementsByTagName("sunspots")[0].childNodes[0].data
|
||||
signalnoise = i.getElementsByTagName("signalnoise")[0].childNodes[0].data
|
||||
solar_cond = "A-Index: " + solar_a_index + "\nK-Index: " + solar_k_index + "\nSunspots: " + sunspots + "\nX-Ray Flux: " + solar_xray + "\nSolar Flux: " + solar_flux + "\nSignal Noise: " + signalnoise
|
||||
solar_cond = "A: " + solar_a_index + "\nK: " + solar_k_index + "\nSunspots: " + sunspots + "\nX-Ray Flux: " + solar_xray + "\nSolar Flux: " + solar_flux + "\nNoise: " + signalnoise
|
||||
else:
|
||||
logger.error("Solar: Error fetching solar conditions")
|
||||
solar_cond = ERROR_FETCHING_DATA
|
||||
|
||||
+88
-18
@@ -852,7 +852,7 @@ def messageChunker(message):
|
||||
except Exception as e:
|
||||
logger.warning(f"System: Exception during message chunking: {e} (message length: {len(message)})")
|
||||
|
||||
def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
|
||||
def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False, reply_id=None):
|
||||
# Send a message to a channel or DM
|
||||
interface = globals()[f'interface{nodeInt}']
|
||||
# Check if the message is empty
|
||||
@@ -860,6 +860,28 @@ def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
|
||||
return False
|
||||
|
||||
try:
|
||||
def _send_with_reply(**kwargs):
|
||||
# For threaded replies, send as DATA payload to match Meshtastic inline-reply behavior. no API call today.
|
||||
if reply_id is not None:
|
||||
text_payload = kwargs.pop('text', '')
|
||||
if isinstance(text_payload, str):
|
||||
raw_payload = text_payload.encode('utf-8')
|
||||
else:
|
||||
raw_payload = text_payload
|
||||
|
||||
data_kwargs = {
|
||||
# 1 == TEXT_MESSAGE_APP, required so clients render payload as chat text.
|
||||
'portNum': 1,
|
||||
'channelIndex': kwargs.get('channelIndex', ch),
|
||||
'wantAck': kwargs.get('wantAck', wantAck),
|
||||
}
|
||||
if kwargs.get('destinationId'):
|
||||
data_kwargs['destinationId'] = kwargs.get('destinationId')
|
||||
# send the data payload with the replyId for threading
|
||||
return interface.sendData(raw_payload, replyId=reply_id, **data_kwargs)
|
||||
# Otherwise, send as normal text message
|
||||
return interface.sendText(**kwargs)
|
||||
|
||||
# Force chunking and log if message exceeds maxBuffer
|
||||
if len(message.encode('utf-8')) > maxBuffer:
|
||||
logger.debug(f"System: Message length {len(message.encode('utf-8'))} exceeds maxBuffer{maxBuffer}, forcing chunking.")
|
||||
@@ -880,20 +902,20 @@ def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
|
||||
# Send to channel
|
||||
if wantAck:
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"req.ACK " + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
|
||||
interface.sendText(text=m, channelIndex=ch, wantAck=True)
|
||||
_send_with_reply(text=m, channelIndex=ch, wantAck=True)
|
||||
else:
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + f"Chunker{chunkOf} SendingChannel: " + CustomFormatter.white + m.replace('\n', ' '))
|
||||
interface.sendText(text=m, channelIndex=ch)
|
||||
_send_with_reply(text=m, channelIndex=ch)
|
||||
else:
|
||||
# Send to DM
|
||||
if wantAck:
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + f"req.ACK " + f"Chunker{chunkOf} Sending DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
interface.sendText(text=m, channelIndex=ch, destinationId=nodeid, wantAck=True)
|
||||
_send_with_reply(text=m, channelIndex=ch, destinationId=nodeid, wantAck=True)
|
||||
else:
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + f"Chunker{chunkOf} Sending DM: " + CustomFormatter.white + m.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
interface.sendText(text=m, channelIndex=ch, destinationId=nodeid)
|
||||
_send_with_reply(text=m, channelIndex=ch, destinationId=nodeid)
|
||||
|
||||
# Throttle the message sending to prevent spamming the device
|
||||
if (message_list.index(m)+1) % 4 == 0:
|
||||
@@ -908,20 +930,20 @@ def send_message(message, ch, nodeid=0, nodeInt=1, bypassChuncking=False):
|
||||
# Send to channel
|
||||
if wantAck:
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "req.ACK " + "SendingChannel: " + CustomFormatter.white + message.replace('\n', ' '))
|
||||
interface.sendText(text=message, channelIndex=ch, wantAck=True)
|
||||
_send_with_reply(text=message, channelIndex=ch, wantAck=True)
|
||||
else:
|
||||
logger.info(f"Device:{nodeInt} Channel:{ch} " + CustomFormatter.red + "SendingChannel: " + CustomFormatter.white + message.replace('\n', ' '))
|
||||
interface.sendText(text=message, channelIndex=ch)
|
||||
_send_with_reply(text=message, channelIndex=ch)
|
||||
else:
|
||||
# Send to DM
|
||||
if wantAck:
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "req.ACK " + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
interface.sendText(text=message, channelIndex=ch, destinationId=nodeid, wantAck=True)
|
||||
_send_with_reply(text=message, channelIndex=ch, destinationId=nodeid, wantAck=True)
|
||||
else:
|
||||
logger.info(f"Device:{nodeInt} " + CustomFormatter.red + "Sending DM: " + CustomFormatter.white + message.replace('\n', ' ') + CustomFormatter.purple +\
|
||||
" To: " + CustomFormatter.white + f"{get_name_from_number(nodeid, 'long', nodeInt)}")
|
||||
interface.sendText(text=message, channelIndex=ch, destinationId=nodeid)
|
||||
_send_with_reply(text=message, channelIndex=ch, destinationId=nodeid)
|
||||
# Throttle the message sending to prevent spamming the device
|
||||
time.sleep(responseDelay)
|
||||
return True
|
||||
@@ -929,17 +951,24 @@ 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):
|
||||
def send_raw_bytes(nodeid, raw_bytes, nodeInt=1, channel=0, portnum=256, want_ack=True, reply_id=None):
|
||||
# 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
|
||||
)
|
||||
send_kwargs = {
|
||||
'destinationId': nodeid,
|
||||
'portNum': portnum,
|
||||
'channelIndex': channel,
|
||||
'wantAck': want_ack,
|
||||
}
|
||||
if reply_id is not None:
|
||||
try:
|
||||
interface.sendData(raw_bytes, replyId=reply_id, **send_kwargs)
|
||||
except TypeError:
|
||||
logger.debug("System: replyId/replyID unsupported for sendData; sending without threaded reply")
|
||||
interface.sendData(raw_bytes, **send_kwargs)
|
||||
else:
|
||||
interface.sendData(raw_bytes, **send_kwargs)
|
||||
# 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)
|
||||
@@ -1518,6 +1547,14 @@ def initializeMeshLeaderboard():
|
||||
}
|
||||
|
||||
initializeMeshLeaderboard()
|
||||
|
||||
# Known Meshtastic firmware PKI routing errors and practical operator guidance.
|
||||
PKI_ROUTING_ERROR_HINTS = {
|
||||
'PKI_SEND_FAIL_PUBLIC_KEY': 'bot does not have destination public key. or key is missing from the device. Add the destination nodeID to the favorite nodes list, then retry.',
|
||||
'PKI_UNKNOWN_PUBKEY': 'Receiver could not decrypt PKI packet due to missing sender public key. Trigger a NodeInfo exchange both directions, then retry.',
|
||||
'PKI_FAILED': 'PKI was explicitly requested but send prerequisites were not met. Verify PKI-capable firmware/config, key material, and direct-send destination.',
|
||||
}
|
||||
|
||||
def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
global positionMetadata, localTelemetryData, meshLeaderboard
|
||||
uptime = battery = temp = iaq = nodeID = 0
|
||||
@@ -1830,6 +1867,39 @@ def consumeMetadata(packet, rxNode=0, channel=-1):
|
||||
except Exception as e:
|
||||
logger.debug(f"System: ADMIN_APP decode error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
|
||||
# ROUTING_APP - meta for logs
|
||||
if packet_type == 'ROUTING_APP':
|
||||
try:
|
||||
if debugMetadata and 'ROUTING_APP' not in metadataFilter:
|
||||
print(f"DEBUG ROUTING_APP: {packet}\n\n")
|
||||
routing_data = packet['decoded']['routing']
|
||||
|
||||
# Meshtastic Python/client can surface this field as errorReason or error_reason.
|
||||
error_reason = routing_data.get('errorReason', routing_data.get('error_reason', ''))
|
||||
if error_reason:
|
||||
requester_node = packet.get('from', nodeID)
|
||||
requester_id = packet.get('fromId', '')
|
||||
target_node = packet.get('to', 0)
|
||||
request_id = packet.get('decoded', {}).get('requestId', packet.get('decoded', {}).get('request_id', 0))
|
||||
pki_hint = PKI_ROUTING_ERROR_HINTS.get(error_reason, 'No playbook entry yet. Check node public keys/NodeInfo sync and firmware versions on both peers.')
|
||||
|
||||
# Standardized PKI routing failure log with source/target context for triage.
|
||||
if str(error_reason).startswith('PKI_'):
|
||||
logger.warning(
|
||||
f"System: PKI Routing Error Device:{rxNode} Channel:{channel} Reason:{error_reason} "
|
||||
f"RequesterNode:{requester_node} RequesterID:{requester_id} "
|
||||
f"RequesterShort:{get_name_from_number(requester_node, 'short', rxNode)} "
|
||||
f"TargetNode:{target_node} RequestId:{request_id} Guidance:{pki_hint}"
|
||||
)
|
||||
elif logMetaStats:
|
||||
logger.info(
|
||||
f"System: ROUTING_APP Error Device:{rxNode} Channel:{channel} Reason:{error_reason} "
|
||||
f"RequesterNode:{requester_node} TargetNode:{target_node} RequestId:{request_id}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.debug(f"System: ROUTING_APP decode error: Device: {rxNode} Channel: {channel} {e} packet {packet}")
|
||||
|
||||
|
||||
# IP_TUNNEL_APP - Track tunneling packets 🚨
|
||||
if packet_type == 'IP_TUNNEL_APP':
|
||||
try:
|
||||
@@ -2442,7 +2512,7 @@ def saveAllData():
|
||||
save_bbsBanList()
|
||||
logger.debug("Persistence: Ban list saved")
|
||||
|
||||
logger.info("Persistence: Save completed")
|
||||
#logger.info("Persistence: Save completed")
|
||||
except Exception as e:
|
||||
logger.error(f"Persistence: Save error: {e}")
|
||||
|
||||
|
||||
+2
-3
@@ -1,7 +1,6 @@
|
||||
meshtastic
|
||||
pubsub
|
||||
datetime
|
||||
pyephem
|
||||
PyPubSub
|
||||
ephem
|
||||
requests
|
||||
maidenhead
|
||||
beautifulsoup4
|
||||
|
||||
Reference in New Issue
Block a user