chore: tidy repository layout and consolidate hardware docs
Root had grown to 30 entries, most of it generated output and one-off scripts. It now holds 12. Docs: - Merge HARDWARE_SETUP, HARDWARE_PINOUT, GPIO_BUTTONS and ACCELEROMETER_PAGE_FLIP into a single docs/HARDWARE.md - Move ARCHITECTURE, REQUIREMENTS and HAL_IMPLEMENTATION_SPEC to docs/, leaving only README.md in the root - Update cross-references in README, setup_rpi.py and install_hardware_drivers.sh, and re-base ARCHITECTURE's source links Merging surfaced three errors, reconciled against hardware_config.json: - The power button was documented as GPIO 21 to GND with a pull-up. It is active high (pull_up: false); wiring it as documented reads as permanently pressed. - GPIO_BUTTONS used GPIO 23 for next-page; it is GPIO 27. - The FT5316 INT pin was routed to GPIO 27, which collides with the next-page button. Now documented as a conflict. Scripts: - Move debug_overlay_links.py and debug_previous_page.py to scripts/debug/ - Rename test_pagination_visual.py to scripts/debug/visualize_pagination.py; it renders output and asserts nothing, so the test_ prefix was misleading - Fix the __file__-relative paths these three relied on - Track update_pyweblayout.sh under scripts/ Tests: - Reword the backward-navigation tests, which described a pyWebLayout bug that is now fixed, as regression tests Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,552 @@
|
||||
# DReader Application Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
DReader is a full-featured ebook reader application built on top of [pyWebLayout](https://gitea.tourolle.paris/dtourolle/pyWebLayout). It provides a complete reading experience with navigation, bookmarks, highlights, and customizable display settings.
|
||||
|
||||
## System Architecture
|
||||
|
||||
### High-Level Component Structure
|
||||
|
||||
```
|
||||
dreader/
|
||||
├── application.py # Main EbookReader class (coordinator)
|
||||
├── managers/ # Specialized management modules
|
||||
│ ├── document.py # Document loading (EPUB/HTML)
|
||||
│ ├── settings.py # Font and spacing controls
|
||||
│ └── highlight_coordinator.py # Text highlighting
|
||||
├── handlers/
|
||||
│ └── gestures.py # Touch event routing
|
||||
├── overlays/ # UI overlay system
|
||||
│ ├── base.py # Base overlay functionality
|
||||
│ ├── navigation.py # TOC and bookmarks overlay
|
||||
│ └── settings.py # Settings overlay
|
||||
├── library.py # Library browsing and book selection
|
||||
├── state.py # Application state persistence
|
||||
├── html_generator.py # HTML generation for overlays
|
||||
└── gesture.py # Gesture definitions and responses
|
||||
```
|
||||
|
||||
### Relationship to pyWebLayout
|
||||
|
||||
**pyWebLayout** provides low-level rendering primitives:
|
||||
- Text layout and rendering algorithms
|
||||
- Document structure and pagination
|
||||
- Query systems for interactive content
|
||||
- Core rendering infrastructure
|
||||
|
||||
**DReader** is an application framework that:
|
||||
- Combines pyWebLayout components into a complete reader
|
||||
- Provides high-level APIs for common ereader tasks
|
||||
- Manages application state (bookmarks, highlights, positions)
|
||||
- Handles business logic for gestures and interactions
|
||||
|
||||
Think of it as:
|
||||
- **pyWebLayout** = React (library)
|
||||
- **DReader** = Next.js (framework)
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. EbookReader (Main Coordinator)
|
||||
|
||||
**Location**: [application.py](../dreader/application.py)
|
||||
|
||||
The central orchestrator that coordinates all subsystems:
|
||||
|
||||
```python
|
||||
class EbookReader:
|
||||
"""Main ebook reader application"""
|
||||
|
||||
# Core dependencies
|
||||
manager: EreaderLayoutManager # pyWebLayout layout engine
|
||||
doc_manager: DocumentManager # Document loading
|
||||
settings_manager: SettingsManager # Display settings
|
||||
highlight_coordinator: HighlightCoordinator # Text highlighting
|
||||
gesture_router: GestureRouter # Gesture handling
|
||||
overlay_manager: OverlayManager # Overlay rendering
|
||||
```
|
||||
|
||||
**Key Responsibilities**:
|
||||
- Document lifecycle (load, close)
|
||||
- Page navigation (next, previous, chapters)
|
||||
- Bookmark management
|
||||
- Position persistence
|
||||
- Settings coordination
|
||||
- Gesture event routing
|
||||
|
||||
### 2. Document Manager
|
||||
|
||||
**Location**: [managers/document.py](../dreader/managers/document.py)
|
||||
|
||||
Handles document loading and metadata extraction.
|
||||
|
||||
**Responsibilities**:
|
||||
- Load EPUB files via pyWebLayout
|
||||
- Extract book metadata (title, author, etc.)
|
||||
- Provide document info to other components
|
||||
|
||||
### 3. Settings Manager
|
||||
|
||||
**Location**: [managers/settings.py](../dreader/managers/settings.py)
|
||||
|
||||
Manages all display settings with persistence.
|
||||
|
||||
**Settings**:
|
||||
- Font scale (adjustable font size)
|
||||
- Line spacing
|
||||
- Inter-block spacing (paragraph spacing)
|
||||
- Word spacing
|
||||
|
||||
**Features**:
|
||||
- Real-time preview in settings overlay
|
||||
- Persistent across sessions
|
||||
- Position preservation when settings change
|
||||
|
||||
### 4. Gesture Router
|
||||
|
||||
**Location**: [handlers/gestures.py](../dreader/handlers/gestures.py)
|
||||
|
||||
Routes touch events to appropriate handlers based on application state.
|
||||
|
||||
**Gesture Types**:
|
||||
- `TAP` - Word selection, link following, overlay interaction
|
||||
- `SWIPE_LEFT` - Next page
|
||||
- `SWIPE_RIGHT` - Previous page
|
||||
- `SWIPE_UP` - Open navigation overlay (from bottom 20%)
|
||||
- `SWIPE_DOWN` - Open settings overlay (from top) or close overlay
|
||||
- `PINCH_IN/OUT` - Font size adjustment
|
||||
- `DRAG` - Text selection (start, move, end)
|
||||
|
||||
**Routing Logic**:
|
||||
```
|
||||
Touch Event → GestureRouter
|
||||
├─ Is overlay open?
|
||||
│ ├─ Yes → Route to overlay handler
|
||||
│ └─ No → Route to reading mode handler
|
||||
└─ Return GestureResponse
|
||||
```
|
||||
|
||||
### 5. Overlay System
|
||||
|
||||
**Location**: [overlays/](../dreader/overlays/)
|
||||
|
||||
The overlay system provides modal UI panels over the reading content.
|
||||
|
||||
#### Overlay Manager
|
||||
|
||||
**Location**: [overlays/base.py](../dreader/overlays/base.py)
|
||||
|
||||
Core overlay rendering and compositing infrastructure.
|
||||
|
||||
**Responsibilities**:
|
||||
- Render overlay HTML to images
|
||||
- Composite overlays over base page
|
||||
- Darken background for modal effect
|
||||
- Handle coordinate translation for interaction
|
||||
- Cache for performance
|
||||
|
||||
#### Navigation Overlay
|
||||
|
||||
**Location**: [overlays/navigation.py](../dreader/overlays/navigation.py)
|
||||
|
||||
Unified overlay with tabbed interface for:
|
||||
- **Contents Tab**: Chapter navigation (TOC)
|
||||
- **Bookmarks Tab**: Saved position management
|
||||
|
||||
**Features**:
|
||||
- Tab switching without closing overlay
|
||||
- Chapter selection with jump
|
||||
- Bookmark selection with jump
|
||||
- Add/delete bookmarks
|
||||
|
||||
#### Settings Overlay
|
||||
|
||||
**Location**: [overlays/settings.py](../dreader/overlays/settings.py)
|
||||
|
||||
Interactive settings panel with real-time preview.
|
||||
|
||||
**Controls**:
|
||||
- Font size: A- / A+ buttons
|
||||
- Line spacing: +/- buttons
|
||||
- Block spacing: +/- buttons
|
||||
- Word spacing: +/- buttons
|
||||
|
||||
**Interaction**: Changes apply immediately, overlay refreshes to show updated values.
|
||||
|
||||
### 6. Library Manager
|
||||
|
||||
**Location**: [library.py](../dreader/library.py)
|
||||
|
||||
Manages the library browsing experience.
|
||||
|
||||
**Features**:
|
||||
- Scan directory for EPUB files
|
||||
- Extract and cache metadata
|
||||
- Render library grid view
|
||||
- Handle book selection via tap
|
||||
- Cache cover images for performance
|
||||
|
||||
**Display**: Renders books in a grid with cover thumbnails and metadata.
|
||||
|
||||
### 7. State Manager
|
||||
|
||||
**Location**: [state.py](../dreader/state.py)
|
||||
|
||||
Persistent application state across sessions.
|
||||
|
||||
**State Structure**:
|
||||
```python
|
||||
class AppState:
|
||||
mode: EreaderMode # LIBRARY or READING
|
||||
overlay: OverlayState # Current overlay type
|
||||
current_book: BookState # Currently open book
|
||||
library: LibraryState # Library scan cache
|
||||
settings: SettingsState # Display settings
|
||||
```
|
||||
|
||||
**Persistence**:
|
||||
- Location: `~/.config/dreader/state.json`
|
||||
- Auto-save every 60 seconds
|
||||
- Immediate save on mode change, settings change, shutdown
|
||||
- Atomic writes for safety
|
||||
|
||||
**Boot Behavior**:
|
||||
- Resume last book at last position
|
||||
- Restore all settings
|
||||
- Fall back to library if book missing
|
||||
|
||||
## Data Flow Diagrams
|
||||
|
||||
### Opening an Overlay
|
||||
|
||||
```
|
||||
User Action
|
||||
↓
|
||||
EbookReader.open_navigation_overlay()
|
||||
├─ Get current page (base layer)
|
||||
├─ Get chapters and bookmarks
|
||||
↓
|
||||
OverlayManager.open_navigation_overlay()
|
||||
├─ Generate HTML
|
||||
├─ Render to image (using temp reader)
|
||||
├─ Composite over base page
|
||||
│ ├─ Darken background
|
||||
│ ├─ Add border
|
||||
│ └─ Paste panel at center
|
||||
└─ Cache base page, overlay, offset
|
||||
↓
|
||||
Return composited image
|
||||
```
|
||||
|
||||
### Overlay Interaction
|
||||
|
||||
```
|
||||
User Touch (x, y)
|
||||
↓
|
||||
GestureRouter.handle_touch()
|
||||
├─ Overlay open? YES
|
||||
↓
|
||||
EbookReader._handle_overlay_tap(x, y)
|
||||
↓
|
||||
OverlayManager.query_overlay_pixel(x, y)
|
||||
├─ Translate screen coords to overlay coords
|
||||
├─ Query pyWebLayout for link at position
|
||||
└─ Return link_target (e.g., "chapter:5")
|
||||
↓
|
||||
Parse link_target and execute action:
|
||||
├─ "chapter:N" → jump_to_chapter(N), close overlay
|
||||
├─ "bookmark:name" → load_position(name), close overlay
|
||||
├─ "setting:action" → apply setting, refresh overlay
|
||||
└─ "tab:name" → switch tab, keep overlay open
|
||||
↓
|
||||
Return GestureResponse
|
||||
```
|
||||
|
||||
### State Persistence
|
||||
|
||||
```
|
||||
Application Running
|
||||
↓
|
||||
StateManager auto-save timer (every 60s)
|
||||
├─ Gather current state
|
||||
├─ Serialize to JSON
|
||||
└─ Atomic write to disk
|
||||
|
||||
OR
|
||||
|
||||
User performs action (page turn, setting change)
|
||||
├─ StateManager.save_state()
|
||||
└─ Immediate write
|
||||
|
||||
Application Shutdown
|
||||
├─ Save position: reader.save_position("__auto_resume__")
|
||||
├─ Stop auto-save
|
||||
└─ Final state.json write
|
||||
```
|
||||
|
||||
### Boot Sequence
|
||||
|
||||
```
|
||||
Application Start
|
||||
↓
|
||||
StateManager.load_state()
|
||||
├─ Read state.json
|
||||
├─ Validate and parse
|
||||
└─ Create AppState object
|
||||
↓
|
||||
Check previous mode:
|
||||
├─ READING mode?
|
||||
│ ├─ Load last book
|
||||
│ ├─ Apply saved settings
|
||||
│ └─ Restore position ("__auto_resume__")
|
||||
│
|
||||
└─ LIBRARY mode?
|
||||
└─ Show library grid
|
||||
```
|
||||
|
||||
## File Organization
|
||||
|
||||
### Application State Files
|
||||
|
||||
```
|
||||
~/.config/dreader/
|
||||
├── state.json # Application state
|
||||
├── covers/ # Cached book covers
|
||||
│ └── {book_id}.png
|
||||
├── bookmarks/ # Per-book bookmarks
|
||||
│ └── {document_id}_{bookmark_name}.json
|
||||
└── highlights/ # Per-book highlights
|
||||
└── {document_id}_highlights.json
|
||||
```
|
||||
|
||||
### Bookmark Format
|
||||
|
||||
Each book's position is stored separately using document ID:
|
||||
```json
|
||||
{
|
||||
"document_id": "book123",
|
||||
"bookmark_name": "__auto_resume__",
|
||||
"position": {
|
||||
"offset": 1234,
|
||||
"chapter": 5
|
||||
},
|
||||
"timestamp": "2025-11-09T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Gesture Handling
|
||||
|
||||
### Gesture Priority and Routing
|
||||
|
||||
```
|
||||
Touch Event
|
||||
↓
|
||||
Is overlay open?
|
||||
├─ YES → Overlay Mode
|
||||
│ ├─ TAP → Handle overlay interaction
|
||||
│ ├─ SWIPE_DOWN → Close overlay
|
||||
│ └─ Other → Ignore (modal behavior)
|
||||
│
|
||||
└─ NO → Reading Mode
|
||||
├─ TAP
|
||||
│ ├─ On link → Follow link
|
||||
│ ├─ On word → Select word
|
||||
│ ├─ Left edge → Previous page
|
||||
│ └─ Right edge → Next page
|
||||
│
|
||||
├─ SWIPE
|
||||
│ ├─ LEFT → Next page
|
||||
│ ├─ RIGHT → Previous page
|
||||
│ ├─ UP (from bottom 20%) → Open navigation
|
||||
│ └─ DOWN (from top 20%) → Open settings
|
||||
│
|
||||
├─ PINCH
|
||||
│ ├─ IN → Decrease font size
|
||||
│ └─ OUT → Increase font size
|
||||
│
|
||||
└─ DRAG
|
||||
├─ START → Begin text selection
|
||||
├─ MOVE → Extend selection
|
||||
└─ END → Complete selection
|
||||
```
|
||||
|
||||
### Response Types
|
||||
|
||||
```python
|
||||
class ActionType(Enum):
|
||||
NONE = "none"
|
||||
PAGE_TURN = "page_turn"
|
||||
WORD_SELECTED = "word_selected"
|
||||
LINK_FOLLOWED = "link_followed"
|
||||
CHAPTER_SELECTED = "chapter_selected"
|
||||
BOOKMARK_SELECTED = "bookmark_selected"
|
||||
SETTING_CHANGED = "setting_changed"
|
||||
OVERLAY_OPENED = "overlay_opened"
|
||||
OVERLAY_CLOSED = "overlay_closed"
|
||||
TAB_SWITCHED = "tab_switched"
|
||||
```
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Rendering Performance
|
||||
|
||||
- **Page Turn**: ~50-100ms (depends on page complexity)
|
||||
- **Overlay Open**: ~200-250ms (includes HTML generation and rendering)
|
||||
- **Tab Switch**: ~125ms (uses cached base page)
|
||||
- **Setting Change**: ~150ms (re-render with new settings)
|
||||
- **Tap Interaction**: ~5-10ms (coordinate query)
|
||||
|
||||
### Memory Usage
|
||||
|
||||
- **Base Application**: ~20-30MB
|
||||
- **Per Book**: ~10-50MB (depends on images)
|
||||
- **Overlay Cache**: ~5-10MB
|
||||
|
||||
### Optimization Strategies
|
||||
|
||||
1. **Caching**:
|
||||
- Base page cached during overlay display
|
||||
- Overlay panel cached for tab switching
|
||||
- Cover images cached to disk
|
||||
- Metadata cached between sessions
|
||||
|
||||
2. **Lazy Loading**:
|
||||
- Library covers loaded on-demand
|
||||
- Book content loaded only when opened
|
||||
- Overlays rendered only when needed
|
||||
|
||||
3. **Efficient Updates**:
|
||||
- Tab switching reuses base page
|
||||
- Setting changes use incremental rendering
|
||||
- Position saves are debounced
|
||||
|
||||
## Extension Points
|
||||
|
||||
### Adding New Overlays
|
||||
|
||||
To add a new overlay type:
|
||||
|
||||
1. Define new `OverlayState` enum value in [state.py](../dreader/state.py#L27-L33)
|
||||
2. Create HTML generator in [html_generator.py](../dreader/html_generator.py)
|
||||
3. Add overlay class in `overlays/` directory
|
||||
4. Implement open/close methods in [overlay manager](../dreader/overlays/base.py)
|
||||
5. Add gesture handling in [application.py](../dreader/application.py)
|
||||
|
||||
### Custom Gesture Handlers
|
||||
|
||||
To add custom gestures:
|
||||
|
||||
1. Define gesture type in [gesture.py](../dreader/gesture.py)
|
||||
2. Add handler in [gestures.py](../dreader/handlers/gestures.py)
|
||||
3. Define action type for response
|
||||
4. Update gesture router logic
|
||||
|
||||
### HAL Integration
|
||||
|
||||
To integrate with hardware:
|
||||
|
||||
Create a display abstraction layer implementing:
|
||||
```python
|
||||
class DisplayHAL(ABC):
|
||||
@abstractmethod
|
||||
def show_image(self, image: Image.Image):
|
||||
"""Display image on hardware"""
|
||||
|
||||
@abstractmethod
|
||||
def get_touch_events(self) -> Iterator[TouchEvent]:
|
||||
"""Get touch input from hardware"""
|
||||
|
||||
@abstractmethod
|
||||
def set_brightness(self, level: int):
|
||||
"""Control display brightness"""
|
||||
```
|
||||
|
||||
Examples:
|
||||
- **E-ink**: IT8951, Remarkable device SDK
|
||||
- **Desktop**: pygame, tkinter
|
||||
- **Web**: Flask + HTML canvas
|
||||
- **Qt**: QPixmap + QTouchEvent
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- State serialization and persistence
|
||||
- Gesture routing logic
|
||||
- Coordinate translation
|
||||
- HTML generation
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Mode transitions (LIBRARY ↔ READING)
|
||||
- Overlay lifecycle (open → interact → close)
|
||||
- Boot recovery and resume
|
||||
- Settings persistence
|
||||
|
||||
### Example-Based Testing
|
||||
|
||||
Working examples demonstrate full integration:
|
||||
- [simple_ereader_example.py](../examples/simple_ereader_example.py)
|
||||
- [library_reading_integration.py](../examples/library_reading_integration.py)
|
||||
- [navigation_overlay_example.py](../examples/navigation_overlay_example.py)
|
||||
- [demo_settings_overlay.py](../examples/demo_settings_overlay.py)
|
||||
|
||||
## Design Patterns
|
||||
|
||||
### Component-Based Architecture
|
||||
|
||||
- **Managers**: Single-responsibility modules for specific tasks
|
||||
- **Handlers**: Event routing and processing
|
||||
- **Overlays**: Self-contained UI components
|
||||
|
||||
### Delegation Over Inheritance
|
||||
|
||||
- EbookReader delegates to specialized managers
|
||||
- No deep inheritance hierarchies
|
||||
- Composition for flexibility
|
||||
|
||||
### State Machine Pattern
|
||||
|
||||
- Clear state transitions (modes, overlays)
|
||||
- State persistence for resume
|
||||
- Predictable behavior
|
||||
|
||||
### Event-Driven Architecture
|
||||
|
||||
- Touch events drive all interactions
|
||||
- Response objects communicate results
|
||||
- Decoupled components
|
||||
|
||||
## Future Architecture Considerations
|
||||
|
||||
### Sub-Application Pattern
|
||||
|
||||
Current overlay handling uses a monolithic approach. Future refactoring could extract overlays into sub-applications:
|
||||
|
||||
```python
|
||||
class OverlaySubApplication(ABC):
|
||||
def open(self, context: OverlayContext) -> Image.Image: ...
|
||||
def handle_tap(self, x: int, y: int) -> GestureResponse: ...
|
||||
def close(self) -> Image.Image: ...
|
||||
```
|
||||
|
||||
Benefits:
|
||||
- Self-contained overlay logic
|
||||
- Easier testing
|
||||
- Plugin support
|
||||
- Composable overlays
|
||||
|
||||
### Plugin System
|
||||
|
||||
Enable third-party extensions:
|
||||
- Custom overlay types
|
||||
- Additional gestures
|
||||
- Export formats
|
||||
- Cloud sync providers
|
||||
|
||||
## References
|
||||
|
||||
- [pyWebLayout Documentation](https://gitea.tourolle.paris/dtourolle/pyWebLayout)
|
||||
- [REQUIREMENTS.md](REQUIREMENTS.md) - Detailed feature specifications
|
||||
- [README.md](../README.md) - User-facing documentation
|
||||
- [examples/](../examples/) - Working code examples
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,853 @@
|
||||
# DReader Hardware Guide
|
||||
|
||||
Everything needed to build, wire, configure and run DReader on real e-ink hardware.
|
||||
|
||||
This guide covers the reference device: a Raspberry Pi driving an IT8951 e-ink
|
||||
controller with an FT5316 touch panel, optional sensors and three physical buttons.
|
||||
If you are *implementing a HAL for different hardware*, see
|
||||
[HAL_IMPLEMENTATION_SPEC.md](HAL_IMPLEMENTATION_SPEC.md) instead.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [1. Hardware Requirements](#1-hardware-requirements)
|
||||
- [2. Wiring](#2-wiring)
|
||||
- [3. Software Installation](#3-software-installation)
|
||||
- [4. Configuration](#4-configuration)
|
||||
- [5. Running DReader](#5-running-dreader)
|
||||
- [6. Input Reference](#6-input-reference)
|
||||
- [7. Accelerometer Page Flipping](#7-accelerometer-page-flipping)
|
||||
- [8. Verifying Hardware](#8-verifying-hardware)
|
||||
- [9. Troubleshooting](#9-troubleshooting)
|
||||
- [10. Performance Notes](#10-performance-notes)
|
||||
- [Appendix: Programmatic Usage](#appendix-programmatic-usage)
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
On a Raspberry Pi with the hardware already wired:
|
||||
|
||||
```bash
|
||||
# 1. Clone and set up
|
||||
git clone https://gitea.tourolle.paris/dtourolle/dreader-application.git
|
||||
cd dreader-application
|
||||
git submodule update --init --recursive
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# 2. Install the application and hardware drivers
|
||||
pip install -e .
|
||||
./install_hardware_drivers.sh
|
||||
|
||||
# 3. Interactive setup — detects hardware, writes hardware_config.json
|
||||
sudo python3 setup_rpi.py
|
||||
|
||||
# 4. Run
|
||||
python examples/run_on_hardware_config.py
|
||||
```
|
||||
|
||||
> ⚠️ **Before first run, check your display's VCOM voltage.** See
|
||||
> [VCOM Voltage](#vcom-voltage) — an incorrect value can damage the panel.
|
||||
|
||||
---
|
||||
|
||||
## 1. Hardware Requirements
|
||||
|
||||
### Required
|
||||
|
||||
| Component | Notes |
|
||||
|-----------|-------|
|
||||
| **Raspberry Pi** (or compatible SBC) | 512MB RAM minimum |
|
||||
| **IT8951 E-ink Display Controller** | 1872 × 1404, SPI |
|
||||
| **FT5316 Capacitive Touch Panel** | I2C, address `0x38` |
|
||||
|
||||
### Optional
|
||||
|
||||
| Component | Purpose | I2C Address |
|
||||
|-----------|---------|-------------|
|
||||
| **BMA400 Accelerometer** | Auto-rotation and tilt page-flipping | `0x14` (or `0x15`) |
|
||||
| **PCF8523 RTC** | Timekeeping with battery backup | `0x68` |
|
||||
| **INA219 Power Monitor** | Battery level monitoring | `0x40` |
|
||||
| **Momentary pushbuttons** ×3 | Page turns and power off | — |
|
||||
|
||||
### Power Requirements
|
||||
|
||||
- **Input:** 5V via USB-C or GPIO header
|
||||
- **Display:** ~3.3V, peak 500mA during refresh
|
||||
- **Touch panel:** 3.3V, ~20mA
|
||||
- **Total (active):** ~1–2W
|
||||
- **Total (sleep):** ~50–100mW
|
||||
|
||||
---
|
||||
|
||||
## 2. Wiring
|
||||
|
||||
### Pin Assignment Summary
|
||||
|
||||
All GPIO numbers in this guide use **BCM numbering**, not physical pin numbers.
|
||||
|
||||
| GPIO | Physical Pin | Function |
|
||||
|------|--------------|----------|
|
||||
| 2 | 3 | I2C1 SDA — touch, accelerometer, RTC, power monitor |
|
||||
| 3 | 5 | I2C1 SCL — all I2C devices |
|
||||
| 8 | 24 | SPI0 CE0 — display chip select |
|
||||
| 9 | 21 | SPI0 MISO |
|
||||
| 10 | 19 | SPI0 MOSI |
|
||||
| 11 | 23 | SPI0 SCLK |
|
||||
| 17 | 11 | Display RST |
|
||||
| 21 | 40 | Power off button |
|
||||
| 22 | 15 | Previous page button |
|
||||
| 24 | 18 | Display HRDY |
|
||||
| 27 | 13 | Next page button |
|
||||
|
||||
### IT8951 E-ink Display (SPI)
|
||||
|
||||
| IT8951 Pin | Raspberry Pi | Description |
|
||||
|------------|--------------|-------------|
|
||||
| VCC | 3.3V (Pin 1) | Power supply |
|
||||
| GND | GND (Pin 6) | Ground |
|
||||
| MISO | GPIO 9 (Pin 21) | SPI MISO |
|
||||
| MOSI | GPIO 10 (Pin 19) | SPI MOSI |
|
||||
| SCK | GPIO 11 (Pin 23) | SPI Clock |
|
||||
| CS | GPIO 8 (Pin 24) | SPI Chip Select |
|
||||
| RST | GPIO 17 (Pin 11) | Reset |
|
||||
| HRDY | GPIO 24 (Pin 18) | Ready signal |
|
||||
|
||||
### I2C Devices
|
||||
|
||||
All I2C devices share SDA (GPIO 2, Pin 3) and SCL (GPIO 3, Pin 5), plus 3.3V and GND.
|
||||
Each must have a unique address.
|
||||
|
||||
| Device | Extra connections |
|
||||
|--------|-------------------|
|
||||
| **FT5316 Touch Panel** | INT is optional and unused in the reference build — see note below |
|
||||
| **BMA400 Accelerometer** | None. Confirm the address on your module (`0x14` or `0x15`) |
|
||||
| **PCF8523 RTC** | BAT → CR2032 backup battery |
|
||||
| **INA219 Power Monitor** | VIN+ → battery positive; VIN- → through shunt resistor to load |
|
||||
|
||||
> **Note on the FT5316 INT pin:** some wiring guides route it to GPIO 27, which
|
||||
> collides with the next-page button on this device. The reference build leaves INT
|
||||
> unconnected and polls the touch panel instead. If you want to use INT, move it to a
|
||||
> free GPIO (see [safe pins](#choosing-gpio-pins)) or relocate the next-page button.
|
||||
|
||||
### GPIO Buttons
|
||||
|
||||
The reference device has three buttons:
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ │
|
||||
│ [Power Off] │ ← Side button (GPIO 21)
|
||||
│ │
|
||||
│ E-INK │
|
||||
│ DISPLAY │
|
||||
│ 1872x1404 │
|
||||
│ │
|
||||
│ [Prev] [Next] │ ← Bottom edge (GPIO 22, GPIO 27)
|
||||
│ │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
| GPIO | Function | Gesture Generated | Wiring |
|
||||
|------|----------|-------------------|--------|
|
||||
| 22 | Previous Page | `swipe_right` | GPIO → button → **GND**, internal pull-up, active LOW |
|
||||
| 27 | Next Page | `swipe_left` | GPIO → button → **GND**, internal pull-up, active LOW |
|
||||
| 21 | Power Off (long press) | `long_press` | GPIO → button → **3.3V**, pull-up disabled, active HIGH |
|
||||
|
||||
> ⚠️ **The power button is wired differently from the page buttons.** Prev/Next pull
|
||||
> LOW to GND; the power button pulls HIGH when pressed and sets `"pull_up": false` in
|
||||
> the config. Wiring it to GND like the others will make it appear permanently pressed.
|
||||
|
||||
Active-low page button wiring:
|
||||
|
||||
```
|
||||
+3.3V
|
||||
|
|
||||
R (internal pull-up)
|
||||
|
|
||||
GPIO --|------ Button ------ GND
|
||||
|
|
||||
(to BCM2835)
|
||||
```
|
||||
|
||||
Use momentary pushbuttons (normally open) rated for at least 10,000 cycles. Tactile
|
||||
switches give the best feedback. Keep wires short to reduce noise, use stranded wire
|
||||
for flexibility, and add strain relief at the connection points.
|
||||
|
||||
### Physical Layout
|
||||
|
||||
```
|
||||
Raspberry Pi GPIO Header (BCM Numbering)
|
||||
|
||||
3V3 (1) (2) 5V
|
||||
GPIO2 (3) (4) 5V ← I2C1 SDA (touch, sensors)
|
||||
GPIO3 (5) (6) GND ← I2C1 SCL
|
||||
GPIO4 (7) (8) GPIO14
|
||||
GND (9) (10) GPIO15
|
||||
GPIO17 (11) (12) GPIO18 ← Display RST
|
||||
GPIO27 (13) (14) GND ← Next page button
|
||||
GPIO22 (15) (16) GPIO23 ← Previous page button
|
||||
3V3 (17) (18) GPIO24 ← Display HRDY
|
||||
GPIO10 (19) (20) GND ← SPI0 MOSI
|
||||
GPIO9 (21) (22) GPIO25 ← SPI0 MISO
|
||||
GPIO11 (23) (24) GPIO8 ← SPI0 SCLK, CE0
|
||||
GND (25) (26) GPIO7
|
||||
GPIO0 (27) (28) GPIO1
|
||||
GPIO5 (29) (30) GND
|
||||
GPIO6 (31) (32) GPIO12
|
||||
GPIO13 (33) (34) GND
|
||||
GPIO19 (35) (36) GPIO16
|
||||
GPIO26 (37) (38) GPIO20
|
||||
GND (39) (40) GPIO21 ← Power off button
|
||||
```
|
||||
|
||||
### Choosing GPIO Pins
|
||||
|
||||
If you are remapping buttons:
|
||||
|
||||
**Safe for buttons:** GPIO 5, 6, 12, 13, 16, 18, 19, 20, 21, 22, 23, 25, 26, 27
|
||||
|
||||
**Avoid:**
|
||||
|
||||
| Pins | Reserved for |
|
||||
|------|--------------|
|
||||
| GPIO 0, 1 | ID EEPROM |
|
||||
| GPIO 2, 3 | I2C — touch and sensors |
|
||||
| GPIO 7–11 | SPI — e-ink display |
|
||||
| GPIO 14, 15 | UART — serial console |
|
||||
| GPIO 17, 24 | Display RST and HRDY on this device |
|
||||
|
||||
---
|
||||
|
||||
## 3. Software Installation
|
||||
|
||||
### 1. System Dependencies
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y python3-dev python3-pip python3-venv
|
||||
sudo apt install -y i2c-tools python3-smbus
|
||||
|
||||
# Enable I2C and SPI
|
||||
sudo raspi-config
|
||||
# Navigate to: Interface Options -> Enable I2C and SPI
|
||||
```
|
||||
|
||||
### 2. Clone and Set Up the Application
|
||||
|
||||
```bash
|
||||
git clone https://gitea.tourolle.paris/dtourolle/dreader-application.git
|
||||
cd dreader-application
|
||||
|
||||
# dreader-hal is a submodule
|
||||
git submodule update --init --recursive
|
||||
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### 3. Install dreader-hal and Drivers
|
||||
|
||||
The simplest route is the bundled script:
|
||||
|
||||
```bash
|
||||
./install_hardware_drivers.sh
|
||||
```
|
||||
|
||||
To do it by hand — `dreader-hal` keeps its driver dependencies in its own
|
||||
`external/` directory:
|
||||
|
||||
```bash
|
||||
pip install -e external/dreader-hal
|
||||
|
||||
cd external/dreader-hal/external
|
||||
pip install -e IT8951
|
||||
pip install -e PyFTtxx6
|
||||
pip install -e PyBMA400
|
||||
pip install -e PyPCF8523
|
||||
pip install -e pi_ina219
|
||||
cd ../../..
|
||||
```
|
||||
|
||||
### 4. Raspberry Pi GPIO
|
||||
|
||||
```bash
|
||||
pip install RPi.GPIO spidev
|
||||
```
|
||||
|
||||
### 5. Permissions
|
||||
|
||||
```bash
|
||||
sudo usermod -a -G spi,i2c,gpio $USER
|
||||
# Log out and back in for group changes to take effect
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Configuration
|
||||
|
||||
DReader uses two configuration files:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `hardware_config.json` | Display, buttons, sensors, application defaults |
|
||||
| `accelerometer_config.json` | Tilt calibration — see [section 7](#7-accelerometer-page-flipping) |
|
||||
|
||||
### Generating the Config
|
||||
|
||||
The interactive setup script detects your hardware and writes `hardware_config.json`:
|
||||
|
||||
```bash
|
||||
sudo python3 setup_rpi.py
|
||||
```
|
||||
|
||||
It will detect connected I2C devices, enable I2C/SPI if needed, configure the GPIO
|
||||
button mapping and set the VCOM voltage.
|
||||
|
||||
### VCOM Voltage
|
||||
|
||||
⚠️ **CRITICAL**: Every e-ink panel has a unique VCOM voltage printed on a label,
|
||||
usually on the ribbon cable or the back of the panel.
|
||||
|
||||
- Read the value from your display's label (e.g. `VCOM = -2.06V`)
|
||||
- Set it as `display.vcom` in the config, or pass `--vcom -2.06` on the command line
|
||||
- **Using the wrong VCOM can damage the display**
|
||||
|
||||
### hardware_config.json Reference
|
||||
|
||||
```json
|
||||
{
|
||||
"display": {
|
||||
"width": 1872,
|
||||
"height": 1404,
|
||||
"vcom": -1.7,
|
||||
"spi_hz": 24000000,
|
||||
"rotate": "CW",
|
||||
"auto_sleep": true
|
||||
},
|
||||
|
||||
"gpio_buttons": {
|
||||
"enabled": true,
|
||||
"pull_up": true,
|
||||
"bounce_time_ms": 200,
|
||||
"buttons": [
|
||||
{"name": "prev_page", "gpio": 22, "gesture": "swipe_right"},
|
||||
{"name": "next_page", "gpio": 27, "gesture": "swipe_left"},
|
||||
{"name": "power_off", "gpio": 21, "gesture": "long_press", "pull_up": false}
|
||||
]
|
||||
},
|
||||
|
||||
"accelerometer": {
|
||||
"enabled": true,
|
||||
"tilt_enabled": false,
|
||||
"orientation_enabled": true,
|
||||
"calibration_file": "accelerometer_config.json"
|
||||
},
|
||||
|
||||
"rtc": {"enabled": true},
|
||||
|
||||
"power_monitor": {
|
||||
"enabled": true,
|
||||
"shunt_ohms": 0.1,
|
||||
"battery_capacity_mah": 3000,
|
||||
"low_battery_threshold": 20.0,
|
||||
"show_battery_interval": 100
|
||||
},
|
||||
|
||||
"application": {
|
||||
"library_path": "/home/pi/Books",
|
||||
"auto_save_interval": 60,
|
||||
"force_library_mode": false,
|
||||
"log_level": "INFO"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `gpio_buttons`
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `enabled` | bool | Enable or disable all GPIO buttons |
|
||||
| `pull_up` | bool | Default pull-up for all buttons; override per button |
|
||||
| `bounce_time_ms` | int | Debounce time in milliseconds (default 200) |
|
||||
| `buttons` | array | Button definitions |
|
||||
|
||||
Each button takes:
|
||||
|
||||
| Key | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `name` | string | Unique identifier |
|
||||
| `gpio` | int | BCM pin number |
|
||||
| `gesture` | string | Gesture emitted on press — see [gesture table](#gestures) |
|
||||
| `pull_up` | bool | Optional per-button override of the group default |
|
||||
| `description` | string | Optional human-readable note |
|
||||
|
||||
Buttons can be mapped to *any* gesture, so custom layouts are just config. For example,
|
||||
a four-button reading layout:
|
||||
|
||||
```json
|
||||
{
|
||||
"gpio_buttons": {
|
||||
"enabled": true,
|
||||
"buttons": [
|
||||
{"name": "next", "gpio": 27, "gesture": "swipe_left"},
|
||||
{"name": "prev", "gpio": 22, "gesture": "swipe_right"},
|
||||
{"name": "zoom_in", "gpio": 25, "gesture": "pinch_out"},
|
||||
{"name": "zoom_out", "gpio": 23, "gesture": "pinch_in"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Button handling lives in [dreader/gpio_buttons.py](../dreader/gpio_buttons.py).
|
||||
|
||||
---
|
||||
|
||||
## 5. Running DReader
|
||||
|
||||
### With a Config File (Recommended)
|
||||
|
||||
```bash
|
||||
python examples/run_on_hardware_config.py
|
||||
```
|
||||
|
||||
### Direct Command Line
|
||||
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
python examples/run_on_hardware.py /path/to/books --vcom -2.06
|
||||
```
|
||||
|
||||
### Without Hardware (Virtual Display)
|
||||
|
||||
Test the integration on a development machine — this opens a Tkinter window
|
||||
simulating the e-ink panel:
|
||||
|
||||
```bash
|
||||
python examples/run_on_hardware.py /path/to/books \
|
||||
--virtual --no-orientation --no-rtc --no-power
|
||||
```
|
||||
|
||||
### Common Options
|
||||
|
||||
| Option | Effect |
|
||||
|--------|--------|
|
||||
| `--vcom <volts>` | Display VCOM voltage (**check your label**) |
|
||||
| `--virtual` | Tkinter virtual display instead of real hardware |
|
||||
| `--no-orientation` | Disable the accelerometer |
|
||||
| `--no-rtc` | Disable the real-time clock |
|
||||
| `--no-power` | Disable the battery monitor |
|
||||
| `--show-battery` | Print battery level periodically |
|
||||
| `--battery-capacity <mAh>` | Set battery capacity (default 3000) |
|
||||
| `--force-library` | Always start in library mode, ignoring saved state |
|
||||
| `--verbose` | Verbose debug logging |
|
||||
|
||||
Full reference: `python examples/run_on_hardware.py --help`
|
||||
|
||||
---
|
||||
|
||||
## 6. Input Reference
|
||||
|
||||
### Gestures
|
||||
|
||||
Every input source — touch, buttons, accelerometer — is normalised into the same
|
||||
gesture stream, so any source can trigger any action.
|
||||
|
||||
| Gesture | Default Action |
|
||||
|---------|----------------|
|
||||
| `swipe_left` | Next page |
|
||||
| `swipe_right` | Previous page |
|
||||
| `swipe_up` (from bottom) | Open navigation/TOC overlay |
|
||||
| `swipe_down` (from top) | Open settings overlay |
|
||||
| `tap` | Select book, word, or link |
|
||||
| `long_press` | Word definition / context menu |
|
||||
| `pinch_in` | Decrease font size |
|
||||
| `pinch_out` | Increase font size |
|
||||
| `tilt_forward` | Next page — see [section 7](#7-accelerometer-page-flipping) |
|
||||
| `tilt_backward` | Previous page — see [section 7](#7-accelerometer-page-flipping) |
|
||||
|
||||
---
|
||||
|
||||
## 7. Accelerometer Page Flipping
|
||||
|
||||
The BMA400 can detect device tilt and turn pages hands-free — useful while eating,
|
||||
holding the device one-handed, when it is mounted on a stand, or for accessibility.
|
||||
|
||||
> Tilt is **off by default** (`accelerometer.tilt_enabled: false`). Enable it in
|
||||
> `hardware_config.json` after calibrating.
|
||||
|
||||
### Calibration
|
||||
|
||||
Calibration establishes which direction is "up" for your device:
|
||||
|
||||
```bash
|
||||
python examples/calibrate_accelerometer.py
|
||||
```
|
||||
|
||||
1. The display shows an arrow pointing in the direction of gravity
|
||||
2. Rotate the device until the arrow points up
|
||||
3. Tap the screen to save
|
||||
4. Calibration is written to `accelerometer_config.json`
|
||||
|
||||
### Calibration File Format
|
||||
|
||||
```json
|
||||
{
|
||||
"up_vector": {"x": 0.0, "y": 9.8, "z": 0.0},
|
||||
"tilt_threshold": 0.3,
|
||||
"debounce_time": 0.5
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Description |
|
||||
|-----|-------------|
|
||||
| `up_vector` | Gravity vector (m/s²) when the device is upright |
|
||||
| `tilt_threshold` | Tilt angle threshold in radians |
|
||||
| `debounce_time` | Minimum seconds between gestures |
|
||||
|
||||
**Tuning `tilt_threshold`:**
|
||||
|
||||
| Value | Behaviour |
|
||||
|-------|-----------|
|
||||
| 0.1 rad (~6°) | Very sensitive — small tilts turn pages |
|
||||
| 0.3 rad (~17°) | Default |
|
||||
| 0.5 rad (~29°) | Requires a deliberate, larger tilt |
|
||||
|
||||
**Tuning `debounce_time`:**
|
||||
|
||||
| Value | Behaviour |
|
||||
|-------|-----------|
|
||||
| 0.2s | Fast — flip several pages quickly |
|
||||
| 0.5s | Default — prevents accidental double-flips |
|
||||
| 1.0s | Slow — requires a pause between flips |
|
||||
|
||||
### How Tilt Detection Works
|
||||
|
||||
1. **Read** the accelerometer for (x, y, z) acceleration in m/s²
|
||||
2. **Normalise** both the current gravity vector and the calibrated up vector
|
||||
3. **Compute the tilt angle** by projecting gravity onto the plane perpendicular to
|
||||
the up vector
|
||||
4. **Compare** against `tilt_threshold`
|
||||
5. **Determine direction** from the sign of the perpendicular y-component
|
||||
6. **Debounce** to suppress repeat triggers
|
||||
|
||||
Given a calibrated up vector `U = (ux, uy, uz)` and current gravity `G = (gx, gy, gz)`:
|
||||
|
||||
```python
|
||||
# Component of G along U
|
||||
dot = gx*ux + gy*uy + gz*uz
|
||||
|
||||
# Perpendicular component
|
||||
perp = G - dot*U
|
||||
perp_magnitude = |perp|
|
||||
|
||||
# Tilt angle
|
||||
angle = atan2(perp_magnitude, |dot|)
|
||||
|
||||
# Direction
|
||||
if perp_y > 0:
|
||||
gesture = TILT_FORWARD # next page
|
||||
else:
|
||||
gesture = TILT_BACKWARD # previous page
|
||||
```
|
||||
|
||||
The implementation lives in [dreader/hal_hardware.py](../dreader/hal_hardware.py)
|
||||
(`get_tilt_gesture`), with the gesture types defined in
|
||||
[dreader/gesture.py](../dreader/gesture.py) and handled in
|
||||
[dreader/handlers/gestures.py](../dreader/handlers/gestures.py).
|
||||
|
||||
### Using Tilt in Your Own Code
|
||||
|
||||
```python
|
||||
from dreader.hal_hardware import HardwareDisplayHAL
|
||||
|
||||
hal = HardwareDisplayHAL(
|
||||
width=1872,
|
||||
height=1404,
|
||||
enable_orientation=True, # required for tilt
|
||||
)
|
||||
await hal.initialize()
|
||||
|
||||
if hal.load_accelerometer_calibration("accelerometer_config.json"):
|
||||
print("Accelerometer calibrated!")
|
||||
else:
|
||||
print("No calibration found - tilt gestures disabled")
|
||||
```
|
||||
|
||||
**Unified event loop (recommended)** — `get_event()` polls both touch and
|
||||
accelerometer, prioritising touch:
|
||||
|
||||
```python
|
||||
while running:
|
||||
event = await hal.get_event()
|
||||
if event:
|
||||
response = gesture_router.handle_touch(event)
|
||||
await asyncio.sleep(0.01)
|
||||
```
|
||||
|
||||
**Separate polling** — more control over each source:
|
||||
|
||||
```python
|
||||
while running:
|
||||
touch_event = await hal.get_touch_event()
|
||||
tilt_event = await hal.get_tilt_gesture() # None if uncalibrated
|
||||
|
||||
if touch_event:
|
||||
response = gesture_router.handle_touch(touch_event)
|
||||
if tilt_event:
|
||||
response = gesture_router.handle_touch(tilt_event)
|
||||
|
||||
await asyncio.sleep(0.05) # ~20Hz
|
||||
```
|
||||
|
||||
### Tilt API Reference
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `load_accelerometer_calibration(config_path="accelerometer_config.json") -> bool` | Load calibration from JSON. Returns `True` on success. |
|
||||
| `async get_event() -> Optional[TouchEvent]` | **Recommended.** Next event from any input source; touch takes priority over tilt. `None` if nothing pending. |
|
||||
| `async get_tilt_gesture() -> Optional[TouchEvent]` | Poll the accelerometer only. Returns a `TILT_FORWARD`/`TILT_BACKWARD` event, or `None` if there is no tilt, no calibration, or the debounce window is still open. |
|
||||
|
||||
### Demos and Tests
|
||||
|
||||
```bash
|
||||
# Simple demo using the unified get_event() API
|
||||
python examples/demo_accelerometer_simple.py ~/Books/mybook.epub
|
||||
|
||||
# Full-featured demo with separate polling
|
||||
python examples/demo_accelerometer_page_flip.py ~/Books/mybook.epub
|
||||
|
||||
# Tests
|
||||
python -m pytest tests/test_accelerometer_gestures.py -v
|
||||
```
|
||||
|
||||
### Known Limitations
|
||||
|
||||
1. **Orientation lock** — tilt detection assumes a fixed device orientation, so
|
||||
auto-rotation can interfere with it.
|
||||
2. **Movement** — walking may cause false positives. Raise the threshold or disable
|
||||
tilt while moving.
|
||||
3. **Calibration drift** — the accelerometer drifts over time; re-calibrate periodically.
|
||||
4. **Simplified direction heuristic** — complex orientations may be misread.
|
||||
5. **Single axis** — only tilt in one plane is detected; left/right tilts are not
|
||||
distinguished.
|
||||
|
||||
---
|
||||
|
||||
## 8. Verifying Hardware
|
||||
|
||||
### Check I2C Devices
|
||||
|
||||
```bash
|
||||
i2cdetect -y 1
|
||||
```
|
||||
|
||||
Expected on a fully-populated device:
|
||||
|
||||
```
|
||||
0 1 2 3 4 5 6 7 8 9 a b c d e f
|
||||
00: -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
10: -- -- -- -- 14 -- -- -- -- -- -- -- -- -- -- -- ← BMA400
|
||||
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
30: -- -- -- -- -- -- -- -- 38 -- -- -- -- -- -- -- ← FT5316
|
||||
40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ← INA219
|
||||
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- -- ← PCF8523
|
||||
70: -- -- -- -- -- -- -- --
|
||||
```
|
||||
|
||||
### Check SPI
|
||||
|
||||
```bash
|
||||
ls /dev/spi*
|
||||
# Should show: /dev/spidev0.0 /dev/spidev0.1
|
||||
```
|
||||
|
||||
### Check Buttons
|
||||
|
||||
```bash
|
||||
sudo apt install gpiod
|
||||
|
||||
gpioget gpiochip0 22 # Previous page — 1 idle, 0 when pressed
|
||||
gpioget gpiochip0 27 # Next page — 1 idle, 0 when pressed
|
||||
gpioget gpiochip0 21 # Power off — 0 idle, 1 when pressed (active high)
|
||||
```
|
||||
|
||||
Or watch the application logs:
|
||||
|
||||
```bash
|
||||
python examples/run_on_hardware_config.py --verbose
|
||||
```
|
||||
|
||||
Pressing a button should log something like:
|
||||
|
||||
```
|
||||
Button pressed: next_page (GPIO 27)
|
||||
Button event queued: next_page -> swipe_left
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Troubleshooting
|
||||
|
||||
### Display Not Working
|
||||
|
||||
1. **Check VCOM voltage** — it must match the label on your display
|
||||
2. **Check SPI is enabled** — `ls /dev/spi*` should list two devices
|
||||
3. **Enable SPI if missing** — `sudo raspi-config` → Interface Options → SPI → Enable
|
||||
4. **Check permissions** — `sudo usermod -a -G spi $USER`, then log out and back in
|
||||
5. **Isolate the problem** — run with `--virtual` to test the software without hardware
|
||||
|
||||
### Touch Not Working
|
||||
|
||||
1. **Check I2C connections** — `i2cdetect -y 1` should show `0x38`
|
||||
2. **Enable I2C if missing** — `sudo raspi-config` → Interface Options → I2C → Enable
|
||||
3. **Confirm the module loaded** — `lsmod | grep i2c` should show `i2c_dev` and `i2c_bcm2835`
|
||||
4. **Check permissions** — `sudo usermod -a -G i2c $USER`, then log out and back in
|
||||
5. **Calibrate touch** — see the dreader-hal calibration docs
|
||||
|
||||
### Buttons Not Working
|
||||
|
||||
1. **Check the wiring polarity** — prev/next go to GND; the power button goes to 3.3V
|
||||
2. **Verify the pin numbers** are BCM, not physical pin numbers
|
||||
3. **Check permissions** — `sudo usermod -a -G gpio $USER`, then log out and back in
|
||||
4. **Test the pin directly** with `gpioget` (see [section 8](#check-buttons))
|
||||
5. **Check for conflicts** — make sure no other program holds those GPIOs
|
||||
|
||||
### Buttons Trigger Multiple Times
|
||||
|
||||
1. Increase `bounce_time_ms` in the config (try 300–500ms)
|
||||
2. Add a hardware debounce capacitor (0.1µF between GPIO and GND)
|
||||
3. Check for loose connections
|
||||
|
||||
### Button Performs the Wrong Action
|
||||
|
||||
1. Check the `gesture` field for that button in `hardware_config.json`
|
||||
2. Run with `--verbose` to see which gesture is actually emitted
|
||||
|
||||
### Tilt Gestures Not Working
|
||||
|
||||
1. **Calibrate first** — `python examples/calibrate_accelerometer.py`
|
||||
2. **Enable the accelerometer** — `HardwareDisplayHAL(enable_orientation=True)`
|
||||
3. **Verify the calibration loaded** — `hal.load_accelerometer_calibration()` returns `True`
|
||||
4. **Confirm you are polling** — call `get_event()` or `get_tilt_gesture()` in your loop
|
||||
5. **Adjust sensitivity** — tune `tilt_threshold` and `debounce_time`
|
||||
|
||||
If tilting forward turns the page *backward*, the direction heuristic is inverted for
|
||||
your device's mounting. Flip the comparison in `get_tilt_gesture()` in
|
||||
[dreader/hal_hardware.py](../dreader/hal_hardware.py):
|
||||
|
||||
```python
|
||||
if perp_y < 0: # was: perp_y > 0
|
||||
gesture = AppGestureType.TILT_FORWARD
|
||||
else:
|
||||
gesture = AppGestureType.TILT_BACKWARD
|
||||
```
|
||||
|
||||
### Import Errors
|
||||
|
||||
If you see `ModuleNotFoundError` for the drivers, reinstall them all:
|
||||
|
||||
```bash
|
||||
cd external/dreader-hal/external
|
||||
for dir in */; do pip install -e "$dir"; done
|
||||
cd ../../..
|
||||
```
|
||||
|
||||
### Display Ghosting
|
||||
|
||||
E-ink panels retain a faint previous image. The HAL performs a full refresh every 10
|
||||
page turns automatically to clear it. See the dreader-hal documentation for manual
|
||||
refresh control.
|
||||
|
||||
---
|
||||
|
||||
## 10. Performance Notes
|
||||
|
||||
### E-ink Refresh Modes
|
||||
|
||||
dreader-hal selects the refresh mode automatically:
|
||||
|
||||
| Mode | Time | Used for |
|
||||
|------|------|----------|
|
||||
| **Fast** (DU) | ~200ms | Text updates |
|
||||
| **Quality** (GC16) | ~1000ms | Images |
|
||||
| **Full** (INIT) | ~1000ms | Every 10 pages, to clear ghosting |
|
||||
|
||||
### Battery Life
|
||||
|
||||
With default settings:
|
||||
|
||||
- Active reading: ~10–20 hours
|
||||
- Standby (display sleeping): ~1–2 weeks
|
||||
|
||||
### Memory Usage
|
||||
|
||||
- Base application: ~30–50MB
|
||||
- Per book: ~10–30MB depending on size
|
||||
- Minimum 512MB RAM recommended
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Programmatic Usage
|
||||
|
||||
Driving the hardware HAL directly from your own script:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from dreader.hal_hardware import HardwareDisplayHAL
|
||||
from dreader.main import DReaderApplication, AppConfig
|
||||
|
||||
async def main():
|
||||
hal = HardwareDisplayHAL(
|
||||
width=1872,
|
||||
height=1404,
|
||||
vcom=-2.06, # YOUR DISPLAY'S VCOM!
|
||||
virtual_display=False,
|
||||
enable_orientation=True,
|
||||
enable_rtc=True,
|
||||
enable_power_monitor=True,
|
||||
)
|
||||
|
||||
config = AppConfig(
|
||||
display_hal=hal,
|
||||
library_path="/home/pi/Books",
|
||||
page_size=(1872, 1404),
|
||||
)
|
||||
|
||||
app = DReaderApplication(config)
|
||||
|
||||
try:
|
||||
await hal.initialize()
|
||||
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)
|
||||
|
||||
finally:
|
||||
await app.shutdown()
|
||||
await hal.cleanup()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [README.md](../README.md) — application features and usage
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) — system architecture and design
|
||||
- [HAL_IMPLEMENTATION_SPEC.md](HAL_IMPLEMENTATION_SPEC.md) — writing a HAL for other hardware
|
||||
- [examples/](../examples/) — runnable examples
|
||||
- [external/dreader-hal/README.md](../external/dreader-hal/README.md) — HAL library details
|
||||
|
||||
### Support
|
||||
|
||||
- Hardware issues: [dreader-hal issues](https://gitea.tourolle.paris/dtourolle/dreader-hal/issues)
|
||||
- Application issues: [dreader-application issues](https://gitea.tourolle.paris/dtourolle/dreader-application/issues)
|
||||
@@ -0,0 +1,196 @@
|
||||
# E-Reader Application Requirements
|
||||
|
||||
## Overview
|
||||
This document defines the core requirements for a full-featured e-reader application built on dreader/pyWebLayout. The application supports library browsing, reading with overlays, state persistence, and gesture-based interaction.
|
||||
|
||||
## Application Modes
|
||||
|
||||
### LIBRARY Mode
|
||||
Browse and select books from the user's library.
|
||||
|
||||
**Features**:
|
||||
- Grid display of books with covers and metadata (title, author)
|
||||
- Book selection via tap/click
|
||||
- Visual feedback on selection
|
||||
|
||||
**Interactions**:
|
||||
- **Tap book**: Open in READING mode
|
||||
- **Swipe**: Scroll library (future)
|
||||
|
||||
### READING Mode
|
||||
Read the current book with page navigation.
|
||||
|
||||
**Features**:
|
||||
- Page rendering via pyWebLayout
|
||||
- Page navigation (next/previous)
|
||||
- Access to overlays
|
||||
- Progress indicator
|
||||
|
||||
**Interactions**:
|
||||
- **Tap edges**: Previous/Next page
|
||||
- **Swipe left/right**: Page navigation
|
||||
- **Swipe up (from bottom)**: Open navigation overlay
|
||||
- **Swipe down (from top)**: Open settings overlay
|
||||
- **Pinch in/out**: Adjust font size
|
||||
- **Long-press on word**: Highlight/lookup (future)
|
||||
|
||||
## Overlay System
|
||||
|
||||
### Navigation Overlay
|
||||
Unified overlay with tabbed interface for navigation.
|
||||
|
||||
**Tabs**:
|
||||
- **Contents**: Chapter list for TOC navigation
|
||||
- **Bookmarks**: Saved positions with jump/delete/add
|
||||
|
||||
**Interactions**:
|
||||
- **Tap chapter/bookmark**: Jump to location, close overlay
|
||||
- **Tap tab**: Switch between Contents and Bookmarks
|
||||
- **Swipe down**: Close overlay
|
||||
|
||||
### Settings Overlay
|
||||
Adjust reading preferences with real-time preview.
|
||||
|
||||
**Controls**:
|
||||
- Font size (A-, A+)
|
||||
- Line spacing (+/-)
|
||||
- Block spacing (+/-)
|
||||
- Word spacing (+/-)
|
||||
- Back to Library button
|
||||
|
||||
**Interactions**:
|
||||
- **Tap buttons**: Adjust settings immediately
|
||||
- **Swipe down**: Close overlay
|
||||
|
||||
### Word Lookup Overlay (Planned - Phase 2)
|
||||
Provide word definitions and contextual information.
|
||||
|
||||
**Features**:
|
||||
- **Dictionary**: Word definition, pronunciation
|
||||
- **X-Ray**: LLM-generated spoiler-free character/place/concept information up to current reading position
|
||||
- **Highlight**: Add colored highlight
|
||||
- **Copy**: Copy to clipboard
|
||||
|
||||
**X-Ray Behavior**:
|
||||
- Pre-generated per book via offline LLM analysis
|
||||
- Only shows information revealed up to current page (spoiler-free)
|
||||
- Character relationships, place descriptions, concept explanations
|
||||
- Entity occurrence tracking
|
||||
|
||||
## State Management
|
||||
|
||||
### Persistent State Structure
|
||||
```json
|
||||
{
|
||||
"mode": "reading|library",
|
||||
"overlay": "none|navigation|settings|word_lookup",
|
||||
"current_book": {
|
||||
"path": "/path/to/book.epub",
|
||||
"title": "Book Title",
|
||||
"author": "Author Name"
|
||||
},
|
||||
"library": {
|
||||
"books_path": "/path/to/library",
|
||||
"scan_cache": [...]
|
||||
},
|
||||
"settings": {
|
||||
"font_scale": 1.0,
|
||||
"line_spacing": 5,
|
||||
"inter_block_spacing": 15,
|
||||
"brightness": 8
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**State Location**: `~/.config/dreader/state.json`
|
||||
|
||||
**Save Triggers**:
|
||||
- Every 60 seconds (auto-save)
|
||||
- On mode change
|
||||
- On settings change
|
||||
- On application shutdown
|
||||
|
||||
**Boot Behavior**:
|
||||
- **Cold start**: Show library
|
||||
- **Resume**: Reopen last book at saved position with restored settings
|
||||
- **Error handling**: Fall back to library if book missing or state corrupt
|
||||
|
||||
### Position Persistence
|
||||
- Per-book positions stored via EbookReader bookmark system
|
||||
- Special bookmark `__auto_resume__` for last reading position
|
||||
- Position stable across font size and spacing changes
|
||||
|
||||
## Library Management
|
||||
|
||||
**Features**:
|
||||
- Scan directory for EPUB files
|
||||
- Extract metadata (title, author) and cover images
|
||||
- Cache covers to disk for performance
|
||||
- Incremental updates (scan only new/modified files)
|
||||
|
||||
**Cache Structure**:
|
||||
```
|
||||
~/.config/dreader/
|
||||
├── state.json # Application state
|
||||
├── covers/ # Cached cover images
|
||||
├── bookmarks/ # Per-book bookmarks
|
||||
├── highlights/ # Per-book highlights
|
||||
└── xray/ # X-Ray data (future)
|
||||
```
|
||||
|
||||
## Gesture Handling
|
||||
|
||||
### Reading Mode Gestures
|
||||
- `TAP`: Word selection, link following, page turn (edges)
|
||||
- `SWIPE_LEFT/RIGHT`: Page navigation
|
||||
- `SWIPE_UP` (from bottom 20%): Open navigation overlay
|
||||
- `SWIPE_DOWN` (from top 20%): Open settings overlay
|
||||
- `PINCH_IN/OUT`: Font size adjustment
|
||||
- `LONG_PRESS`: Word lookup (future)
|
||||
|
||||
### Overlay Mode Gestures
|
||||
- `TAP`: Interact with overlay elements
|
||||
- `SWIPE_DOWN`: Close overlay
|
||||
|
||||
### Library Mode Gestures
|
||||
- `TAP`: Select book
|
||||
|
||||
## Technical Requirements
|
||||
|
||||
### Performance Targets
|
||||
- Boot time: < 3 seconds
|
||||
- Page turn: < 200ms
|
||||
- Library load: < 1 second (up to 100 books)
|
||||
- State save: < 50ms (non-blocking)
|
||||
|
||||
### Platform Integration
|
||||
Application requires a display HAL (Hardware Abstraction Layer):
|
||||
```python
|
||||
class DisplayHAL(ABC):
|
||||
def show_image(image: Image.Image)
|
||||
def get_touch_events() -> Iterator[TouchEvent]
|
||||
def set_brightness(level: int)
|
||||
```
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1 (MVP) - Complete ✅
|
||||
- Core reading (page navigation, bookmarks)
|
||||
- Library browsing and book selection
|
||||
- Navigation overlay (TOC + Bookmarks)
|
||||
- Settings overlay with persistence
|
||||
- State management and auto-resume
|
||||
- Gesture handling
|
||||
- Highlighting system
|
||||
|
||||
### Phase 2 - In Progress
|
||||
- Word lookup overlay with dictionary
|
||||
- X-Ray feature (spoiler-free contextual info)
|
||||
- Enhanced library features (search, sort)
|
||||
|
||||
### Phase 3 - Future
|
||||
- Night/sepia themes
|
||||
- Full-text search within books
|
||||
- Cloud sync for bookmarks
|
||||
- PDF support
|
||||
- Reading statistics
|
||||
Reference in New Issue
Block a user