Files
dreader-application/docs/HARDWARE.md
T
dtourolleandClaude Opus 5 f7d59d025f 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>
2026-08-08 22:27:53 +02:00

24 KiB
Raw Blame History

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

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): ~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) 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

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.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

{
  "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

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 in hardware_config.json after calibrating.

Calibration

Calibration establishes which direction is "up" for your device:

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

{
  "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):

# 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

  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

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

  1. Check VCOM voltage — it must match the label on your display
  2. Check SPI is enabledls /dev/spi* should list two devices
  3. Enable SPI if missingsudo raspi-config → Interface Options → SPI → Enable
  4. Check permissionssudo 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 connectionsi2cdetect -y 1 should show 0x38
  2. Enable I2C if missingsudo raspi-config → Interface Options → I2C → Enable
  3. Confirm the module loadedlsmod | grep i2c should show i2c_dev and i2c_bcm2835
  4. Check permissionssudo 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 permissionssudo usermod -a -G gpio $USER, then log out and back in
  4. Test the pin directly with gpioget (see section 8)
  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 firstpython examples/calibrate_accelerometer.py
  2. Enable the accelerometerHardwareDisplayHAL(enable_orientation=True)
  3. Verify the calibration loadedhal.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:

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: ~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:

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

Support