37 KiB
DReader Hardware Abstraction Layer (HAL) Implementation Specification
Version: 1.0 Date: 2025-11-09 Target: Hardware driver developers implementing platform-specific HALs
Table of Contents
- Overview
- Architecture
- HAL Interface Specification
- Touch Event & Gesture System
- Implementation Requirements
- Testing & Validation
- Reference Implementation
- Platform-Specific Considerations
- Example Implementations
1. Overview
1.1 Purpose
The DReader HAL provides a platform-independent abstraction for:
- Display output: Rendering PIL images to hardware screen
- Touch input: Capturing touch/mouse events and converting to gestures
- Hardware control: Brightness, power management, etc.
1.2 Design Goals
- Portability: Same application code runs on different hardware platforms
- Async-first: All operations are async for non-blocking I/O
- Testability: Easy desktop testing before deploying to device
- Simplicity: Minimal interface with clear contracts
1.3 Key Responsibilities
What the HAL MUST do:
- Display PIL Images on physical screen
- Detect touch events and classify gestures
- Return standardized
TouchEventobjects - Handle platform-specific initialization/cleanup
What the HAL does NOT do:
- Application logic (handled by
EbookReaderandDReaderApplication) - Gesture routing (handled by
GestureRouter) - State management (handled by
StateManager) - Content rendering (handled by pyWebLayout)
2. Architecture
2.1 System Layers
┌─────────────────────────────────────────────────────────┐
│ DReaderApplication (main.py) │
│ - State management, mode switching, event routing │
└───────────────────────┬─────────────────────────────────┘
│
│ Uses DisplayHAL interface
↓
┌─────────────────────────────────────────────────────────┐
│ DisplayHAL (Abstract Interface) │
│ - show_image(PIL.Image) │
│ - get_touch_event() -> TouchEvent │
│ - set_brightness(int) │
│ - initialize() / cleanup() │
└───────────────────────┬─────────────────────────────────┘
│
│ Implemented by
↓
┌─────────────────────────────────────────────────────────┐
│ Platform-Specific HAL Implementation │
│ │
│ Examples: │
│ • PygameDisplayHAL (Desktop/Testing) │
│ • EinkDisplayHAL (E-ink devices) │
│ • KoboDisplayHAL (Kobo readers) │
│ • RemarkableDisplayHAL (reMarkable tablets) │
│ • [Your Custom HAL] │
└───────────────────────┬─────────────────────────────────┘
│
│ Controls
↓
┌─────────────────────────────────────────────────────────┐
│ Hardware Drivers │
│ • Framebuffer / Display controller │
│ • Touch sensor / Digitizer │
│ • Backlight / Frontlight │
│ • Power management │
└─────────────────────────────────────────────────────────┘
2.2 Event Flow
Hardware Touch Sensor
↓
HAL Driver
↓
Touch Detection
↓
Gesture Classification ← HAL responsibility
↓
TouchEvent
↓
DReaderApplication.handle_touch()
↓
GestureRouter
↓
Gesture Handlers
↓
GestureResponse
↓
Display Update
↓
HAL.show_image()
↓
Hardware Display
3. HAL Interface Specification
3.1 Base Interface: DisplayHAL
File: dreader/hal.py
from abc import ABC, abstractmethod
from typing import Optional
from PIL import Image
from dreader.gesture import TouchEvent
class DisplayHAL(ABC):
"""Abstract interface for display and input hardware"""
3.2 Required Methods
3.2.1 show_image(image: PIL.Image.Image) ⭐ CRITICAL
Purpose: Display a PIL Image on the physical screen
Contract:
async def show_image(self, image: Image.Image):
"""
Display a PIL Image on the screen.
Args:
image: PIL Image to display (typically RGB mode)
Requirements:
- MUST handle image format conversion if needed
- MUST handle resolution mismatch (scale/crop)
- MUST update physical display hardware
- SHOULD complete within 100ms for responsiveness
- SHOULD support e-ink optimization if applicable
Image Specifications:
- Format: Usually RGB (may be 'L' for grayscale)
- Size: Typically matches page_size config (e.g., 800x1200)
- Color depth: 8-bit per channel RGB
"""
pass
Implementation Notes:
- E-ink devices: Convert to grayscale, apply dithering, use appropriate refresh mode
- Color displays: May need RGB↔BGR conversion depending on driver
- Framebuffer: May need to write directly to
/dev/fb0or use platform API - Performance: Cache converted images if format conversion is expensive
Example (Pseudocode):
async def show_image(self, image: Image.Image):
# Convert to device format
if self.is_eink:
image = image.convert('L') # Grayscale
image = apply_dithering(image)
# Resize if needed
if image.size != (self.width, self.height):
image = image.resize((self.width, self.height), Image.LANCZOS)
# Write to framebuffer or display API
self.framebuffer.write(image)
# Refresh display
self.display_controller.refresh()
3.2.2 get_touch_event() -> Optional[TouchEvent] ⭐ CRITICAL
Purpose: Wait for and return the next touch event with gesture classification
Contract:
async def get_touch_event(self) -> Optional[TouchEvent]:
"""
Get the next touch event from hardware.
Returns:
TouchEvent if available, None if no event
Requirements:
- MUST classify gesture type (tap, swipe, long press, etc.)
- MUST return None immediately if no event (non-blocking)
- SHOULD detect gestures accurately with sensible thresholds
- MUST provide accurate pixel coordinates
- MAY implement timeouts for responsiveness
Gesture Detection Thresholds (Recommended):
- Tap: < 30px movement, < 300ms duration
- Long press: < 30px movement, >= 500ms duration
- Swipe: >= 30px movement, < 500ms duration
- Drag: >= 30px movement, sustained motion
- Pinch: Two-finger distance change
Coordinate System:
- Origin (0, 0) = top-left corner
- X increases right
- Y increases down
- Range: (0, 0) to (width-1, height-1)
"""
pass
TouchEvent Format:
@dataclass
class TouchEvent:
gesture: GestureType # Required: gesture classification
x: int # Required: primary touch X
y: int # Required: primary touch Y
x2: Optional[int] = None # Optional: secondary touch X (pinch)
y2: Optional[int] = None # Optional: secondary touch Y (pinch)
timestamp_ms: float = 0 # Optional: timestamp
Implementation Pattern:
async def get_touch_event(self) -> Optional[TouchEvent]:
# Read from touch hardware
touch_data = await self.read_touch_sensor()
if not touch_data:
return None
# Track touch state
if touch_data.is_down:
self._touch_start = (touch_data.x, touch_data.y)
self._touch_start_time = time.time()
elif touch_data.is_up:
# Calculate gesture
dx = touch_data.x - self._touch_start[0]
dy = touch_data.y - self._touch_start[1]
distance = (dx**2 + dy**2) ** 0.5
duration = time.time() - self._touch_start_time
# Classify gesture
if distance < 30:
if duration >= 0.5:
gesture = GestureType.LONG_PRESS
else:
gesture = GestureType.TAP
else:
# Swipe direction
if abs(dx) > abs(dy):
gesture = GestureType.SWIPE_LEFT if dx < 0 else GestureType.SWIPE_RIGHT
else:
gesture = GestureType.SWIPE_UP if dy < 0 else GestureType.SWIPE_DOWN
return TouchEvent(gesture, touch_data.x, touch_data.y)
return None
3.2.3 set_brightness(level: int)
Purpose: Control display brightness/frontlight
Contract:
async def set_brightness(self, level: int):
"""
Set display brightness.
Args:
level: Brightness level (0-10)
0 = dimmest (may be off)
10 = brightest
Requirements:
- MUST accept range 0-10
- SHOULD map to hardware's native range
- MAY be no-op if brightness control unavailable
- SHOULD persist setting if supported by hardware
"""
pass
Implementation Notes:
- Map 0-10 to hardware range (e.g., 0-255)
- E-ink frontlight: Control LED PWM or similar
- LCD backlight: Control via sysfs or platform API
- Desktop: May be no-op or control window alpha
3.2.4 initialize() (Optional)
Purpose: One-time hardware initialization
Contract:
async def initialize(self):
"""
Initialize the display hardware.
Called once before application starts.
Typical Tasks:
- Open framebuffer device
- Initialize touch sensor
- Configure display controller
- Set default brightness
- Calibrate touch screen
- Clear screen to white/black
"""
pass
3.2.5 cleanup() (Optional)
Purpose: Hardware cleanup on shutdown
Contract:
async def cleanup(self):
"""
Clean up display hardware resources.
Called during application shutdown.
Typical Tasks:
- Close file descriptors
- Restore default display state
- Power down display
- Release hardware locks
"""
pass
3.2.6 show_message(message: str, duration: float) (Optional)
Purpose: Display simple text messages (loading screens, errors)
Contract:
async def show_message(self, message: str, duration: float = 2.0):
"""
Display a text message.
Args:
message: Text to display
duration: How long to show (seconds)
Default implementation provided (creates PIL Image with text).
Override for platform-specific message display.
"""
pass
3.3 Extended Interface: EventLoopHAL
Purpose: For platforms that provide their own event loop (Pygame, Qt, Tkinter)
File: dreader/hal.py
class EventLoopHAL(DisplayHAL):
"""Extended HAL that provides its own event loop"""
@abstractmethod
async def run_event_loop(self, app):
"""
Run the platform's event loop.
Args:
app: DReaderApplication instance
Typical Flow:
1. await self.initialize()
2. await app.start()
3. Enter event loop:
a. Get events
b. await app.handle_touch(event)
c. Handle quit/close
4. await app.shutdown()
5. await self.cleanup()
"""
pass
Use Case: When the platform requires control of the main loop (e.g., Pygame, Qt)
4. Touch Event & Gesture System
4.1 GestureType Enumeration
File: dreader/gesture.py
All gestures that HAL must be able to detect:
class GestureType(Enum):
TAP = "tap" # Single finger tap
LONG_PRESS = "long_press" # Hold for 500ms+
SWIPE_LEFT = "swipe_left" # Horizontal swipe left
SWIPE_RIGHT = "swipe_right" # Horizontal swipe right
SWIPE_UP = "swipe_up" # Vertical swipe up
SWIPE_DOWN = "swipe_down" # Vertical swipe down
PINCH_IN = "pinch_in" # Two-finger pinch (zoom out)
PINCH_OUT = "pinch_out" # Two-finger spread (zoom in)
DRAG_START = "drag_start" # Start dragging
DRAG_MOVE = "drag_move" # Continue dragging
DRAG_END = "drag_end" # End dragging
4.2 Gesture Priority & Requirements
4.2.1 Essential Gestures (MUST implement)
| Gesture | Usage | Detection |
|---|---|---|
| TAP | Link/button activation, word selection | < 30px movement, < 300ms |
| SWIPE_LEFT | Next page | Horizontal, dx < -30px |
| SWIPE_RIGHT | Previous page | Horizontal, dx > 30px |
| SWIPE_UP | Open navigation/TOC | Vertical, dy < -30px |
| SWIPE_DOWN | Open settings (from top 20%) | Vertical, dy > 30px |
4.2.2 Important Gestures (SHOULD implement)
| Gesture | Usage | Detection |
|---|---|---|
| LONG_PRESS | Word definition, context menu | < 30px movement, >= 500ms |
| PINCH_OUT | Increase font size | Two-finger distance increase |
| PINCH_IN | Decrease font size | Two-finger distance decrease |
4.2.3 Advanced Gestures (MAY implement)
| Gesture | Usage | Detection |
|---|---|---|
| DRAG_START/MOVE/END | Text selection | Sustained motion >= 30px |
4.3 Gesture Detection Algorithm
4.3.1 State Machine
IDLE
↓ touch_down
TOUCHING
↓ timeout(500ms) → LONG_PRESS
↓ movement > 30px → MOVING
↓ touch_up → TAP
MOVING
↓ touch_up → SWIPE_*
↓ sustained → DRAG_*
4.3.2 Recommended Thresholds
# Distance thresholds
TAP_THRESHOLD = 30 # pixels
SWIPE_MIN_DISTANCE = 30 # pixels
DRAG_THRESHOLD = 30 # pixels
# Time thresholds
LONG_PRESS_DURATION = 0.5 # seconds (500ms)
TAP_MAX_DURATION = 0.3 # seconds (300ms)
SWIPE_MAX_DURATION = 0.5 # seconds
# Direction thresholds
SWIPE_ANGLE_THRESHOLD = 45 # degrees (for horizontal vs vertical)
4.3.3 Pseudocode
class GestureDetector:
def on_touch_down(self, x, y):
self.start_pos = (x, y)
self.start_time = time.now()
self.state = TOUCHING
def on_touch_move(self, x, y):
dx = x - self.start_pos[0]
dy = y - self.start_pos[1]
distance = sqrt(dx**2 + dy**2)
if distance > TAP_THRESHOLD and self.state == TOUCHING:
self.state = MOVING
# Could emit DRAG_START here
def on_touch_up(self, x, y):
dx = x - self.start_pos[0]
dy = y - self.start_pos[1]
distance = sqrt(dx**2 + dy**2)
duration = time.now() - self.start_time
if distance < TAP_THRESHOLD:
if duration >= LONG_PRESS_DURATION:
return GestureType.LONG_PRESS
else:
return GestureType.TAP
else:
# Determine swipe direction
if abs(dx) > abs(dy): # Horizontal
if dx > 0:
return GestureType.SWIPE_RIGHT
else:
return GestureType.SWIPE_LEFT
else: # Vertical
if dy > 0:
return GestureType.SWIPE_DOWN
else:
return GestureType.SWIPE_UP
4.4 TouchEvent Construction
# Example: Creating a TAP event
event = TouchEvent(
gesture=GestureType.TAP,
x=450,
y=320,
timestamp_ms=time.time() * 1000
)
# Example: Creating a SWIPE event
event = TouchEvent(
gesture=GestureType.SWIPE_LEFT,
x=600, # End position
y=400,
timestamp_ms=time.time() * 1000
)
# Example: Creating a PINCH event (two fingers)
event = TouchEvent(
gesture=GestureType.PINCH_OUT,
x=400, # Finger 1
y=500,
x2=600, # Finger 2
y2=700,
timestamp_ms=time.time() * 1000
)
5. Implementation Requirements
5.1 Mandatory Features
✅ MUST implement:
show_image()- Display PIL Imagesget_touch_event()- Return TouchEvents- Basic gesture detection (TAP, SWIPE_LEFT/RIGHT/UP/DOWN)
- Correct coordinate system (0,0 = top-left)
- Async/await support for all methods
5.2 Recommended Features
⭐ SHOULD implement:
initialize()andcleanup()- Proper lifecycle managementset_brightness()- Brightness control- LONG_PRESS gesture
- PINCH_IN/OUT gestures (if multitouch available)
- Efficient image format conversion
- E-ink optimization (partial refresh, dithering)
5.3 Optional Features
💡 MAY implement:
show_message()override - Custom loading screens- DRAG gestures - Text selection
- Touch pressure sensitivity
- Multi-touch tracking
- Hardware acceleration
- Custom refresh modes (e-ink A2/DU/GC16)
5.4 Performance Targets
| Metric | Target | Critical |
|---|---|---|
| Image display latency | < 100ms | < 500ms |
| Touch event latency | < 50ms | < 200ms |
| Frame rate (color) | 30 FPS | 10 FPS |
| E-ink refresh (fast) | < 200ms | < 500ms |
| E-ink refresh (full) | < 1000ms | < 2000ms |
| Gesture detection accuracy | > 95% | > 80% |
6. Testing & Validation
6.1 Unit Tests
Test each HAL method in isolation:
import pytest
from your_hal import YourDisplayHAL
@pytest.mark.asyncio
async def test_show_image():
hal = YourDisplayHAL()
await hal.initialize()
# Create test image
img = Image.new('RGB', (800, 1200), color=(255, 0, 0))
# Should not raise
await hal.show_image(img)
await hal.cleanup()
@pytest.mark.asyncio
async def test_gesture_detection():
hal = YourDisplayHAL()
await hal.initialize()
# Simulate tap
event = await hal.get_touch_event()
assert event.gesture == GestureType.TAP
assert 0 <= event.x < hal.width
assert 0 <= event.y < hal.height
6.2 Integration Tests
Test with actual DReaderApplication:
from dreader.main import DReaderApplication, AppConfig
async def test_integration():
hal = YourDisplayHAL()
config = AppConfig(
display_hal=hal,
library_path="./test_books",
page_size=(800, 1200)
)
app = DReaderApplication(config)
await app.start()
# Simulate touch
event = TouchEvent(GestureType.TAP, 400, 600)
await app.handle_touch(event)
await app.shutdown()
6.3 Manual Test Checklist
- Display shows full-screen images without distortion
- Tap on link navigates to chapter
- Swipe left advances page
- Swipe right goes back page
- Swipe up opens TOC overlay
- Swipe down (from top) opens settings
- Long press on word shows definition
- Pinch gestures change font size (if supported)
- Brightness control works (if supported)
- No memory leaks after 100+ page turns
- No display artifacts or tearing
6.4 Validation Tools
Debug Logging:
import logging
logger = logging.getLogger(__name__)
async def show_image(self, image):
logger.info(f"Displaying image: {image.size}, {image.mode}")
# ... implementation
async def get_touch_event(self):
event = # ... detect gesture
logger.info(f"Gesture detected: {event.gesture} at ({event.x}, {event.y})")
return event
Touch Coordinate Visualization:
- Draw red dot at touch coordinates
- Display gesture type as text overlay
- Show swipe trajectory lines
7. Reference Implementation
7.1 PygameDisplayHAL
File: dreader/hal_pygame.py
The Pygame HAL is a complete reference implementation for desktop testing.
Key features:
- Mouse → touch event conversion
- Gesture detection algorithm
- Keyboard shortcuts
- Debug logging
Study this implementation for:
- Gesture detection state machine
- Coordinate handling
- Event loop structure
- Error handling patterns
7.2 Minimal HAL Template
from dreader.hal import DisplayHAL
from dreader.gesture import TouchEvent, GestureType
from PIL import Image
from typing import Optional
import asyncio
class MinimalHAL(DisplayHAL):
"""Minimal HAL implementation template"""
def __init__(self, width: int = 800, height: int = 1200):
self.width = width
self.height = height
# Gesture tracking
self._touch_start = None
self._touch_start_time = 0
async def initialize(self):
"""Initialize hardware"""
# TODO: Open framebuffer, initialize touch sensor
pass
async def cleanup(self):
"""Cleanup hardware"""
# TODO: Close file descriptors, cleanup
pass
async def show_image(self, image: Image.Image):
"""Display image on screen"""
# Convert to device format
if image.mode != 'RGB':
image = image.convert('RGB')
# Resize if needed
if image.size != (self.width, self.height):
image = image.resize(
(self.width, self.height),
Image.Resampling.LANCZOS
)
# TODO: Write to framebuffer or display API
# framebuffer.write(image)
async def get_touch_event(self) -> Optional[TouchEvent]:
"""Get next touch event"""
# TODO: Read from touch hardware
# touch_data = await read_touch_sensor()
# If no touch, return None
# if not touch_data:
# return None
# Classify gesture based on touch_down/touch_up
# See gesture detection algorithm in section 4.3
pass
async def set_brightness(self, level: int):
"""Set brightness (0-10)"""
# Map to hardware range
hardware_value = int(level * 255 / 10)
# TODO: Write to brightness control
# with open('/sys/class/backlight/.../brightness', 'w') as f:
# f.write(str(hardware_value))
8. Platform-Specific Considerations
8.1 E-Ink Devices (Kobo, Kindle, reMarkable)
Display Characteristics:
- Grayscale only (8-bit per pixel)
- Slow refresh (200ms - 1000ms)
- Multiple refresh modes (A2, DU, GC16, GL16)
- Requires dithering for images
Optimization Strategies:
1. Partial Refresh:
async def show_image(self, image: Image.Image, partial: bool = True):
if partial:
# Use fast A2 mode for text-only updates
self.controller.set_mode('A2')
else:
# Full GC16 refresh every N pages
self.controller.set_mode('GC16')
self.framebuffer.write(image)
self.controller.refresh()
2. Dithering:
from PIL import ImageDraw
def apply_dithering(image: Image.Image) -> Image.Image:
# Convert to grayscale
image = image.convert('L')
# Apply Floyd-Steinberg dithering
image = image.convert('1', dither=Image.FLOYDSTEINBERG)
image = image.convert('L')
return image
3. Refresh Strategy:
- Text pages: A2 mode (fast, ~200ms)
- Image-heavy pages: GC16 mode (slow, ~1000ms)
- Full refresh every 5-10 pages to clear ghosting
Touch Input:
- Often resistive touch (single-point only)
- Lower resolution than display
- May require calibration
Example: Kobo HAL skeleton:
class KoboDisplayHAL(DisplayHAL):
def __init__(self):
self.fb = open('/dev/fb0', 'wb')
self.touch = open('/dev/input/event0', 'rb')
self.width = 1072
self.height = 1448
self.refresh_counter = 0
async def show_image(self, image: Image.Image):
# Convert to grayscale
image = image.convert('L')
# Resize
image = image.resize((self.width, self.height))
# Write to framebuffer
self.fb.write(image.tobytes())
# Decide refresh mode
self.refresh_counter += 1
if self.refresh_counter % 10 == 0:
# Full refresh every 10 pages
self._trigger_refresh('GC16')
else:
# Fast refresh
self._trigger_refresh('A2')
8.2 Linux Framebuffer
Display:
async def show_image(self, image: Image.Image):
# Open framebuffer
with open('/dev/fb0', 'wb') as fb:
# Convert to RGB565 or RGB888 depending on device
if self.color_depth == 16:
# RGB565
data = image.convert('RGB').tobytes('raw', 'RGB')
# Convert to RGB565 packed format
data = self._convert_rgb888_to_rgb565(data)
else:
# RGB888
data = image.convert('RGB').tobytes()
fb.write(data)
Touch Input (evdev):
import evdev
async def get_touch_event(self) -> Optional[TouchEvent]:
device = evdev.InputDevice('/dev/input/event0')
async for event in device.async_read_loop():
if event.type == evdev.ecodes.EV_ABS:
if event.code == evdev.ecodes.ABS_X:
self.current_x = event.value
elif event.code == evdev.ecodes.ABS_Y:
self.current_y = event.value
elif event.type == evdev.ecodes.EV_KEY:
if event.code == evdev.ecodes.BTN_TOUCH:
if event.value == 1: # Touch down
self._on_touch_down(self.current_x, self.current_y)
else: # Touch up
gesture = self._on_touch_up(self.current_x, self.current_y)
if gesture:
return TouchEvent(gesture, self.current_x, self.current_y)
8.3 Embedded Systems (Raspberry Pi, etc.)
Recommendations:
- Use hardware-accelerated PIL if available (Pillow-SIMD)
- Minimize memory allocations (reuse image buffers)
- Consider using mmap for framebuffer access
- Implement watchdog for crash recovery
import mmap
class EmbeddedHAL(DisplayHAL):
def __init__(self):
self.fb_fd = os.open('/dev/fb0', os.O_RDWR)
self.fb_size = self.width * self.height * 4 # RGB888
self.fb_mmap = mmap.mmap(
self.fb_fd,
self.fb_size,
mmap.MAP_SHARED,
mmap.PROT_READ | mmap.PROT_WRITE
)
async def show_image(self, image: Image.Image):
# Write directly to mmap'd framebuffer
data = image.convert('RGB').tobytes()
self.fb_mmap.seek(0)
self.fb_mmap.write(data)
8.4 Desktop Testing (Windows/macOS/Linux)
Use PygameDisplayHAL (already provided) or implement similar with:
- Tkinter: For simple cross-platform windows
- Qt (PyQt5/PySide6): For advanced GUI features
- SDL2: For game-like responsiveness
9. Example Implementations
9.1 Complete Example: Pygame HAL
See dreader/hal_pygame.py for full implementation.
Key sections to study:
- Lines 158-240: Gesture detection in
get_touch_event() - Lines 111-140: Image display in
show_image() - Lines 302-354: Event loop in
run_event_loop()
9.2 Pseudocode: E-Ink HAL
from dreader.hal import DisplayHAL
from dreader.gesture import TouchEvent, GestureType
from PIL import Image
import time
class EInkHAL(DisplayHAL):
"""E-Ink device HAL with partial refresh support"""
def __init__(self, width=1072, height=1448):
self.width = width
self.height = height
self.refresh_count = 0
# Hardware interfaces
self.framebuffer = None
self.touch_device = None
self.frontlight = None
# Gesture tracking
self._touch_down_pos = None
self._touch_down_time = 0
async def initialize(self):
"""Initialize e-ink display and touch sensor"""
# Open framebuffer
self.framebuffer = open('/dev/fb0', 'r+b')
# Open touch device
self.touch_device = evdev.InputDevice('/dev/input/event0')
# Initialize frontlight
self.frontlight = open('/sys/class/backlight/mxc_mst716/brightness', 'w')
# Clear screen
blank = Image.new('L', (self.width, self.height), 255)
await self.show_image(blank)
async def cleanup(self):
"""Cleanup hardware resources"""
if self.framebuffer:
self.framebuffer.close()
if self.frontlight:
self.frontlight.close()
async def show_image(self, image: Image.Image):
"""Display image with e-ink optimization"""
# Convert to grayscale
if image.mode != 'L':
image = image.convert('L')
# Resize if needed
if image.size != (self.width, self.height):
image = image.resize((self.width, self.height), Image.LANCZOS)
# Write to framebuffer
self.framebuffer.seek(0)
self.framebuffer.write(image.tobytes())
# Decide refresh mode
self.refresh_count += 1
if self.refresh_count % 10 == 0:
# Full refresh every 10 pages (remove ghosting)
self._trigger_refresh('FULL')
await asyncio.sleep(1.0) # Wait for full refresh
else:
# Partial refresh (fast)
self._trigger_refresh('PARTIAL')
await asyncio.sleep(0.2) # Wait for partial refresh
def _trigger_refresh(self, mode: str):
"""Trigger display refresh"""
if mode == 'FULL':
# Device-specific: trigger full GC16 refresh
# Example: ioctl(MXCFB_SEND_UPDATE, waveform=GC16)
pass
else:
# Device-specific: trigger partial A2 refresh
# Example: ioctl(MXCFB_SEND_UPDATE, waveform=A2)
pass
async def get_touch_event(self) -> Optional[TouchEvent]:
"""Read touch events from evdev"""
current_x = 0
current_y = 0
# Read events (with timeout)
try:
events = await asyncio.wait_for(
self._read_touch_events(),
timeout=0.1
)
except asyncio.TimeoutError:
return None
for event in events:
if event.type == evdev.ecodes.EV_ABS:
if event.code == evdev.ecodes.ABS_X:
current_x = event.value
elif event.code == evdev.ecodes.ABS_Y:
current_y = event.value
elif event.type == evdev.ecodes.EV_KEY:
if event.code == evdev.ecodes.BTN_TOUCH:
if event.value == 1: # Touch down
self._touch_down_pos = (current_x, current_y)
self._touch_down_time = time.time()
else: # Touch up
if not self._touch_down_pos:
continue
# Calculate gesture
dx = current_x - self._touch_down_pos[0]
dy = current_y - self._touch_down_pos[1]
distance = (dx**2 + dy**2) ** 0.5
duration = time.time() - self._touch_down_time
# Classify
if distance < 30:
if duration >= 0.5:
gesture = GestureType.LONG_PRESS
else:
gesture = GestureType.TAP
else:
if abs(dx) > abs(dy):
gesture = GestureType.SWIPE_LEFT if dx < 0 else GestureType.SWIPE_RIGHT
else:
gesture = GestureType.SWIPE_UP if dy < 0 else GestureType.SWIPE_DOWN
self._touch_down_pos = None
return TouchEvent(gesture, current_x, current_y)
return None
async def _read_touch_events(self):
"""Helper to read touch events with async"""
events = []
async for event in self.touch_device.async_read_loop():
events.append(event)
if event.type == evdev.ecodes.EV_SYN:
break # End of event batch
return events
async def set_brightness(self, level: int):
"""Set frontlight brightness"""
# Map 0-10 to hardware range (e.g., 0-100)
value = level * 10
self.frontlight.seek(0)
self.frontlight.write(str(value))
self.frontlight.flush()
9.3 Usage Example
import asyncio
from your_hal import YourDisplayHAL
from dreader.main import DReaderApplication, AppConfig
async def main():
# Create HAL
hal = YourDisplayHAL(width=800, height=1200)
# Configure application
config = AppConfig(
display_hal=hal,
library_path="/home/user/Books",
page_size=(800, 1200),
log_level=logging.INFO
)
# Create application
app = DReaderApplication(config)
# If using EventLoopHAL:
await hal.run_event_loop(app)
# OR if using basic DisplayHAL:
# await app.start()
# while app.is_running():
# event = await hal.get_touch_event()
# if event:
# await app.handle_touch(event)
# await asyncio.sleep(0.01)
# await app.shutdown()
if __name__ == "__main__":
asyncio.run(main())
10. Troubleshooting
Common Issues
Issue: Images not displaying
- Check framebuffer path (
/dev/fb0) - Verify color format (RGB vs BGR)
- Check image size matches display
- Verify write permissions
Issue: Touch not detected
- Check evdev device path
- Verify touch coordinates are mapped correctly
- Check coordinate system (origin, scaling)
- Test with
evtestutility
Issue: Gestures not recognized
- Check thresholds (TAP_THRESHOLD, etc.)
- Add debug logging to gesture detection
- Verify touch up/down events are paired
- Check timestamp accuracy
Issue: Slow performance
- Profile image conversion (use cProfile)
- Cache converted images
- Use partial refresh on e-ink
- Consider hardware acceleration
11. Resources
Documentation
- DReader Architecture:
INTEGRATION_DOCUMENTATION_INDEX.md - pyWebLayout Integration:
PYWEBLAYOUT_INTEGRATION_GUIDE.md - Async Rendering:
ASYNC_DIRTY_FLAG_RENDERING.md
Reference Code
- Pygame HAL:
dreader/hal_pygame.py - HAL Interface:
dreader/hal.py - Gesture Types:
dreader/gesture.py - Main Application:
dreader/main.py
External References
- Linux Framebuffer: https://www.kernel.org/doc/Documentation/fb/api.txt
- evdev Input: https://python-evdev.readthedocs.io/
- PIL/Pillow: https://pillow.readthedocs.io/
- E-Ink Controllers: Device-specific documentation (Kobo, Kindle, reMarkable)
12. Checklist for HAL Implementers
Use this checklist to track your implementation:
Setup
- Read this specification completely
- Study PygameDisplayHAL reference implementation
- Set up development environment
- Test DReader with Pygame HAL first
Core Implementation
- Create HAL class inheriting from DisplayHAL
- Implement
__init__()with device configuration - Implement
initialize()- hardware setup - Implement
cleanup()- resource cleanup - Implement
show_image()- display rendering - Implement
get_touch_event()- input handling - Implement gesture detection algorithm
- Implement
set_brightness()(if supported)
Testing
- Unit test each method
- Test with minimal application
- Test with full DReader application
- Test all essential gestures
- Performance profiling
- Memory leak testing
- Long-term stability testing
Optimization
- Profile and optimize hot paths
- Implement caching where appropriate
- Add platform-specific optimizations
- Tune gesture thresholds
- Optimize display refresh strategy
Documentation
- Document platform-specific requirements
- Document known limitations
- Create usage examples
- Write troubleshooting guide
Version History
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2025-11-09 | Initial specification |
End of HAL Implementation Specification
For questions or issues, please refer to the DReader documentation or create an issue in the project repository.