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:
2026-08-08 22:27:53 +02:00
co-authored by Claude Opus 5
parent c62b8eff38
commit f7d59d025f
17 changed files with 942 additions and 1450 deletions
+552
View File
@@ -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
+853
View File
@@ -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):** ~12W
- **Total (sleep):** ~50100mW
---
## 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 711 | 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 300500ms)
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: ~1020 hours
- Standby (display sleeping): ~12 weeks
### Memory Usage
- Base application: ~3050MB
- Per book: ~1030MB 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)
+196
View File
@@ -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