HW integratation
Python CI / test (3.12) (push) Successful in 7m1s
Python CI / test (3.13) (push) Successful in 7m10s

This commit is contained in:
2025-11-11 11:57:39 +01:00
parent 5a573c901e
commit 70c0b4a1f2
19 changed files with 4082 additions and 21 deletions
+363
View File
@@ -0,0 +1,363 @@
#!/usr/bin/env python3
"""
Accelerometer Calibration Script
This script helps calibrate the accelerometer for gravity-based page flipping.
It displays visual instructions on the e-ink display to guide the user through
aligning the device with the "up" direction.
The calibration process:
1. Shows an arrow pointing up
2. User rotates device until arrow aligns with desired "up" direction
3. User confirms by tapping screen
4. Script saves calibration offset to config file
Usage:
python examples/calibrate_accelerometer.py
"""
import asyncio
import sys
import json
import math
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.hal_hardware import HardwareDisplayHAL
from dreader.gesture import GestureType
class AccelerometerCalibrator:
"""Interactive accelerometer calibration tool"""
def __init__(self, hal: HardwareDisplayHAL, config_path: str = "accelerometer_config.json"):
self.hal = hal
self.config_path = Path(config_path)
self.width = hal.width
self.height = hal.height
self.calibrated = False
# Calibration data
self.up_vector = None # (x, y, z) when device is in "up" position
async def run(self):
"""Run the calibration process"""
print("Starting accelerometer calibration...")
print(f"Display: {self.width}x{self.height}")
await self.hal.initialize()
try:
# Show welcome screen
await self.show_welcome()
await self.wait_for_tap()
# Calibration loop
await self.calibration_loop()
# Show completion screen
await self.show_completion()
await asyncio.sleep(3)
finally:
await self.hal.cleanup()
async def show_welcome(self):
"""Display welcome/instruction screen"""
img = Image.new('RGB', (self.width, self.height), color=(255, 255, 255))
draw = ImageDraw.Draw(img)
# Try to load a font, fall back to default
try:
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 48)
body_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32)
except:
title_font = ImageFont.load_default()
body_font = ImageFont.load_default()
# Title
title = "Accelerometer Calibration"
title_bbox = draw.textbbox((0, 0), title, font=title_font)
title_width = title_bbox[2] - title_bbox[0]
draw.text(((self.width - title_width) // 2, 100), title, fill=(0, 0, 0), font=title_font)
# Instructions
instructions = [
"This will calibrate the accelerometer",
"for gravity-based page flipping.",
"",
"You will:",
"1. See an arrow on screen",
"2. Rotate device until arrow points UP",
"3. Tap screen to confirm",
"",
"Tap anywhere to begin..."
]
y = 250
for line in instructions:
line_bbox = draw.textbbox((0, 0), line, font=body_font)
line_width = line_bbox[2] - line_bbox[0]
draw.text(((self.width - line_width) // 2, y), line, fill=(0, 0, 0), font=body_font)
y += 50
await self.hal.show_image(img)
async def calibration_loop(self):
"""Main calibration loop - show live arrow and accelerometer reading"""
print("\nCalibration mode:")
print("Rotate device until arrow points UP, then tap screen.")
last_display_time = 0
display_interval = 0.2 # Update display every 200ms
while not self.calibrated:
# Get current acceleration
x, y, z = await self.hal.hal.orientation.get_acceleration()
# Update display if enough time has passed
current_time = asyncio.get_event_loop().time()
if current_time - last_display_time >= display_interval:
await self.show_calibration_screen(x, y, z)
last_display_time = current_time
# Check for touch event
event = await self.hal.get_touch_event()
if event and event.gesture == GestureType.TAP:
# Save current orientation as "up"
self.up_vector = (x, y, z)
self.calibrated = True
print(f"\nCalibration saved: up_vector = ({x:.2f}, {y:.2f}, {z:.2f})")
break
await asyncio.sleep(0.05) # Poll at ~20Hz
async def show_calibration_screen(self, ax: float, ay: float, az: float):
"""
Show arrow pointing in direction of gravity
Args:
ax, ay, az: Acceleration components in m/s²
"""
img = Image.new('RGB', (self.width, self.height), color=(255, 255, 255))
draw = ImageDraw.Draw(img)
# Calculate gravity direction (normalized)
magnitude = math.sqrt(ax**2 + ay**2 + az**2)
if magnitude < 0.1: # Avoid division by zero
magnitude = 1.0
gx = ax / magnitude
gy = ay / magnitude
gz = az / magnitude
# Project gravity onto screen plane (assuming z is out of screen)
# We want to show which way is "down" on the device
# Arrow should point opposite to gravity (toward "up")
arrow_dx = -gx
arrow_dy = -gy
# Normalize for display
arrow_length = min(self.width, self.height) * 0.3
arrow_magnitude = math.sqrt(arrow_dx**2 + arrow_dy**2)
if arrow_magnitude < 0.1:
arrow_magnitude = 1.0
arrow_dx = (arrow_dx / arrow_magnitude) * arrow_length
arrow_dy = (arrow_dy / arrow_magnitude) * arrow_length
# Center point
cx = self.width // 2
cy = self.height // 2
# Arrow endpoint
end_x = cx + int(arrow_dx)
end_y = cy + int(arrow_dy)
# Draw large arrow
self.draw_arrow(draw, cx, cy, end_x, end_y, width=10)
# Draw circle at center
circle_radius = 30
draw.ellipse(
[(cx - circle_radius, cy - circle_radius),
(cx + circle_radius, cy + circle_radius)],
outline=(0, 0, 0),
width=5
)
# Draw text with acceleration values
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 28)
except:
font = ImageFont.load_default()
text = f"X: {ax:6.2f} m/s²"
draw.text((50, 50), text, fill=(0, 0, 0), font=font)
text = f"Y: {ay:6.2f} m/s²"
draw.text((50, 100), text, fill=(0, 0, 0), font=font)
text = f"Z: {az:6.2f} m/s²"
draw.text((50, 150), text, fill=(0, 0, 0), font=font)
text = "Rotate device until arrow points UP"
text_bbox = draw.textbbox((0, 0), text, font=font)
text_width = text_bbox[2] - text_bbox[0]
draw.text(((self.width - text_width) // 2, self.height - 150),
text, fill=(0, 0, 0), font=font)
text = "Then TAP screen to save"
text_bbox = draw.textbbox((0, 0), text, font=font)
text_width = text_bbox[2] - text_bbox[0]
draw.text(((self.width - text_width) // 2, self.height - 100),
text, fill=(0, 0, 0), font=font)
await self.hal.show_image(img)
def draw_arrow(self, draw: ImageDraw.Draw, x1: int, y1: int, x2: int, y2: int, width: int = 5):
"""Draw an arrow from (x1, y1) to (x2, y2)"""
# Main line
draw.line([(x1, y1), (x2, y2)], fill=(0, 0, 0), width=width)
# Arrow head
dx = x2 - x1
dy = y2 - y1
length = math.sqrt(dx**2 + dy**2)
if length < 0.1:
return
# Normalize
dx /= length
dy /= length
# Arrow head size
head_length = 40
head_width = 30
# Perpendicular vector
px = -dy
py = dx
# Arrow head points
p1_x = x2 - dx * head_length + px * head_width
p1_y = y2 - dy * head_length + py * head_width
p2_x = x2 - dx * head_length - px * head_width
p2_y = y2 - dy * head_length - py * head_width
# Draw arrow head
draw.polygon([(x2, y2), (p1_x, p1_y), (p2_x, p2_y)], fill=(0, 0, 0))
async def show_completion(self):
"""Show calibration complete screen"""
img = Image.new('RGB', (self.width, self.height), color=(255, 255, 255))
draw = ImageDraw.Draw(img)
try:
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 48)
body_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32)
except:
title_font = ImageFont.load_default()
body_font = ImageFont.load_default()
# Title
title = "Calibration Complete!"
title_bbox = draw.textbbox((0, 0), title, font=title_font)
title_width = title_bbox[2] - title_bbox[0]
draw.text(((self.width - title_width) // 2, 200), title, fill=(0, 0, 0), font=title_font)
# Details
if self.up_vector:
x, y, z = self.up_vector
details = [
f"Up vector saved:",
f"X: {x:.3f} m/s²",
f"Y: {y:.3f} m/s²",
f"Z: {z:.3f} m/s²",
"",
f"Saved to: {self.config_path}"
]
y_pos = 350
for line in details:
line_bbox = draw.textbbox((0, 0), line, font=body_font)
line_width = line_bbox[2] - line_bbox[0]
draw.text(((self.width - line_width) // 2, y_pos), line, fill=(0, 0, 0), font=body_font)
y_pos += 50
await self.hal.show_image(img)
# Save calibration to file
self.save_calibration()
def save_calibration(self):
"""Save calibration data to JSON file"""
if not self.up_vector:
print("Warning: No calibration data to save")
return
x, y, z = self.up_vector
config = {
"up_vector": {
"x": x,
"y": y,
"z": z
},
"tilt_threshold": 0.3, # Radians (~17 degrees)
"debounce_time": 0.5, # Seconds between tilt gestures
}
with open(self.config_path, 'w') as f:
json.dump(config, f, indent=2)
print(f"Calibration saved to {self.config_path}")
async def wait_for_tap(self):
"""Wait for user to tap screen"""
while True:
event = await self.hal.get_touch_event()
if event and event.gesture == GestureType.TAP:
break
await asyncio.sleep(0.05)
async def main():
"""Main entry point"""
# Create HAL with accelerometer enabled
print("Initializing hardware...")
hal = HardwareDisplayHAL(
width=1872,
height=1404,
enable_orientation=True,
enable_rtc=False,
enable_power_monitor=False,
virtual_display=False # Set to True for testing without hardware
)
# Create calibrator
calibrator = AccelerometerCalibrator(hal)
# Run calibration
await calibrator.run()
print("\nCalibration complete!")
print("You can now use accelerometer-based page flipping.")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nCalibration cancelled by user")
sys.exit(0)
except Exception as e:
print(f"\nError during calibration: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
"""
Demo: Accelerometer-based Page Flipping
This example demonstrates how to use the accelerometer for hands-free
page turning by tilting the device forward or backward.
Features:
- Tilt device forward to advance to next page
- Tilt device backward to go to previous page
- Touch gestures still work normally
- Configurable tilt threshold and debounce time
Prerequisites:
1. Run calibration first: python examples/calibrate_accelerometer.py
2. This creates accelerometer_config.json with calibration data
Usage:
python examples/demo_accelerometer_page_flip.py <epub_file>
"""
import asyncio
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.hal_hardware import HardwareDisplayHAL
from dreader.application import EbookReader
from dreader.gesture import GestureType
class AccelerometerPageFlipDemo:
"""Demo application with accelerometer-based page flipping"""
def __init__(self, epub_path: str):
self.epub_path = epub_path
# Create HAL with accelerometer enabled
print("Initializing hardware HAL...")
self.hal = HardwareDisplayHAL(
width=1872,
height=1404,
enable_orientation=True,
enable_rtc=False,
enable_power_monitor=False,
virtual_display=False # Set to True for testing without hardware
)
# Create reader
print("Creating ebook reader...")
self.reader = EbookReader(
page_size=(self.hal.width, self.hal.height),
margin=60
)
self.running = False
async def run(self):
"""Run the demo application"""
print("\n" + "="*60)
print("Accelerometer Page Flip Demo")
print("="*60)
# Initialize HAL
await self.hal.initialize()
# Load accelerometer calibration
print("\nLoading accelerometer calibration...")
calibrated = self.hal.load_accelerometer_calibration("accelerometer_config.json")
if not calibrated:
print("\nWARNING: Accelerometer not calibrated!")
print("Please run: python examples/calibrate_accelerometer.py")
print("\nProceeding with touch gestures only...\n")
else:
print("Accelerometer calibration loaded successfully!")
print(f" Up vector: {self.hal.accel_up_vector}")
print(f" Tilt threshold: {self.hal.accel_tilt_threshold:.2f} rad")
print(f" Debounce time: {self.hal.accel_debounce_time:.2f}s")
# Load EPUB
print(f"\nLoading EPUB: {self.epub_path}")
success = self.reader.load_epub(self.epub_path)
if not success:
print(f"ERROR: Failed to load {self.epub_path}")
await self.hal.cleanup()
return
print(f"Loaded: {self.reader.book_title}")
print(f"Author: {self.reader.book_author}")
# Display first page
print("\nDisplaying first page...")
img = self.reader.get_current_page()
await self.hal.show_image(img)
# Instructions
print("\n" + "="*60)
print("Controls:")
print(" - Tilt FORWARD to go to next page")
print(" - Tilt BACKWARD to go to previous page")
print(" - Swipe LEFT for next page (touch)")
print(" - Swipe RIGHT for previous page (touch)")
print(" - Long press to exit")
print("="*60 + "\n")
# Main event loop
self.running = True
try:
await self.event_loop()
finally:
await self.hal.cleanup()
print("\nDemo finished!")
async def event_loop(self):
"""Main event loop - poll for touch and accelerometer events"""
accel_poll_interval = 0.05 # Check accelerometer every 50ms
while self.running:
# Check for touch events
touch_event = await self.hal.get_touch_event()
if touch_event:
await self.handle_event(touch_event)
# Check for accelerometer tilt events (if calibrated)
if hasattr(self.hal, 'accel_up_vector'):
tilt_event = await self.hal.get_tilt_gesture()
if tilt_event:
await self.handle_event(tilt_event)
# Small delay to avoid busy-waiting
await asyncio.sleep(accel_poll_interval)
async def handle_event(self, event):
"""Handle a gesture event (touch or accelerometer)"""
gesture = event.gesture
print(f"Gesture: {gesture.value}")
# Navigation gestures
if gesture in [GestureType.SWIPE_LEFT, GestureType.TILT_FORWARD]:
await self.next_page()
elif gesture in [GestureType.SWIPE_RIGHT, GestureType.TILT_BACKWARD]:
await self.previous_page()
# Exit on long press
elif gesture == GestureType.LONG_PRESS:
print("\nLong press detected - exiting...")
self.running = False
# Word tap
elif gesture == GestureType.TAP:
# You could implement word selection here
print(f" Tap at ({event.x}, {event.y})")
async def next_page(self):
"""Go to next page"""
img = self.reader.next_page()
if img:
progress = self.reader.get_reading_progress()
chapter = self.reader.get_current_chapter_info()
print(f" -> Next page ({progress['percent']:.1f}% - {chapter['title']})")
await self.hal.show_image(img)
else:
print(" -> At end of book")
async def previous_page(self):
"""Go to previous page"""
img = self.reader.previous_page()
if img:
progress = self.reader.get_reading_progress()
chapter = self.reader.get_current_chapter_info()
print(f" -> Previous page ({progress['percent']:.1f}% - {chapter['title']})")
await self.hal.show_image(img)
else:
print(" -> At start of book")
async def main():
"""Main entry point"""
if len(sys.argv) < 2:
print("Usage: python demo_accelerometer_page_flip.py <epub_file>")
print("\nExample:")
print(" python demo_accelerometer_page_flip.py ~/Books/mybook.epub")
sys.exit(1)
epub_path = sys.argv[1]
# Check if file exists
if not Path(epub_path).exists():
print(f"ERROR: File not found: {epub_path}")
sys.exit(1)
# Run demo
demo = AccelerometerPageFlipDemo(epub_path)
await demo.run()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nDemo interrupted by user")
sys.exit(0)
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
Simple Accelerometer Demo - Using Unified Event API
This is a simplified version of the accelerometer demo that uses
the HAL's get_event() convenience method to poll both touch and
accelerometer in a single call.
Usage:
python examples/demo_accelerometer_simple.py <epub_file>
"""
import asyncio
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.hal_hardware import HardwareDisplayHAL
from dreader.application import EbookReader
from dreader.gesture import GestureType
async def main():
"""Simple demo using unified event API"""
if len(sys.argv) < 2:
print("Usage: python demo_accelerometer_simple.py <epub_file>")
sys.exit(1)
epub_path = sys.argv[1]
# Create HAL with accelerometer enabled
print("Initializing hardware...")
hal = HardwareDisplayHAL(
width=1872,
height=1404,
enable_orientation=True
)
await hal.initialize()
# Load accelerometer calibration (optional)
if hal.load_accelerometer_calibration("accelerometer_config.json"):
print("✓ Accelerometer calibrated - tilt gestures enabled")
else:
print("✗ No accelerometer calibration - touch only")
# Create reader and load book
print(f"\nLoading: {epub_path}")
reader = EbookReader(page_size=(hal.width, hal.height), margin=60)
if not reader.load_epub(epub_path):
print(f"ERROR: Failed to load {epub_path}")
await hal.cleanup()
return
print(f"Loaded: {reader.book_title}")
# Display first page
img = reader.get_current_page()
await hal.show_image(img)
print("\nControls:")
print(" Swipe LEFT or Tilt FORWARD → Next page")
print(" Swipe RIGHT or Tilt BACKWARD → Previous page")
print(" Long press → Exit\n")
# Main event loop - simple unified API!
running = True
while running:
# Get event from any source (touch or accelerometer)
event = await hal.get_event()
if event:
print(f"Gesture: {event.gesture.value}")
# Page navigation
if event.gesture in [GestureType.SWIPE_LEFT, GestureType.TILT_FORWARD]:
img = reader.next_page()
if img:
progress = reader.get_reading_progress()
print(f" → Page {progress['current']}/{progress['total']} ({progress['percent']:.1f}%)")
await hal.show_image(img)
else:
print(" → End of book")
elif event.gesture in [GestureType.SWIPE_RIGHT, GestureType.TILT_BACKWARD]:
img = reader.previous_page()
if img:
progress = reader.get_reading_progress()
print(f" ← Page {progress['current']}/{progress['total']} ({progress['percent']:.1f}%)")
await hal.show_image(img)
else:
print(" ← Start of book")
# Exit
elif event.gesture == GestureType.LONG_PRESS:
print("\nExiting...")
running = False
await asyncio.sleep(0.01)
await hal.cleanup()
print("Done!")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nInterrupted")
sys.exit(0)
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
+292
View File
@@ -0,0 +1,292 @@
#!/usr/bin/env python3
"""
Run DReader on real e-ink hardware.
This example demonstrates running the DReader application on real e-ink hardware
using the dreader-hal library for hardware abstraction.
Requirements:
- Raspberry Pi (or compatible SBC)
- IT8951 e-ink display
- FT5xx6 capacitive touch sensor
- Optional: BMA400 accelerometer, PCF8523 RTC, INA219 power monitor
Hardware Setup:
See external/dreader-hal/README.md for wiring instructions
Usage:
# On Raspberry Pi with full hardware
python run_on_hardware.py /path/to/library
# For testing without hardware (virtual display mode)
python run_on_hardware.py /path/to/library --virtual
# Disable optional components
python run_on_hardware.py /path/to/library --no-orientation --no-rtc --no-power
"""
import sys
import asyncio
import argparse
import logging
from pathlib import Path
# Add parent directory to path to import dreader
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.hal_hardware import HardwareDisplayHAL
from dreader.main import DReaderApplication, AppConfig
async def main(args):
"""
Main application entry point.
Args:
args: Command line arguments
"""
# Set up logging
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
logger.info("Starting DReader on hardware")
logger.info(f"Library path: {args.library_path}")
logger.info(f"Display size: {args.width}x{args.height}")
logger.info(f"VCOM: {args.vcom}V")
logger.info(f"Virtual display: {args.virtual}")
# Create hardware HAL
logger.info("Initializing hardware HAL...")
hal = HardwareDisplayHAL(
width=args.width,
height=args.height,
vcom=args.vcom,
virtual_display=args.virtual,
auto_sleep_display=args.auto_sleep,
enable_orientation=args.orientation,
enable_rtc=args.rtc,
enable_power_monitor=args.power,
battery_capacity_mah=args.battery_capacity,
)
# Create application config
config = AppConfig(
display_hal=hal,
library_path=args.library_path,
page_size=(args.width, args.height),
auto_save_interval=60,
force_library_mode=args.force_library,
log_level=logging.DEBUG if args.verbose else logging.INFO,
)
# Create application
app = DReaderApplication(config)
try:
# Initialize hardware
logger.info("Initializing hardware...")
await hal.initialize()
# Start application
logger.info("Starting application...")
await app.start()
# Show battery level if available
if args.power and not args.virtual:
try:
battery = await hal.get_battery_level()
logger.info(f"Battery level: {battery:.1f}%")
if await hal.is_low_battery():
logger.warning("⚠️ Low battery!")
except Exception as e:
logger.warning(f"Could not read battery: {e}")
# Main event loop
logger.info("Entering main event loop (Ctrl+C to exit)")
logger.info("")
logger.info("Touch gestures:")
logger.info(" - Swipe left: Next page")
logger.info(" - Swipe right: Previous page")
logger.info(" - Swipe up (from bottom): Open navigation/TOC")
logger.info(" - Swipe down (from top): Open settings")
logger.info(" - Tap: Select book/word/link")
logger.info("")
while app.is_running():
# Get touch event (non-blocking)
event = await hal.get_touch_event()
if event:
logger.debug(f"Touch event: {event.gesture.value} at ({event.x}, {event.y})")
# Handle touch event
await app.handle_touch(event)
# Check battery periodically (every ~100 events)
if args.power and not args.virtual and args.show_battery:
if hasattr(app, '_event_count'):
app._event_count += 1
else:
app._event_count = 1
if app._event_count % 100 == 0:
battery = await hal.get_battery_level()
logger.info(f"Battery: {battery:.1f}%")
# Small delay to prevent CPU spinning
await asyncio.sleep(0.01)
except KeyboardInterrupt:
logger.info("Received interrupt signal, shutting down...")
except Exception as e:
logger.error(f"Error in main loop: {e}", exc_info=True)
finally:
# Shutdown
logger.info("Shutting down application...")
await app.shutdown()
logger.info("Cleaning up hardware...")
await hal.cleanup()
logger.info("DReader stopped")
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Run DReader on e-ink hardware",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Run on real hardware
%(prog)s /home/pi/Books
# Test with virtual display (no hardware required)
%(prog)s /home/pi/Books --virtual
# Custom display size and VCOM
%(prog)s /home/pi/Books --width 1200 --height 1600 --vcom -2.3
# Disable optional sensors
%(prog)s /home/pi/Books --no-orientation --no-rtc --no-power
"""
)
# Required arguments
parser.add_argument(
'library_path',
type=str,
help='Path to directory containing EPUB files'
)
# Display arguments
parser.add_argument(
'--width',
type=int,
default=1872,
help='Display width in pixels (default: 1872)'
)
parser.add_argument(
'--height',
type=int,
default=1404,
help='Display height in pixels (default: 1404)'
)
parser.add_argument(
'--vcom',
type=float,
default=-2.0,
help='E-ink VCOM voltage - CHECK YOUR DISPLAY LABEL! (default: -2.0)'
)
# Virtual display mode
parser.add_argument(
'--virtual',
action='store_true',
help='Use virtual display mode for testing without hardware'
)
# Display features
parser.add_argument(
'--no-auto-sleep',
dest='auto_sleep',
action='store_false',
help='Disable automatic display sleep after updates'
)
# Optional hardware components
parser.add_argument(
'--no-orientation',
dest='orientation',
action='store_false',
help='Disable orientation sensor (BMA400)'
)
parser.add_argument(
'--no-rtc',
dest='rtc',
action='store_false',
help='Disable RTC (PCF8523)'
)
parser.add_argument(
'--no-power',
dest='power',
action='store_false',
help='Disable power monitor (INA219)'
)
# Battery monitoring
parser.add_argument(
'--battery-capacity',
type=float,
default=3000,
help='Battery capacity in mAh (default: 3000)'
)
parser.add_argument(
'--show-battery',
action='store_true',
help='Periodically log battery level'
)
# Application behavior
parser.add_argument(
'--force-library',
action='store_true',
help='Always start in library mode (ignore saved state)'
)
# Debugging
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Enable verbose debug logging'
)
args = parser.parse_args()
# Validate library path
library_path = Path(args.library_path).expanduser()
if not library_path.exists():
parser.error(f"Library path does not exist: {library_path}")
if not library_path.is_dir():
parser.error(f"Library path is not a directory: {library_path}")
args.library_path = str(library_path)
return args
if __name__ == '__main__':
args = parse_args()
# Run async main
try:
asyncio.run(main(args))
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(0)
+293
View File
@@ -0,0 +1,293 @@
#!/usr/bin/env python3
"""
Run DReader on hardware using hardware_config.json configuration.
This script loads all hardware configuration from hardware_config.json,
including display settings, GPIO buttons, and optional components.
Usage:
# Use default config file (hardware_config.json)
python run_on_hardware_config.py
# Use custom config file
python run_on_hardware_config.py --config my_config.json
# Override config settings
python run_on_hardware_config.py --library ~/MyBooks --verbose
"""
import sys
import asyncio
import argparse
import logging
import json
from pathlib import Path
# Add parent directory to path to import dreader
sys.path.insert(0, str(Path(__file__).parent.parent))
from dreader.hal_hardware import HardwareDisplayHAL
from dreader.main import DReaderApplication, AppConfig
from dreader.gpio_buttons import load_button_config_from_dict
def load_config(config_path: str) -> dict:
"""Load hardware configuration from JSON file."""
config_file = Path(config_path)
if not config_file.exists():
raise FileNotFoundError(
f"Configuration file not found: {config_path}\n"
f"Run 'sudo python3 setup_rpi.py' to create it."
)
with open(config_file, 'r') as f:
config = json.load(f)
return config
async def main(args):
"""Main application entry point."""
# Load configuration
logger = logging.getLogger(__name__)
logger.info(f"Loading configuration from {args.config}")
try:
config = load_config(args.config)
except Exception as e:
print(f"Error loading configuration: {e}")
sys.exit(1)
# Apply command-line overrides
if args.library:
config['application']['library_path'] = args.library
if args.verbose:
config['application']['log_level'] = 'DEBUG'
# Set up logging
log_level = getattr(logging, config['application']['log_level'].upper())
logging.basicConfig(
level=log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger.info("="*70)
logger.info("DReader Hardware Mode")
logger.info("="*70)
# Display configuration summary
display_cfg = config['display']
logger.info(f"Display: {display_cfg['width']}x{display_cfg['height']}, VCOM={display_cfg['vcom']}V")
gpio_cfg = config.get('gpio_buttons', {})
if gpio_cfg.get('enabled', False):
logger.info(f"GPIO Buttons: {len(gpio_cfg.get('buttons', []))} configured")
accel_cfg = config.get('accelerometer', {})
if accel_cfg.get('enabled', False):
logger.info("Accelerometer: Enabled")
rtc_cfg = config.get('rtc', {})
if rtc_cfg.get('enabled', False):
logger.info("RTC: Enabled")
power_cfg = config.get('power_monitor', {})
if power_cfg.get('enabled', False):
logger.info("Power Monitor: Enabled")
# Create hardware HAL
logger.info("\nInitializing hardware HAL...")
hal = HardwareDisplayHAL(
width=display_cfg['width'],
height=display_cfg['height'],
vcom=display_cfg['vcom'],
spi_hz=display_cfg.get('spi_hz', 24_000_000),
virtual_display=False,
auto_sleep_display=display_cfg.get('auto_sleep', True),
enable_orientation=accel_cfg.get('enabled', True),
enable_rtc=rtc_cfg.get('enabled', True),
enable_power_monitor=power_cfg.get('enabled', True),
shunt_ohms=power_cfg.get('shunt_ohms', 0.1),
battery_capacity_mah=power_cfg.get('battery_capacity_mah', 3000),
)
# Load accelerometer tilt calibration if enabled
if accel_cfg.get('tilt_enabled', False):
calib_file = accel_cfg.get('calibration_file', 'accelerometer_config.json')
if hal.load_accelerometer_calibration(calib_file):
logger.info(f"Accelerometer tilt detection enabled (calibration from {calib_file})")
else:
logger.warning("Accelerometer tilt detection requested but calibration not loaded")
# Set up GPIO buttons
button_handler = None
if gpio_cfg.get('enabled', False):
logger.info("Setting up GPIO buttons...")
button_handler = load_button_config_from_dict(
config,
screen_width=display_cfg['width'],
screen_height=display_cfg['height']
)
if button_handler:
await button_handler.initialize()
logger.info(f"GPIO buttons initialized: {len(gpio_cfg.get('buttons', []))} buttons")
# Create application config
app_cfg = config['application']
app_config = AppConfig(
display_hal=hal,
library_path=app_cfg['library_path'],
page_size=(display_cfg['width'], display_cfg['height']),
auto_save_interval=app_cfg.get('auto_save_interval', 60),
force_library_mode=app_cfg.get('force_library_mode', False),
log_level=log_level,
)
# Create application
app = DReaderApplication(app_config)
try:
# Initialize hardware
logger.info("Initializing hardware...")
await hal.initialize()
# Start application
logger.info("Starting application...")
await app.start()
# Show battery level if available
if power_cfg.get('enabled', False):
try:
battery = await hal.get_battery_level()
logger.info(f"Battery level: {battery:.1f}%")
if await hal.is_low_battery(power_cfg.get('low_battery_threshold', 20.0)):
logger.warning("⚠️ Low battery!")
except Exception as e:
logger.warning(f"Could not read battery: {e}")
# Main event loop
logger.info("\nApplication ready!")
logger.info("="*70)
event_count = 0
show_battery_interval = power_cfg.get('show_battery_interval', 100)
while app.is_running():
# Check for touch events
touch_event = await hal.get_touch_event()
if touch_event:
logger.debug(f"Touch: {touch_event.gesture.value} at ({touch_event.x}, {touch_event.y})")
await app.handle_touch(touch_event)
event_count += 1
# Check for button events
if button_handler:
button_event = await button_handler.get_button_event()
if button_event:
logger.info(f"Button: {button_event.gesture.value}")
await app.handle_touch(button_event)
event_count += 1
# Check for tilt gestures if enabled
if accel_cfg.get('tilt_enabled', False):
tilt_event = await hal.get_tilt_gesture()
if tilt_event:
logger.info(f"Tilt: {tilt_event.gesture.value}")
await app.handle_touch(tilt_event)
event_count += 1
# Show battery periodically
if power_cfg.get('enabled', False) and event_count % show_battery_interval == 0 and event_count > 0:
try:
battery = await hal.get_battery_level()
logger.info(f"Battery: {battery:.1f}%")
except:
pass
# Small delay to prevent CPU spinning
await asyncio.sleep(0.01)
except KeyboardInterrupt:
logger.info("\nReceived interrupt signal, shutting down...")
except Exception as e:
logger.error(f"Error in main loop: {e}", exc_info=True)
finally:
# Shutdown
logger.info("Shutting down application...")
await app.shutdown()
logger.info("Cleaning up GPIO buttons...")
if button_handler:
await button_handler.cleanup()
logger.info("Cleaning up hardware...")
await hal.cleanup()
logger.info("DReader stopped")
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
description="Run DReader using hardware_config.json",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Configuration:
Edit hardware_config.json to configure your hardware settings.
Run 'sudo python3 setup_rpi.py' to create/update the config file.
Examples:
# Use default config
%(prog)s
# Use custom config file
%(prog)s --config my_hardware.json
# Override library path
%(prog)s --library ~/MyBooks
# Enable verbose logging
%(prog)s --verbose
"""
)
parser.add_argument(
'--config',
type=str,
default='hardware_config.json',
help='Path to hardware configuration file (default: hardware_config.json)'
)
parser.add_argument(
'--library',
type=str,
help='Override library path from config'
)
parser.add_argument(
'-v', '--verbose',
action='store_true',
help='Enable verbose debug logging'
)
args = parser.parse_args()
return args
if __name__ == '__main__':
args = parse_args()
# Run async main
try:
asyncio.run(main(args))
except KeyboardInterrupt:
print("\nInterrupted by user")
sys.exit(0)