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>
24 KiB
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 instead.
Contents
- Quick Start
- 1. Hardware Requirements
- 2. Wiring
- 3. Software Installation
- 4. Configuration
- 5. Running DReader
- 6. Input Reference
- 7. Accelerometer Page Flipping
- 8. Verifying Hardware
- 9. Troubleshooting
- 10. Performance Notes
- Appendix: Programmatic Usage
Quick Start
On a Raspberry Pi with the hardware already wired:
# 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 — 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) 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": falsein 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
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
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:
./install_hardware_drivers.sh
To do it by hand — dreader-hal keeps its driver dependencies in its own
external/ directory:
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
pip install RPi.GPIO spidev
5. Permissions
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 |
Generating the Config
The interactive setup script detects your hardware and writes hardware_config.json:
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.vcomin the config, or pass--vcom -2.06on the command line - Using the wrong VCOM can damage the display
hardware_config.json Reference
{
"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 |
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:
{
"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.
5. Running DReader
With a Config File (Recommended)
python examples/run_on_hardware_config.py
Direct Command Line
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:
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 |
tilt_backward |
Previous page — see section 7 |
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 inhardware_config.jsonafter calibrating.
Calibration
Calibration establishes which direction is "up" for your device:
python examples/calibrate_accelerometer.py
- The display shows an arrow pointing in the direction of gravity
- Rotate the device until the arrow points up
- Tap the screen to save
- Calibration is written to
accelerometer_config.json
Calibration File Format
{
"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
- Read the accelerometer for (x, y, z) acceleration in m/s²
- Normalise both the current gravity vector and the calibrated up vector
- Compute the tilt angle by projecting gravity onto the plane perpendicular to the up vector
- Compare against
tilt_threshold - Determine direction from the sign of the perpendicular y-component
- Debounce to suppress repeat triggers
Given a calibrated up vector U = (ux, uy, uz) and current gravity G = (gx, gy, gz):
# 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
(get_tilt_gesture), with the gesture types defined in
dreader/gesture.py and handled in
dreader/handlers/gestures.py.
Using Tilt in Your Own Code
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:
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:
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
# 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
- Orientation lock — tilt detection assumes a fixed device orientation, so auto-rotation can interfere with it.
- Movement — walking may cause false positives. Raise the threshold or disable tilt while moving.
- Calibration drift — the accelerometer drifts over time; re-calibrate periodically.
- Simplified direction heuristic — complex orientations may be misread.
- Single axis — only tilt in one plane is detected; left/right tilts are not distinguished.
8. Verifying Hardware
Check I2C Devices
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
ls /dev/spi*
# Should show: /dev/spidev0.0 /dev/spidev0.1
Check Buttons
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:
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
- Check VCOM voltage — it must match the label on your display
- Check SPI is enabled —
ls /dev/spi*should list two devices - Enable SPI if missing —
sudo raspi-config→ Interface Options → SPI → Enable - Check permissions —
sudo usermod -a -G spi $USER, then log out and back in - Isolate the problem — run with
--virtualto test the software without hardware
Touch Not Working
- Check I2C connections —
i2cdetect -y 1should show0x38 - Enable I2C if missing —
sudo raspi-config→ Interface Options → I2C → Enable - Confirm the module loaded —
lsmod | grep i2cshould showi2c_devandi2c_bcm2835 - Check permissions —
sudo usermod -a -G i2c $USER, then log out and back in - Calibrate touch — see the dreader-hal calibration docs
Buttons Not Working
- Check the wiring polarity — prev/next go to GND; the power button goes to 3.3V
- Verify the pin numbers are BCM, not physical pin numbers
- Check permissions —
sudo usermod -a -G gpio $USER, then log out and back in - Test the pin directly with
gpioget(see section 8) - Check for conflicts — make sure no other program holds those GPIOs
Buttons Trigger Multiple Times
- Increase
bounce_time_msin the config (try 300–500ms) - Add a hardware debounce capacitor (0.1µF between GPIO and GND)
- Check for loose connections
Button Performs the Wrong Action
- Check the
gesturefield for that button inhardware_config.json - Run with
--verboseto see which gesture is actually emitted
Tilt Gestures Not Working
- Calibrate first —
python examples/calibrate_accelerometer.py - Enable the accelerometer —
HardwareDisplayHAL(enable_orientation=True) - Verify the calibration loaded —
hal.load_accelerometer_calibration()returnsTrue - Confirm you are polling — call
get_event()orget_tilt_gesture()in your loop - Adjust sensitivity — tune
tilt_thresholdanddebounce_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:
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:
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:
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 — application features and usage
- ARCHITECTURE.md — system architecture and design
- HAL_IMPLEMENTATION_SPEC.md — writing a HAL for other hardware
- examples/ — runnable examples
- external/dreader-hal/README.md — HAL library details
Support
- Hardware issues: dreader-hal issues
- Application issues: dreader-application issues