Compare commits

..

35 Commits

Author SHA1 Message Date
Kelly 9ca3781353 bbs help
clarify missing command per https://github.com/SpudGunMan/meshing-around/issues/310

Co-Authored-By: Joe <85746415+wb3ihy@users.noreply.github.com>
2026-05-11 13:17:14 -07:00
Kelly 47e4121c4e PKI error logging
enhanced logs for PKI errors and debug
2026-05-10 15:23:31 -07:00
Kelly 8a0eb62574 fixBug 2026-05-04 18:38:40 -07:00
Kelly 873509b3cc tidySolarData 2026-05-04 18:36:50 -07:00
Kelly b03c5c9c2e fixSchedDay
day + time + interval=1 uses daily at time.
day + time + interval>1 uses every N days at time.
day without time uses every N days.
2026-05-04 17:39:59 -07:00
Kelly 2ce976ca8a superfluous redundant 2026-05-04 17:11:51 -07:00
Kelly 76a5913e08 threaded response
all messages tap-back with a thread now in a channel @NomDeTom for the idea :)
2026-05-04 16:41:50 -07:00
Kelly 3819791fcd Update mesh_bot.py
better protection for memory and bad data
2026-04-24 20:57:11 -07:00
Kelly 9fe580a3cb Update launch.sh 2026-04-18 17:17:56 -07:00
Kelly 8567c3ad84 Update launch.sh 2026-04-18 15:35:30 -07:00
Kelly f68f7f10ca logger
logger enhance https://github.com/SpudGunMan/meshing-around/issues/308
2026-04-18 15:30:17 -07:00
dependabot[bot] a02025d4a0 Bump docker/build-push-action from 7.0.0 to 7.1.0 (#307)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.0.0 to 7.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/d08e5c354a6adb9ed34480a06d141179aa583294...bcafcacb16a39f128d818304e6c9c0c18556b85f)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 18:30:45 -07:00
dependabot[bot] 1a2225e833 Bump docker/login-action (#305)
Bumps [docker/login-action](https://github.com/docker/login-action) from 5c42dd293b89d96bae168b5b3694e4e72a5b1117 to 4907a6ddec9925e35a0a9e82d7399ccc52663121.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/5c42dd293b89d96bae168b5b3694e4e72a5b1117...4907a6ddec9925e35a0a9e82d7399ccc52663121)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4907a6ddec9925e35a0a9e82d7399ccc52663121
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-12 12:54:21 -07:00
dependabot[bot] e3728a965a Bump docker/login-action (#304) 2026-03-30 07:30:47 -07:00
Kelly 4dc6befeab Update requirements.txt
cleanup
2026-03-25 15:18:29 -07:00
Kelly 219eea5399 Update joke.py 2026-03-24 12:48:56 -07:00
Kelly c987c1286e Update mesh_bot.py 2026-03-24 11:05:26 -07:00
Kelly 2ebf721bc9 dopewars fix
end of game
2026-03-22 17:24:35 -07:00
Kelly bdef9a1f08 Update install_service.sh 2026-03-22 15:39:37 -07:00
Kelly 2da56bc31f Update install_service.sh 2026-03-22 15:37:34 -07:00
Kelly 1e3c3b9ea0 Update install_service.sh 2026-03-22 15:37:05 -07:00
Kelly d01d7ae668 Update install_service.sh 2026-03-22 14:07:03 -07:00
Kelly b875eed9fd Update install_service.sh 2026-03-22 13:54:40 -07:00
Kelly e8cd85700c Create install_service.sh 2026-03-20 19:49:22 -07:00
Kelly 91b02fead4 Update README.md 2026-03-20 18:37:40 -07:00
Kelly cba6fe3ba2 Update bootstrap.sh 2026-03-17 17:18:50 -07:00
Kelly 021efc8c63 Update bootstrap.sh 2026-03-17 17:02:26 -07:00
Kelly a4b67072cb Update bootstrap.sh 2026-03-17 16:40:46 -07:00
Kelly f1e1516919 Update bootstrap.sh 2026-03-17 16:18:46 -07:00
Kelly e675134d08 Create bootstrap.sh 2026-03-17 16:17:15 -07:00
Kelly 655f2bf7e5 Update requirements.txt 2026-03-17 12:19:21 -07:00
Kelly 46cd2a8051 Update README.md 2026-03-16 21:37:10 -07:00
Kelly fcc4f24ea5 Merge pull request #300 from SpudGunMan/dependabot/github_actions/docker/login-action-9fe7774c8f8ebfade96f0a62aa10f3882309d517
Bump docker/login-action from db14339dbc0a1f0b184157be94b23a2138122354 to 9fe7774c8f8ebfade96f0a62aa10f3882309d517
2026-03-16 19:49:15 -07:00
Kelly 7ddf29ca06 Update requirements.txt 2026-03-16 19:41:26 -07:00
dependabot[bot] 372bc0c5a7 Bump docker/login-action
Bumps [docker/login-action](https://github.com/docker/login-action) from db14339dbc0a1f0b184157be94b23a2138122354 to 9fe7774c8f8ebfade96f0a62aa10f3882309d517.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/db14339dbc0a1f0b184157be94b23a2138122354...9fe7774c8f8ebfade96f0a62aa10f3882309d517)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 9fe7774c8f8ebfade96f0a62aa10f3882309d517
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 09:54:07 +00:00
14 changed files with 400 additions and 82 deletions
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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 =
+173
View File
@@ -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"
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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}")
+8 -9
View File
@@ -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
+3 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,7 +1,6 @@
meshtastic
pubsub
datetime
pyephem
PyPubSub
ephem
requests
maidenhead
beautifulsoup4