Added logging for GPIO
This commit is contained in:
parent
a1775baa76
commit
25b20fdbbd
@ -2,6 +2,8 @@
|
||||
Utilities for managing book library, scanning EPUBs, and extracting metadata.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
from dreader import create_ebook_reader
|
||||
@ -11,6 +13,8 @@ from PIL import Image
|
||||
import ebooklib
|
||||
from ebooklib import epub
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def scan_book_directory(directory: Path) -> List[Dict[str, str]]:
|
||||
"""
|
||||
@ -44,10 +48,15 @@ def extract_book_metadata(epub_path: Path, include_cover: bool = True) -> Option
|
||||
Returns:
|
||||
Dictionary with book metadata or None if extraction fails
|
||||
"""
|
||||
start_time = time.time()
|
||||
try:
|
||||
# Create temporary reader to extract metadata
|
||||
reader_start = time.time()
|
||||
reader = create_ebook_reader(page_size=(400, 600))
|
||||
reader.load_epub(str(epub_path))
|
||||
reader_elapsed = time.time() - reader_start
|
||||
|
||||
logger.debug(f"[METADATA] Loaded EPUB {epub_path.name} in {reader_elapsed:.2f}s")
|
||||
|
||||
metadata = {
|
||||
'filename': epub_path.name,
|
||||
@ -58,12 +67,19 @@ def extract_book_metadata(epub_path: Path, include_cover: bool = True) -> Option
|
||||
|
||||
# Extract cover image if requested - use direct EPUB extraction
|
||||
if include_cover:
|
||||
cover_start = time.time()
|
||||
cover_data = extract_cover_from_epub(epub_path)
|
||||
cover_elapsed = time.time() - cover_start
|
||||
metadata['cover_data'] = cover_data
|
||||
logger.debug(f"[METADATA] Extracted cover from {epub_path.name} in {cover_elapsed:.2f}s")
|
||||
|
||||
total_elapsed = time.time() - start_time
|
||||
logger.info(f"[METADATA] Extracted metadata from '{metadata['title']}' in {total_elapsed:.2f}s")
|
||||
|
||||
return metadata
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting metadata from {epub_path}: {e}")
|
||||
print(f"Error extracting metadata from {epub_path}: {e}")
|
||||
return {
|
||||
'filename': epub_path.name,
|
||||
@ -128,15 +144,20 @@ def extract_cover_from_epub(epub_path: Path, max_width: int = 300, max_height: i
|
||||
"""
|
||||
try:
|
||||
# Read the EPUB
|
||||
read_start = time.time()
|
||||
book = epub.read_epub(str(epub_path))
|
||||
read_elapsed = time.time() - read_start
|
||||
logger.debug(f"[COVER] Read EPUB {epub_path.name} in {read_elapsed:.2f}s")
|
||||
|
||||
# Look for cover image
|
||||
cover_image = None
|
||||
search_start = time.time()
|
||||
|
||||
# First, try to find item marked as cover
|
||||
for item in book.get_items():
|
||||
if item.get_type() == ebooklib.ITEM_COVER:
|
||||
cover_image = Image.open(BytesIO(item.get_content()))
|
||||
logger.debug(f"[COVER] Found cover marked as ITEM_COVER in {epub_path.name}")
|
||||
break
|
||||
|
||||
# If not found, look for files with 'cover' in the name
|
||||
@ -146,6 +167,7 @@ def extract_cover_from_epub(epub_path: Path, max_width: int = 300, max_height: i
|
||||
name = item.get_name().lower()
|
||||
if 'cover' in name:
|
||||
cover_image = Image.open(BytesIO(item.get_content()))
|
||||
logger.debug(f"[COVER] Found cover by filename in {epub_path.name}")
|
||||
break
|
||||
|
||||
# If still not found, get the first image
|
||||
@ -154,26 +176,36 @@ def extract_cover_from_epub(epub_path: Path, max_width: int = 300, max_height: i
|
||||
if item.get_type() == ebooklib.ITEM_IMAGE:
|
||||
try:
|
||||
cover_image = Image.open(BytesIO(item.get_content()))
|
||||
logger.debug(f"[COVER] Using first image as cover in {epub_path.name}")
|
||||
break
|
||||
except:
|
||||
continue
|
||||
|
||||
search_elapsed = time.time() - search_start
|
||||
|
||||
if not cover_image:
|
||||
logger.debug(f"[COVER] No cover image found in {epub_path.name}")
|
||||
return None
|
||||
|
||||
# Resize if needed (maintain aspect ratio)
|
||||
process_start = time.time()
|
||||
if cover_image.width > max_width or cover_image.height > max_height:
|
||||
cover_image.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
|
||||
logger.debug(f"[COVER] Resized cover for {epub_path.name}")
|
||||
|
||||
# Convert to base64
|
||||
buffer = BytesIO()
|
||||
cover_image.save(buffer, format='PNG')
|
||||
img_bytes = buffer.getvalue()
|
||||
img_base64 = base64.b64encode(img_bytes).decode('utf-8')
|
||||
process_elapsed = time.time() - process_start
|
||||
|
||||
logger.debug(f"[COVER] Processed cover for {epub_path.name}: search={search_elapsed:.2f}s, encode={process_elapsed:.2f}s")
|
||||
|
||||
return img_base64
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error extracting cover from EPUB {epub_path}: {e}")
|
||||
print(f"Error extracting cover from EPUB {epub_path}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@ -57,6 +57,16 @@ from PIL import Image
|
||||
from .hal import DisplayHAL
|
||||
from .gesture import TouchEvent as AppTouchEvent, GestureType as AppGestureType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Try to import GPIO button support (only available on Raspberry Pi)
|
||||
try:
|
||||
from .gpio_buttons import GPIOButtonHandler, load_button_config_from_dict
|
||||
GPIO_BUTTONS_AVAILABLE = True
|
||||
except (ImportError, RuntimeError) as e:
|
||||
GPIO_BUTTONS_AVAILABLE = False
|
||||
logger.debug(f"GPIO buttons not available: {e}")
|
||||
|
||||
# Import dreader-hal components
|
||||
try:
|
||||
from dreader_hal import (
|
||||
@ -72,8 +82,6 @@ except ImportError as e:
|
||||
DREADER_HAL_AVAILABLE = False
|
||||
_import_error = e
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Gesture type mapping between dreader-hal and dreader-application
|
||||
GESTURE_TYPE_MAP = {
|
||||
@ -139,10 +147,16 @@ class HardwareDisplayHAL(DisplayHAL):
|
||||
enable_power_monitor: bool = True,
|
||||
shunt_ohms: float = 0.1,
|
||||
battery_capacity_mah: float = 3000,
|
||||
gpio_config: Optional[dict] = None,
|
||||
config_file: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initialize hardware HAL.
|
||||
|
||||
Args:
|
||||
gpio_config: GPIO button configuration dict (optional)
|
||||
config_file: Path to hardware_config.json file (optional, defaults to "hardware_config.json")
|
||||
|
||||
Raises:
|
||||
ImportError: If dreader-hal library is not installed
|
||||
"""
|
||||
@ -178,6 +192,36 @@ class HardwareDisplayHAL(DisplayHAL):
|
||||
battery_capacity_mah=battery_capacity_mah,
|
||||
)
|
||||
|
||||
# GPIO button handler (optional)
|
||||
self.gpio_handler: Optional[GPIOButtonHandler] = None
|
||||
|
||||
# Load GPIO config from file if specified
|
||||
if config_file or gpio_config is None:
|
||||
config_path = Path(config_file or "hardware_config.json")
|
||||
if config_path.exists():
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
full_config = json.load(f)
|
||||
gpio_config = full_config
|
||||
logger.info(f"Loaded hardware config from {config_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load hardware config from {config_path}: {e}")
|
||||
|
||||
# Initialize GPIO buttons if configured
|
||||
if gpio_config and GPIO_BUTTONS_AVAILABLE:
|
||||
try:
|
||||
self.gpio_handler = load_button_config_from_dict(
|
||||
gpio_config,
|
||||
screen_width=width,
|
||||
screen_height=height
|
||||
)
|
||||
if self.gpio_handler:
|
||||
logger.info("GPIO button handler created")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not initialize GPIO buttons: {e}")
|
||||
elif gpio_config and not GPIO_BUTTONS_AVAILABLE:
|
||||
logger.info("GPIO buttons configured but RPi.GPIO not available (not on Raspberry Pi)")
|
||||
|
||||
self._initialized = False
|
||||
|
||||
async def initialize(self):
|
||||
@ -190,6 +234,7 @@ class HardwareDisplayHAL(DisplayHAL):
|
||||
- Accelerometer (if enabled)
|
||||
- RTC (if enabled)
|
||||
- Power monitor (if enabled)
|
||||
- GPIO buttons (if configured)
|
||||
"""
|
||||
if self._initialized:
|
||||
logger.warning("Hardware HAL already initialized")
|
||||
@ -197,6 +242,12 @@ class HardwareDisplayHAL(DisplayHAL):
|
||||
|
||||
logger.info("Initializing hardware components...")
|
||||
await self.hal.initialize()
|
||||
|
||||
# Initialize GPIO buttons
|
||||
if self.gpio_handler:
|
||||
logger.info("Initializing GPIO buttons...")
|
||||
await self.gpio_handler.initialize()
|
||||
|
||||
self._initialized = True
|
||||
logger.info("Hardware HAL initialized successfully")
|
||||
|
||||
@ -206,6 +257,12 @@ class HardwareDisplayHAL(DisplayHAL):
|
||||
return
|
||||
|
||||
logger.info("Cleaning up hardware HAL")
|
||||
|
||||
# Clean up GPIO buttons
|
||||
if self.gpio_handler:
|
||||
logger.info("Cleaning up GPIO buttons...")
|
||||
await self.gpio_handler.cleanup()
|
||||
|
||||
await self.hal.cleanup()
|
||||
self._initialized = False
|
||||
logger.info("Hardware HAL cleaned up")
|
||||
@ -232,7 +289,7 @@ class HardwareDisplayHAL(DisplayHAL):
|
||||
|
||||
async def get_touch_event(self) -> Optional[AppTouchEvent]:
|
||||
"""
|
||||
Get the next touch event from hardware.
|
||||
Get the next touch event from hardware (touch sensor or GPIO buttons).
|
||||
|
||||
Returns:
|
||||
TouchEvent if available, None if no event
|
||||
@ -242,11 +299,20 @@ class HardwareDisplayHAL(DisplayHAL):
|
||||
- LONG_PRESS: Hold (< 30px movement, >= 500ms)
|
||||
- SWIPE_*: Directional swipes (>= 30px movement)
|
||||
- PINCH_IN/OUT: Two-finger pinch gestures
|
||||
|
||||
GPIO buttons are also polled and generate TouchEvent objects.
|
||||
"""
|
||||
if not self._initialized:
|
||||
return None
|
||||
|
||||
# Get event from dreader-hal
|
||||
# Check GPIO buttons first (they're more responsive)
|
||||
if self.gpio_handler:
|
||||
button_event = await self.gpio_handler.get_button_event()
|
||||
if button_event:
|
||||
logger.info(f"GPIO button event: {button_event.gesture.value}")
|
||||
return button_event
|
||||
|
||||
# Get event from dreader-hal touch sensor
|
||||
hal_event = await self.hal.get_touch_event()
|
||||
|
||||
if hal_event is None:
|
||||
@ -564,13 +630,13 @@ class HardwareDisplayHAL(DisplayHAL):
|
||||
|
||||
async def get_event(self) -> Optional[AppTouchEvent]:
|
||||
"""
|
||||
Get the next event from any input source (touch or accelerometer).
|
||||
Get the next event from any input source (GPIO, touch, or accelerometer).
|
||||
|
||||
This is a convenience method that polls both touch and accelerometer
|
||||
in a single call, prioritizing touch events over tilt events.
|
||||
This is a convenience method that polls all input sources in a single call.
|
||||
Priority order: GPIO buttons > touch sensor > accelerometer tilt
|
||||
|
||||
Returns:
|
||||
TouchEvent from either touch sensor or accelerometer, or None if no event
|
||||
TouchEvent from GPIO, touch sensor, or accelerometer, or None if no event
|
||||
|
||||
Usage:
|
||||
while running:
|
||||
@ -579,12 +645,28 @@ class HardwareDisplayHAL(DisplayHAL):
|
||||
handle_gesture(event)
|
||||
await asyncio.sleep(0.01)
|
||||
"""
|
||||
# Check touch first (higher priority)
|
||||
touch_event = await self.get_touch_event()
|
||||
if touch_event:
|
||||
return touch_event
|
||||
# Check GPIO buttons first (most responsive)
|
||||
if self.gpio_handler:
|
||||
button_event = await self.gpio_handler.get_button_event()
|
||||
if button_event:
|
||||
logger.info(f"GPIO button event: {button_event.gesture.value}")
|
||||
return button_event
|
||||
|
||||
# Check accelerometer tilt
|
||||
# Check touch sensor (second priority)
|
||||
# Get event from dreader-hal touch sensor directly
|
||||
hal_event = await self.hal.get_touch_event()
|
||||
if hal_event is not None:
|
||||
# Convert from dreader-hal TouchEvent to application TouchEvent
|
||||
app_gesture = GESTURE_TYPE_MAP.get(hal_event.gesture)
|
||||
if app_gesture is not None:
|
||||
logger.debug(f"Touch event: {app_gesture.value} at ({hal_event.x}, {hal_event.y})")
|
||||
return AppTouchEvent(
|
||||
gesture=app_gesture,
|
||||
x=hal_event.x,
|
||||
y=hal_event.y
|
||||
)
|
||||
|
||||
# Check accelerometer tilt (lowest priority)
|
||||
if hasattr(self, 'accel_up_vector'):
|
||||
tilt_event = await self.get_tilt_gesture()
|
||||
if tilt_event:
|
||||
|
||||
@ -10,6 +10,8 @@ Handles:
|
||||
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
from PIL import Image, ImageDraw
|
||||
@ -29,6 +31,8 @@ from pyWebLayout.core.query import QueryResult
|
||||
from .book_utils import scan_book_directory, extract_book_metadata
|
||||
from .state import LibraryState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LibraryManager:
|
||||
"""
|
||||
@ -100,19 +104,34 @@ class LibraryManager:
|
||||
Returns:
|
||||
List of book dictionaries with metadata
|
||||
"""
|
||||
start_time = time.time()
|
||||
logger.info(f"[LIBRARY] Scanning library: {self.library_path}")
|
||||
print(f"Scanning library: {self.library_path}")
|
||||
|
||||
if not self.library_path.exists():
|
||||
logger.error(f"Library path does not exist: {self.library_path}")
|
||||
print(f"Library path does not exist: {self.library_path}")
|
||||
return []
|
||||
|
||||
# Scan directory
|
||||
scan_start = time.time()
|
||||
self.books = scan_book_directory(self.library_path)
|
||||
scan_elapsed = time.time() - scan_start
|
||||
logger.info(f"[LIBRARY] Directory scan completed in {scan_elapsed:.2f}s - found {len(self.books)} books")
|
||||
|
||||
# Cache covers to disk if not already cached
|
||||
for book in self.books:
|
||||
cache_start = time.time()
|
||||
for i, book in enumerate(self.books, 1):
|
||||
book_start = time.time()
|
||||
self._cache_book_cover(book)
|
||||
book_elapsed = time.time() - book_start
|
||||
if book_elapsed > 0.1: # Only log if caching took significant time
|
||||
logger.info(f"[LIBRARY] Cached cover {i}/{len(self.books)}: {book['title']} ({book_elapsed:.2f}s)")
|
||||
cache_elapsed = time.time() - cache_start
|
||||
logger.info(f"[LIBRARY] Cover caching completed in {cache_elapsed:.2f}s")
|
||||
|
||||
total_elapsed = time.time() - start_time
|
||||
logger.info(f"[LIBRARY] Library scan complete: {len(self.books)} books in {total_elapsed:.2f}s")
|
||||
print(f"Found {len(self.books)} books in library")
|
||||
return self.books
|
||||
|
||||
@ -310,15 +329,20 @@ class LibraryManager:
|
||||
Returns:
|
||||
PIL Image of the rendered library
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
if table is None:
|
||||
if self.library_table is None:
|
||||
print("No table to render, creating one first...")
|
||||
logger.info("[LIBRARY] Creating library table...")
|
||||
self.create_library_table()
|
||||
table = self.library_table
|
||||
|
||||
print("Rendering library table...")
|
||||
logger.info("[LIBRARY] Rendering library table...")
|
||||
|
||||
# Create page
|
||||
page_start = time.time()
|
||||
page_style = PageStyle(
|
||||
border_width=0,
|
||||
padding=(30, 30, 30, 30),
|
||||
@ -328,6 +352,8 @@ class LibraryManager:
|
||||
page = Page(size=self.page_size, style=page_style)
|
||||
canvas = page.render()
|
||||
draw = ImageDraw.Draw(canvas)
|
||||
page_elapsed = time.time() - page_start
|
||||
logger.info(f"[LIBRARY] Page creation took {page_elapsed:.2f}s")
|
||||
|
||||
# Table style
|
||||
table_style = TableStyle(
|
||||
@ -344,6 +370,8 @@ class LibraryManager:
|
||||
table_width = page.size[0] - page_style.padding[1] - page_style.padding[3]
|
||||
|
||||
# Render table with canvas support for images
|
||||
render_start = time.time()
|
||||
logger.info("[LIBRARY] Starting table render (this may load fonts)...")
|
||||
self.table_renderer = TableRenderer(
|
||||
table,
|
||||
table_origin,
|
||||
@ -353,10 +381,15 @@ class LibraryManager:
|
||||
canvas # Pass canvas to enable image rendering
|
||||
)
|
||||
self.table_renderer.render()
|
||||
render_elapsed = time.time() - render_start
|
||||
logger.info(f"[LIBRARY] Table rendering took {render_elapsed:.2f}s")
|
||||
|
||||
# Store rendered page for query support
|
||||
self.rendered_page = page
|
||||
|
||||
total_elapsed = time.time() - start_time
|
||||
logger.info(f"[LIBRARY] Total render time: {total_elapsed:.2f}s")
|
||||
|
||||
return canvas
|
||||
|
||||
def handle_library_tap(self, x: int, y: int) -> Optional[str]:
|
||||
|
||||
@ -433,8 +433,9 @@ class DReaderApplication:
|
||||
This method sends the current image to the HAL for display.
|
||||
"""
|
||||
if self.current_image:
|
||||
logger.debug(f"Updating display: {self.current_image.size}")
|
||||
logger.info(f"[DISPLAY] Updating display: {self.current_image.size} in {self.state.mode.value} mode")
|
||||
await self.display_hal.show_image(self.current_image)
|
||||
logger.info("[DISPLAY] Display update complete")
|
||||
else:
|
||||
logger.warning("No image to display")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user