# 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)
