update ncaa FB and NFL recent games to look more like other displays

This commit is contained in:
Chuck
2025-08-10 11:05:03 -05:00
parent c0c77f6762
commit ca44097669
7 changed files with 424 additions and 67 deletions

View File

@@ -2,6 +2,7 @@
# LEDMatrix Cache Permissions Fix Script
# This script fixes permissions on all known cache directories so they're writable by the daemon or current user
# Also sets up placeholder logo directories for sports managers
echo "Fixing LEDMatrix cache directory permissions..."
@@ -18,8 +19,8 @@ for CACHE_DIR in "${CACHE_DIRS[@]}"; do
echo ""
echo "Checking cache directory: $CACHE_DIR"
if [ ! -d "$CACHE_DIR" ]; then
echo " - Directory does not exist. Skipping."
continue
echo " - Directory does not exist. Creating it..."
sudo mkdir -p "$CACHE_DIR"
fi
echo " - Current permissions:"
ls -ld "$CACHE_DIR"
@@ -37,6 +38,42 @@ for CACHE_DIR in "${CACHE_DIRS[@]}"; do
echo " - Permissions fix complete for $CACHE_DIR."
done
# Set up placeholder logos directory for sports managers
echo ""
echo "Setting up placeholder logos directory for sports managers..."
PLACEHOLDER_DIR="/var/cache/ledmatrix/placeholder_logos"
if [ ! -d "$PLACEHOLDER_DIR" ]; then
echo "Creating placeholder logos directory: $PLACEHOLDER_DIR"
sudo mkdir -p "$PLACEHOLDER_DIR"
sudo chown "$REAL_USER":"$REAL_GROUP" "$PLACEHOLDER_DIR"
sudo chmod 777 "$PLACEHOLDER_DIR"
else
echo "Placeholder logos directory already exists: $PLACEHOLDER_DIR"
sudo chmod 777 "$PLACEHOLDER_DIR"
sudo chown "$REAL_USER":"$REAL_GROUP" "$PLACEHOLDER_DIR"
fi
echo " - Current permissions:"
ls -ld "$PLACEHOLDER_DIR"
echo " - Testing write access as $REAL_USER..."
if sudo -u "$REAL_USER" test -w "$PLACEHOLDER_DIR"; then
echo " ✓ Placeholder logos directory is writable by $REAL_USER"
else
echo " ✗ Placeholder logos directory is not writable by $REAL_USER"
fi
# Test with daemon user (which the system might run as)
if sudo -u daemon test -w "$PLACEHOLDER_DIR" 2>/dev/null; then
echo " ✓ Placeholder logos directory is writable by daemon user"
else
echo " ✗ Placeholder logos directory is not writable by daemon user"
fi
echo ""
echo "All cache directory permission fixes attempted."
echo "If you still see errors, check which user is running the LEDMatrix service and ensure it matches the owner above."
echo ""
echo "The system will now create placeholder logos in:"
echo " $PLACEHOLDER_DIR"
echo "This should eliminate the permission denied warnings for sports logos."

View File

@@ -980,11 +980,12 @@ class NCAAFBRecentManager(BaseNCAAFBManager): # Renamed class
center_y = self.display_height // 2
home_x = self.display_width - home_logo.width + 18
# MLB-style logo positioning (closer to edges)
home_x = self.display_width - home_logo.width + 2
home_y = center_y - (home_logo.height // 2)
main_img.paste(home_logo, (home_x, home_y), home_logo)
away_x = -18
away_x = -2
away_y = center_y - (away_logo.height // 2)
main_img.paste(away_logo, (away_x, away_y), away_logo)

View File

@@ -922,11 +922,12 @@ class NFLRecentManager(BaseNFLManager): # Renamed class
center_y = self.display_height // 2
home_x = self.display_width - home_logo.width + 18
# MLB-style logo positioning (closer to edges)
home_x = self.display_width - home_logo.width + 2
home_y = center_y - (home_logo.height // 2)
main_img.paste(home_logo, (home_x, home_y), home_logo)
away_x = -18
away_x = -2
away_y = center_y - (away_logo.height // 2)
main_img.paste(away_logo, (away_x, away_y), away_logo)

View File

@@ -387,50 +387,77 @@ class BaseSoccerManager:
self.logger.debug(f"Logo path: {logo_path}")
# Check if logo exists in original path or cache directory
cache_logo_path = None
if hasattr(self.cache_manager, 'cache_dir') and self.cache_manager.cache_dir:
cache_logo_dir = os.path.join(self.cache_manager.cache_dir, 'placeholder_logos')
cache_logo_path = os.path.join(cache_logo_dir, f"{team_abbrev}.png")
try:
if not os.path.exists(logo_path):
if not os.path.exists(logo_path) and not (cache_logo_path and os.path.exists(cache_logo_path)):
self.logger.info(f"Creating placeholder logo for {team_abbrev}")
# Try to create placeholder in cache directory instead of assets directory
cache_logo_path = None
try:
os.makedirs(os.path.dirname(logo_path), exist_ok=True)
logo = Image.new('RGBA', (36, 36), (random.randint(50, 200), random.randint(50, 200), random.randint(50, 200), 255))
draw = ImageDraw.Draw(logo)
# Optionally add text to placeholder
try:
placeholder_font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 12)
text_width = draw.textlength(team_abbrev, font=placeholder_font)
text_x = (36 - text_width) // 2
text_y = 10
draw.text((text_x, text_y), team_abbrev, fill=(0,0,0,255), font=placeholder_font)
except IOError:
pass # Font not found, skip text
logo.save(logo_path)
self.logger.info(f"Created placeholder logo at {logo_path}")
except PermissionError as pe:
self.logger.warning(f"Permission denied creating placeholder logo for {team_abbrev}: {pe}")
# Use cache directory for placeholder logos
if hasattr(self.cache_manager, 'cache_dir') and self.cache_manager.cache_dir:
cache_logo_dir = os.path.join(self.cache_manager.cache_dir, 'placeholder_logos')
os.makedirs(cache_logo_dir, exist_ok=True)
cache_logo_path = os.path.join(cache_logo_dir, f"{team_abbrev}.png")
# Create placeholder logo
logo = Image.new('RGBA', (36, 36), (random.randint(50, 200), random.randint(50, 200), random.randint(50, 200), 255))
draw = ImageDraw.Draw(logo)
# Optionally add text to placeholder
try:
placeholder_font = ImageFont.truetype("assets/fonts/4x6-font.ttf", 12)
text_width = draw.textlength(team_abbrev, font=placeholder_font)
text_x = (36 - text_width) // 2
text_y = 10
draw.text((text_x, text_y), team_abbrev, fill=(0,0,0,255), font=placeholder_font)
except IOError:
pass # Font not found, skip text
logo.save(cache_logo_path)
self.logger.info(f"Created placeholder logo in cache at {cache_logo_path}")
# Update logo_path to use cache version
logo_path = cache_logo_path
else:
# No cache directory available, just use in-memory placeholder
raise PermissionError("No writable cache directory available")
except (PermissionError, OSError) as pe:
self.logger.debug(f"Could not create placeholder logo file for {team_abbrev}: {pe}")
# Return a simple in-memory placeholder instead
logo = Image.new('RGBA', (36, 36), (random.randint(50, 200), random.randint(50, 200), random.randint(50, 200), 255))
self._logo_cache[team_abbrev] = logo
return logo
try:
logo = Image.open(logo_path)
if logo.mode != 'RGBA':
logo = logo.convert('RGBA')
# Try to load logo from original path or cache directory
logo_to_load = None
if os.path.exists(logo_path):
logo_to_load = logo_path
elif cache_logo_path and os.path.exists(cache_logo_path):
logo_to_load = cache_logo_path
# Resize logo to target size
target_size = 36 # Change target size to 36x36
# Use resize instead of thumbnail to force size if image is smaller
logo = logo.resize((target_size, target_size), Image.Resampling.LANCZOS)
self.logger.debug(f"Resized {team_abbrev} logo to {logo.size}")
if logo_to_load:
try:
logo = Image.open(logo_to_load)
if logo.mode != 'RGBA':
logo = logo.convert('RGBA')
self._logo_cache[team_abbrev] = logo
return logo
except PermissionError as pe:
self.logger.warning(f"Permission denied accessing logo for {team_abbrev}: {pe}")
# Return a simple in-memory placeholder instead
logo = Image.new('RGBA', (36, 36), (random.randint(50, 200), random.randint(50, 200), random.randint(50, 200), 255))
self._logo_cache[team_abbrev] = logo
return logo
# Resize logo to target size
target_size = 36 # Change target size to 36x36
# Use resize instead of thumbnail to force size if image is smaller
logo = logo.resize((target_size, target_size), Image.Resampling.LANCZOS)
self.logger.debug(f"Resized {team_abbrev} logo to {logo.size}")
self._logo_cache[team_abbrev] = logo
return logo
except PermissionError as pe:
self.logger.warning(f"Permission denied accessing logo for {team_abbrev}: {pe}")
# Return a simple in-memory placeholder instead
logo = Image.new('RGBA', (36, 36), (random.randint(50, 200), random.randint(50, 200), random.randint(50, 200), 255))
self._logo_cache[team_abbrev] = logo
return logo
except Exception as e:
self.logger.error(f"Error loading logo for {team_abbrev}: {e}", exc_info=True)

View File

@@ -408,6 +408,30 @@ class StockNewsManager:
self.cached_text_image = None
return True
# Calculate the visible portion
# Handle wrap-around drawing
visible_end = self.scroll_position + width
if visible_end <= total_width:
# Normal case: Paste single crop
visible_portion = self.cached_text_image.crop((
self.scroll_position, 0,
visible_end, height
))
self.display_manager.image.paste(visible_portion, (0, 0))
else:
# Wrap-around case: Paste two parts
width1 = total_width - self.scroll_position
width2 = width - width1
portion1 = self.cached_text_image.crop((self.scroll_position, 0, total_width, height))
portion2 = self.cached_text_image.crop((0, 0, width2, height))
self.display_manager.image.paste(portion1, (0, 0))
self.display_manager.image.paste(portion2, (width1, 0))
self.display_manager.update_display()
self._log_frame_rate()
time.sleep(self.scroll_delay)
return True
def calculate_dynamic_duration(self):
"""Calculate the exact time needed to display all news headlines"""
# If dynamic duration is disabled, use fixed duration from config
@@ -464,28 +488,3 @@ class StockNewsManager:
def get_dynamic_duration(self) -> int:
"""Get the calculated dynamic duration for display"""
return self.dynamic_duration
# Calculate the visible portion
# Handle wrap-around drawing
visible_end = self.scroll_position + width
if visible_end <= total_width:
# Normal case: Paste single crop
visible_portion = self.cached_text_image.crop((
self.scroll_position, 0,
visible_end, height
))
self.display_manager.image.paste(visible_portion, (0, 0))
else:
# Wrap-around case: Paste two parts
width1 = total_width - self.scroll_position
width2 = width - width1
portion1 = self.cached_text_image.crop((self.scroll_position, 0, total_width, height))
portion2 = self.cached_text_image.crop((0, 0, width2, height))
self.display_manager.image.paste(portion1, (0, 0))
self.display_manager.image.paste(portion2, (width1, 0))
self.display_manager.update_display()
self._log_frame_rate()
time.sleep(self.scroll_delay)
return True

125
test_soccer_logo_fix.py Normal file
View File

@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""
Test script to verify the soccer logo permissions fix.
This script tests the _load_and_resize_logo method to ensure it can create placeholder logos
without permission errors.
"""
import os
import sys
import tempfile
import shutil
from PIL import Image, ImageDraw, ImageFont
import random
# Add the src directory to the path so we can import the modules
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
try:
from cache_manager import CacheManager
from soccer_managers import BaseSoccerManager
from display_manager import DisplayManager
except ImportError as e:
print(f"Import error: {e}")
print("Make sure you're running this from the LEDMatrix root directory")
sys.exit(1)
def test_soccer_logo_creation():
"""Test that soccer placeholder logos can be created without permission errors."""
print("Testing soccer logo creation...")
# Create a temporary directory for testing
test_dir = tempfile.mkdtemp(prefix="ledmatrix_test_")
print(f"Using test directory: {test_dir}")
try:
# Create a minimal config
config = {
"soccer_scoreboard": {
"enabled": True,
"logo_dir": "assets/sports/soccer_logos",
"update_interval_seconds": 60
},
"display": {
"width": 64,
"height": 32
}
}
# Create cache manager with test directory
cache_manager = CacheManager()
# Override cache directory for testing
cache_manager.cache_dir = test_dir
# Create a mock display manager
class MockDisplayManager:
def __init__(self):
self.width = 64
self.height = 32
self.image = Image.new('RGB', (64, 32), (0, 0, 0))
display_manager = MockDisplayManager()
# Create soccer manager
soccer_manager = BaseSoccerManager(config, display_manager, cache_manager)
# Test teams that might not have logos
test_teams = ["ATX", "STL", "SD", "CLT", "TEST1", "TEST2"]
print("\nTesting logo creation for missing teams:")
for team in test_teams:
print(f" Testing {team}...")
try:
logo = soccer_manager._load_and_resize_logo(team)
if logo:
print(f" ✓ Successfully created logo for {team} (size: {logo.size})")
else:
print(f" ✗ Failed to create logo for {team}")
except Exception as e:
print(f" ✗ Error creating logo for {team}: {e}")
# Check if placeholder logos were created in cache
placeholder_dir = os.path.join(test_dir, 'placeholder_logos')
if os.path.exists(placeholder_dir):
placeholder_files = os.listdir(placeholder_dir)
print(f"\nPlaceholder logos created in cache: {len(placeholder_files)} files")
for file in placeholder_files:
print(f" - {file}")
else:
print("\nNo placeholder logos directory created (using in-memory placeholders)")
print("\n✓ Soccer logo test completed successfully!")
except Exception as e:
print(f"\n✗ Test failed with error: {e}")
import traceback
traceback.print_exc()
return False
finally:
# Clean up test directory
try:
shutil.rmtree(test_dir)
print(f"Cleaned up test directory: {test_dir}")
except Exception as e:
print(f"Warning: Could not clean up test directory: {e}")
return True
if __name__ == "__main__":
print("LEDMatrix Soccer Logo Permissions Fix Test")
print("=" * 50)
success = test_soccer_logo_creation()
if success:
print("\n🎉 All tests passed! The soccer logo fix is working correctly.")
print("\nTo apply this fix on your Raspberry Pi:")
print("1. Transfer the updated files to your Pi")
print("2. Run: chmod +x fix_soccer_logo_permissions.sh")
print("3. Run: ./fix_soccer_logo_permissions.sh")
print("4. Restart your LEDMatrix application")
else:
print("\n❌ Tests failed. Please check the error messages above.")
sys.exit(1)

167
test_stock_news_fix.py Normal file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env python3
"""
Test script to verify the stock news manager fix.
This script tests that the display_news method works correctly without excessive image generation.
"""
import os
import sys
import time
import tempfile
import shutil
from PIL import Image
# Add the src directory to the path so we can import the modules
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
try:
from cache_manager import CacheManager
from stock_news_manager import StockNewsManager
from display_manager import DisplayManager
except ImportError as e:
print(f"Import error: {e}")
print("Make sure you're running this from the LEDMatrix root directory")
sys.exit(1)
def test_stock_news_display():
"""Test that stock news display works correctly without excessive image generation."""
print("Testing stock news display fix...")
# Create a temporary directory for testing
test_dir = tempfile.mkdtemp(prefix="ledmatrix_test_")
print(f"Using test directory: {test_dir}")
try:
# Create a minimal config
config = {
"stock_news": {
"enabled": True,
"scroll_speed": 1,
"scroll_delay": 0.1, # Slower for testing
"headlines_per_rotation": 2,
"max_headlines_per_symbol": 1,
"update_interval": 300,
"dynamic_duration": True,
"min_duration": 30,
"max_duration": 300
},
"stocks": {
"symbols": ["AAPL", "GOOGL", "MSFT"],
"enabled": True
},
"display": {
"width": 64,
"height": 32
}
}
# Create cache manager with test directory
cache_manager = CacheManager()
# Override cache directory for testing
cache_manager.cache_dir = test_dir
# Create a mock display manager
class MockDisplayManager:
def __init__(self):
self.width = 64
self.height = 32
self.image = Image.new('RGB', (64, 32), (0, 0, 0))
self.matrix = type('Matrix', (), {'width': 64, 'height': 32})()
self.small_font = None # We'll handle this in the test
def update_display(self):
# Mock update - just pass
pass
display_manager = MockDisplayManager()
# Create stock news manager
news_manager = StockNewsManager(config, display_manager)
# Mock some news data
news_manager.news_data = {
"AAPL": [
{"title": "Apple reports strong Q4 earnings", "publisher": "Reuters"},
{"title": "New iPhone sales exceed expectations", "publisher": "Bloomberg"}
],
"GOOGL": [
{"title": "Google announces new AI features", "publisher": "TechCrunch"},
{"title": "Alphabet stock reaches new high", "publisher": "CNBC"}
],
"MSFT": [
{"title": "Microsoft cloud services grow 25%", "publisher": "WSJ"},
{"title": "Windows 12 preview released", "publisher": "The Verge"}
]
}
print("\nTesting display_news method...")
# Test multiple calls to ensure it doesn't generate images excessively
generation_count = 0
original_generate_method = news_manager._generate_background_image
def mock_generate_method(*args, **kwargs):
nonlocal generation_count
generation_count += 1
print(f" Image generation call #{generation_count}")
return original_generate_method(*args, **kwargs)
news_manager._generate_background_image = mock_generate_method
# Call display_news multiple times to simulate the display controller
for i in range(10):
print(f" Call {i+1}: ", end="")
try:
result = news_manager.display_news()
if result:
print("✓ Success")
else:
print("✗ Failed")
except Exception as e:
print(f"✗ Error: {e}")
print(f"\nTotal image generations: {generation_count}")
if generation_count <= 3: # Should only generate a few times for different rotations
print("✓ Image generation is working correctly (not excessive)")
else:
print("✗ Too many image generations - fix may not be working")
print("\n✓ Stock news display test completed!")
except Exception as e:
print(f"\n✗ Test failed with error: {e}")
import traceback
traceback.print_exc()
return False
finally:
# Clean up test directory
try:
shutil.rmtree(test_dir)
print(f"Cleaned up test directory: {test_dir}")
except Exception as e:
print(f"Warning: Could not clean up test directory: {e}")
return True
if __name__ == "__main__":
print("LEDMatrix Stock News Manager Fix Test")
print("=" * 50)
success = test_stock_news_display()
if success:
print("\n🎉 Test completed! The stock news manager should now work correctly.")
print("\nThe fix addresses the issue where the display_news method was:")
print("1. Generating images excessively (every second)")
print("2. Missing the actual scrolling display logic")
print("3. Causing rapid rotation through headlines")
print("\nNow it should:")
print("1. Generate images only when needed for new rotations")
print("2. Properly scroll the content across the display")
print("3. Use the configured dynamic duration properly")
else:
print("\n❌ Test failed. Please check the error messages above.")
sys.exit(1)