mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-07-28 14:29:30 +00:00
Enhance WxCommand logic for period handling and improve map uploader service memory management
- Updated WxCommand to better handle period identification for tomorrow's weather, ensuring accurate retrieval of daytime and nighttime periods based on current conditions. - Added a cleanup mechanism in MapUploaderService to manage memory usage by periodically removing old entries from seen_adverts, preventing unbounded growth and improving performance. - Optimized packet capture service to utilize indexed lookups for faster data retrieval and ensure fallback mechanisms for backward compatibility.
This commit is contained in:
@@ -138,3 +138,4 @@ test_*.log
|
||||
docs/local/
|
||||
tests/
|
||||
test_scripts/
|
||||
dev/
|
||||
|
||||
@@ -560,18 +560,51 @@ class WxCommand(BaseCommand):
|
||||
|
||||
# If current is Tonight and we haven't found Tomorrow yet, look for next day's periods
|
||||
if is_current_tonight and not tomorrow_period:
|
||||
# Look for periods after Tonight (next day)
|
||||
for i, period in enumerate(forecast):
|
||||
if i > 0: # Skip current period
|
||||
period_name = period.get('name', '').lower()
|
||||
# Look for tomorrow, next day, or day names
|
||||
if any(word in period_name for word in ['tomorrow', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']):
|
||||
tomorrow_period = (i, period)
|
||||
break
|
||||
# If today_period is a day name (not "Today"), look for the next period after it
|
||||
if today_period:
|
||||
period_name_lower = today_period[1].get('name', '').lower()
|
||||
day_names = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']
|
||||
if any(day in period_name_lower for day in day_names) and 'today' not in period_name_lower:
|
||||
# today_period is actually tomorrow's daytime period - look for the night period after it
|
||||
today_period_index = today_period[0]
|
||||
# Look for the next period after today_period (should be the night period for that day)
|
||||
for i, period in enumerate(forecast):
|
||||
if i > today_period_index: # Look for periods after today_period
|
||||
period_name = period.get('name', '').lower()
|
||||
# Look for the night period for the same day, or the next day
|
||||
if any(word in period_name for word in ['night', 'tomorrow', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']):
|
||||
tomorrow_period = (i, period)
|
||||
break
|
||||
# If we didn't find a night period, use today_period as tomorrow_period
|
||||
if not tomorrow_period:
|
||||
tomorrow_period = today_period
|
||||
else:
|
||||
# Look for periods after Tonight (next day)
|
||||
for i, period in enumerate(forecast):
|
||||
if i > 0: # Skip current period
|
||||
period_name = period.get('name', '').lower()
|
||||
# Skip if this period is already set as today_period (avoid duplicates)
|
||||
if today_period and today_period[0] == i:
|
||||
continue
|
||||
# Look for tomorrow, next day, or day names
|
||||
if any(word in period_name for word in ['tomorrow', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']):
|
||||
tomorrow_period = (i, period)
|
||||
break
|
||||
else:
|
||||
# Look for periods after Tonight (next day)
|
||||
for i, period in enumerate(forecast):
|
||||
if i > 0: # Skip current period
|
||||
period_name = period.get('name', '').lower()
|
||||
# Look for tomorrow, next day, or day names
|
||||
if any(word in period_name for word in ['tomorrow', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']):
|
||||
tomorrow_period = (i, period)
|
||||
break
|
||||
|
||||
# If current is a night period, prioritize adding Today (the upcoming daytime)
|
||||
# When today_period is a day name (like "Tuesday"), we still add it as tomorrow's daytime period
|
||||
if is_current_night and today_period:
|
||||
period = today_period[1]
|
||||
# Always add today_period - it represents tomorrow's daytime when current is Tonight
|
||||
period_name = self.abbreviate_noaa(period.get('name', 'Today'))
|
||||
period_temp = period.get('temperature', '')
|
||||
period_short = period.get('shortForecast', '')
|
||||
@@ -756,10 +789,17 @@ class WxCommand(BaseCommand):
|
||||
period_str = self._add_period_details(period_str, period_detailed, current_weather_len, max_chars)
|
||||
|
||||
# Only add if we have space (using display width, prioritize current period)
|
||||
# Be more conservative - only add if current period is reasonable length
|
||||
# Be more aggressive about adding tomorrow_period when current is Tonight and we have space
|
||||
max_chars = 128 if (is_current_tonight or is_current_night) else 130
|
||||
if current_weather_len < 110 and self._count_display_width(weather + period_str) <= max_chars:
|
||||
weather += period_str
|
||||
# If current is Tonight and we have plenty of space, be more lenient with the length check
|
||||
if is_current_tonight or is_current_night:
|
||||
# Allow adding tomorrow_period if we're under 120 chars (more lenient than 110)
|
||||
if current_weather_len < 120 and self._count_display_width(weather + period_str) <= max_chars:
|
||||
weather += period_str
|
||||
else:
|
||||
# For non-night periods, use the stricter check
|
||||
if current_weather_len < 110 and self._count_display_width(weather + period_str) <= max_chars:
|
||||
weather += period_str
|
||||
|
||||
return weather, weather_json
|
||||
|
||||
@@ -2949,6 +2989,8 @@ class WxCommand(BaseCommand):
|
||||
# Weather condition emojis
|
||||
if any(word in condition_lower for word in ['sunny', 'clear']):
|
||||
return "☀️"
|
||||
elif any(word in condition_lower for word in ['heavy rain', 'heavy showers', 'excessive rain']):
|
||||
return "🌧️" # Cloud with rain - more rain, less sun
|
||||
elif any(word in condition_lower for word in ['cloudy', 'overcast']):
|
||||
return "☁️"
|
||||
elif any(word in condition_lower for word in ['partly cloudy', 'mostly cloudy']):
|
||||
|
||||
@@ -9,6 +9,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
import time
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
# Import meshcore
|
||||
@@ -123,6 +124,8 @@ class MapUploaderService(BaseServicePlugin):
|
||||
self.connected = False
|
||||
|
||||
# Track seen adverts: {pubkey: last_timestamp}
|
||||
# This prevents duplicate uploads and replay attacks
|
||||
# We'll periodically clean old entries to prevent unbounded growth
|
||||
self.seen_adverts: Dict[str, int] = {}
|
||||
|
||||
# Device keys and info
|
||||
@@ -139,6 +142,10 @@ class MapUploaderService(BaseServicePlugin):
|
||||
# Exit flag
|
||||
self.should_exit = False
|
||||
|
||||
# Cleanup tracking
|
||||
self._last_cleanup_time = 0
|
||||
self._cleanup_interval = 3600 # Clean up every hour
|
||||
|
||||
self.logger.info("Map uploader service initialized")
|
||||
|
||||
def _load_config(self):
|
||||
@@ -239,6 +246,15 @@ class MapUploaderService(BaseServicePlugin):
|
||||
await self.http_session.close()
|
||||
self.http_session = None
|
||||
|
||||
# Clear seen_adverts to free memory
|
||||
self.seen_adverts.clear()
|
||||
|
||||
# Close file handlers
|
||||
for handler in self.logger.handlers[:]:
|
||||
if isinstance(handler, logging.FileHandler):
|
||||
handler.close()
|
||||
self.logger.removeHandler(handler)
|
||||
|
||||
self.logger.info("Map uploader service stopped")
|
||||
|
||||
async def _fetch_private_key(self):
|
||||
@@ -357,6 +373,53 @@ class MapUploaderService(BaseServicePlugin):
|
||||
# Note: meshcore library handles subscription cleanup automatically
|
||||
self.event_subscriptions = []
|
||||
|
||||
async def _cleanup_old_seen_adverts(self, current_timestamp: int):
|
||||
"""Clean up old entries from seen_adverts to prevent unbounded memory growth"""
|
||||
current_time = time.time()
|
||||
|
||||
# Only cleanup periodically (not on every packet)
|
||||
if current_time - self._last_cleanup_time < self._cleanup_interval:
|
||||
return
|
||||
|
||||
self._last_cleanup_time = current_time
|
||||
|
||||
# Remove entries older than min_reupload_interval * 2
|
||||
# This keeps recent entries but removes very old ones
|
||||
cutoff_timestamp = current_timestamp - (self.min_reupload_interval * 2)
|
||||
|
||||
# Count entries before cleanup
|
||||
initial_count = len(self.seen_adverts)
|
||||
|
||||
# Remove old entries
|
||||
keys_to_remove = [
|
||||
pubkey for pubkey, last_ts in self.seen_adverts.items()
|
||||
if last_ts < cutoff_timestamp
|
||||
]
|
||||
|
||||
for pubkey in keys_to_remove:
|
||||
del self.seen_adverts[pubkey]
|
||||
|
||||
removed_count = len(keys_to_remove)
|
||||
|
||||
if removed_count > 0:
|
||||
self.logger.debug(
|
||||
f"Cleaned up {removed_count} old seen_adverts entries "
|
||||
f"({initial_count} -> {len(self.seen_adverts)})"
|
||||
)
|
||||
|
||||
# Safety limit: if dictionary still grows too large, use more aggressive cleanup
|
||||
if len(self.seen_adverts) > 10000:
|
||||
# Keep only the most recent 5000 entries
|
||||
sorted_entries = sorted(
|
||||
self.seen_adverts.items(),
|
||||
key=lambda x: x[1],
|
||||
reverse=True
|
||||
)
|
||||
self.seen_adverts = dict(sorted_entries[:5000])
|
||||
self.logger.warning(
|
||||
f"seen_adverts grew too large, trimmed to 5000 most recent entries"
|
||||
)
|
||||
|
||||
async def _handle_rx_log_data(self, event, metadata=None):
|
||||
"""Handle RX log data events"""
|
||||
try:
|
||||
@@ -464,6 +527,9 @@ class MapUploaderService(BaseServicePlugin):
|
||||
# Update seen adverts
|
||||
self.seen_adverts[pub_key] = timestamp
|
||||
|
||||
# Periodically clean up old entries to prevent unbounded memory growth
|
||||
await self._cleanup_old_seen_adverts(timestamp)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing packet: {e}", exc_info=True)
|
||||
|
||||
|
||||
@@ -536,8 +536,11 @@ class PacketCaptureService(BaseServicePlugin):
|
||||
# 4. Try to get from bot's recent_rf_data cache (if message_handler has processed it)
|
||||
# Note: The bot stores full raw_hex (with framing bytes) for packet_prefix, but we use stripped raw_hex
|
||||
# So we need to use the full raw_hex from payload for prefix matching
|
||||
# Optimized: Use indexed rf_data_by_pubkey for O(1) lookup instead of linear search
|
||||
if packet_hash == '0000000000000000' and hasattr(self.bot, 'message_handler'):
|
||||
try:
|
||||
message_handler = self.bot.message_handler
|
||||
|
||||
# Get full raw_hex from payload for prefix matching (bot uses full raw_hex for packet_prefix)
|
||||
full_raw_hex = payload.get('raw_hex', '')
|
||||
if full_raw_hex:
|
||||
@@ -547,9 +550,23 @@ class PacketCaptureService(BaseServicePlugin):
|
||||
clean_raw_hex_for_lookup = raw_hex.replace('0x', '')
|
||||
packet_prefix = clean_raw_hex_for_lookup[:32] if len(clean_raw_hex_for_lookup) >= 32 else clean_raw_hex_for_lookup
|
||||
|
||||
# Check recent_rf_data for matching packet
|
||||
if hasattr(self.bot.message_handler, 'recent_rf_data'):
|
||||
for rf_data in self.bot.message_handler.recent_rf_data:
|
||||
# Use indexed lookup (O(1)) instead of linear search (O(n))
|
||||
if hasattr(message_handler, 'rf_data_by_pubkey') and packet_prefix:
|
||||
rf_data_list = message_handler.rf_data_by_pubkey.get(packet_prefix, [])
|
||||
# Check most recent entries first (last in list, since they're appended in order)
|
||||
for rf_data in reversed(rf_data_list):
|
||||
if 'packet_hash' in rf_data:
|
||||
packet_hash = rf_data['packet_hash']
|
||||
break
|
||||
elif 'routing_info' in rf_data:
|
||||
routing_info = rf_data.get('routing_info', {})
|
||||
if isinstance(routing_info, dict) and 'packet_hash' in routing_info:
|
||||
packet_hash = routing_info['packet_hash']
|
||||
break
|
||||
|
||||
# Fallback to linear search only if indexed lookup not available (backward compatibility)
|
||||
if packet_hash == '0000000000000000' and hasattr(message_handler, 'recent_rf_data'):
|
||||
for rf_data in message_handler.recent_rf_data:
|
||||
# Match by packet_prefix (bot uses full raw_hex for this)
|
||||
if rf_data.get('packet_prefix') == packet_prefix:
|
||||
if 'packet_hash' in rf_data:
|
||||
@@ -1253,11 +1270,13 @@ class PacketCaptureService(BaseServicePlugin):
|
||||
else:
|
||||
return {"model": "unknown", "version": "unknown"}
|
||||
|
||||
# Return cached info if available and device is not connected
|
||||
if self.cached_firmware_info and (not self.meshcore or not self.meshcore.is_connected):
|
||||
self.logger.debug("Using cached firmware info")
|
||||
# Always use cached info if available - firmware info doesn't change during runtime
|
||||
if self.cached_firmware_info:
|
||||
if self.debug:
|
||||
self.logger.debug("Using cached firmware info")
|
||||
return self.cached_firmware_info
|
||||
|
||||
# Only query if we don't have cached info
|
||||
if not self.meshcore or not self.meshcore.is_connected:
|
||||
return {"model": "unknown", "version": "unknown"}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user