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:
agessaman
2025-12-22 23:33:17 -08:00
parent 43c217fb0d
commit b200c3f500
4 changed files with 145 additions and 17 deletions
+1
View File
@@ -138,3 +138,4 @@ test_*.log
docs/local/
tests/
test_scripts/
dev/
+53 -11
View File
@@ -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"}