DReader Hardware Abstraction Layer (HAL)
Hardware abstraction layer for the DReader e-reader application, providing a unified interface for e-ink displays, touch input, sensors, and power management.
Features
- E-ink Display: IT8951 controller support with optimized refresh modes
- Touch Input: FT5xx6 capacitive touch with gesture detection (tap, swipe, long press, pinch)
- Touch Calibration: Multi-point calibration for precise touch-to-display alignment
- Orientation Sensing: BMA400 accelerometer for auto-rotation
- Timekeeping: PCF8523 RTC with battery backup and alarms
- Power Monitoring: INA219 voltage/current/power monitoring for battery management
- Async-First: All operations use async/await for non-blocking I/O
- Polling Mode: All sensors use polling (no interrupts) for simplicity
- Testable: Virtual display mode for development without hardware
Hardware Components
| Component | Purpose | Driver | Interface |
|---|---|---|---|
| IT8951 | E-ink display controller | IT8951 | SPI |
| FT5316 | Capacitive touch panel | PyFTtxx6 | I2C |
| BMA400 | 3-axis accelerometer | PyBMA400 | I2C |
| PCF8523 | Real-time clock | PyPCF8523 | I2C |
| INA219 | Power monitor | pi_ina219 | I2C |
Installation
From Source
cd dreader-hal
pip install -e .
For Raspberry Pi
pip install -e .[rpi]
Dependencies
External driver libraries are included in external/:
IT8951- E-ink display controllerPyFTtxx6- Touch controller (our repo, can modify)PyBMA400- Accelerometer (our repo, can modify)PyPCF8523- RTC (our repo, can modify)pi_ina219- Power monitor
Quick Start
Basic Usage
import asyncio
from dreader_hal import EReaderDisplayHAL, GestureType
from PIL import Image
async def main():
# Create HAL
hal = EReaderDisplayHAL(
width=800,
height=1200,
vcom=-2.0, # Check your device label
virtual_display=False, # Set True for testing
)
# Initialize
await hal.initialize()
# Display an image
image = Image.open("my_page.png")
await hal.show_image(image)
# Handle touch events
while True:
event = await hal.get_touch_event()
if event:
if event.gesture == GestureType.SWIPE_LEFT:
print("Next page!")
elif event.gesture == GestureType.SWIPE_RIGHT:
print("Previous page!")
elif event.gesture == GestureType.TAP:
print(f"Tapped at ({event.x}, {event.y})")
# Cleanup
await hal.cleanup()
if __name__ == "__main__":
asyncio.run(main())
Testing Without Hardware
Use virtual display mode for development:
hal = EReaderDisplayHAL(
width=800,
height=1200,
virtual_display=True, # Uses Tkinter window
enable_orientation=False, # No accelerometer
enable_rtc=False, # No RTC
enable_power_monitor=False, # No INA219
)
Architecture
┌─────────────────────────────────────────────────────────┐
│ DReaderApplication │
│ (from dreader-application project) │
└──────────────────┬──────────────────────────────────────┘
│
│ Uses DisplayHAL interface
↓
┌─────────────────────────────────────────────────────────┐
│ EReaderDisplayHAL │
│ (Main HAL implementation) │
│ │
│ Methods: │
│ • async show_image(PIL.Image) │
│ • async get_touch_event() -> TouchEvent │
│ • async set_brightness(level: int) │
│ • async get_battery_level() -> float │
│ • async get_power_stats() -> PowerStats │
└──────────────────┬──────────────────────────────────────┘
│
│ Orchestrates
↓
┌─────────────────────────────────────────────────────────┐
│ Hardware Component Wrappers │
│ │
│ IT8951DisplayDriver - E-ink rendering │
│ FT5xx6TouchDriver - Touch + gesture detection │
│ BMA400OrientationSensor - Device orientation │
│ PCF8523RTC - Timekeeping & alarms │
│ INA219PowerMonitor - Battery monitoring │
└─────────────────────────────────────────────────────────┘
Gesture Detection
The HAL implements comprehensive gesture detection:
| Gesture | Detection Criteria | Usage |
|---|---|---|
| TAP | < 30px movement, < 300ms | Select links, buttons |
| LONG_PRESS | < 30px movement, >= 500ms | Context menu, definitions |
| SWIPE_LEFT | Horizontal, dx < -30px | Next page |
| SWIPE_RIGHT | Horizontal, dx > 30px | Previous page |
| SWIPE_UP | Vertical, dy < -30px | Open navigation |
| SWIPE_DOWN | Vertical, dy > 30px | Open settings |
| PINCH_IN | Two-finger distance decrease | Decrease font size |
| PINCH_OUT | Two-finger distance increase | Increase font size |
E-ink Optimization
The display driver implements several e-ink optimizations:
Refresh Modes
- FAST (DU mode): ~200ms, for text/UI updates
- QUALITY (GC16 mode): ~1000ms, for images
- FULL (INIT mode): Full refresh to clear ghosting
- AUTO: Automatically chooses based on content
Power Saving
Automatic Display Sleep: The display automatically enters sleep mode after each update to save power. E-ink displays only need power during refresh, not for static content. This significantly extends battery life.
To disable auto-sleep (e.g., for rapid successive updates):
hal = EReaderDisplayHAL(
width=800,
height=1200,
auto_sleep_display=False # Keep display awake
)
Automatic Ghosting Prevention
Full refresh every 10 pages to clear accumulated ghosting artifacts.
Dithering
Floyd-Steinberg dithering improves grayscale image quality on e-ink.
API Reference
EReaderDisplayHAL
Main HAL class implementing the DisplayHAL interface.
Constructor
EReaderDisplayHAL(
width: int = 800,
height: int = 1200,
vcom: float = -2.0,
spi_hz: int = 24_000_000,
virtual_display: bool = False,
auto_sleep_display: bool = True,
enable_orientation: bool = True,
enable_rtc: bool = True,
enable_power_monitor: bool = True,
shunt_ohms: float = 0.1,
battery_capacity_mah: float = 3000,
)
Methods
Core Methods (from DisplayHAL spec):
async initialize()- Initialize all hardwareasync cleanup()- Cleanup resourcesasync show_image(image: Image.Image)- Display image on screenasync get_touch_event() -> Optional[TouchEvent]- Get touch event with gestureasync set_brightness(level: int)- Set brightness 0-10
Extended Methods:
Power Management:
async get_battery_level() -> float- Get battery % (0-100)async get_power_stats() -> PowerStats- Get detailed power statsasync is_low_battery(threshold: float = 20.0) -> bool- Check low batteryasync set_low_power_mode(enabled: bool)- Enable/disable low power mode
Orientation:
async enable_orientation_monitoring()- Start orientation monitoringasync disable_orientation_monitoring()- Stop orientation monitoringcurrent_orientationproperty - Get current device orientation
RTC (Real-Time Clock):
async get_datetime() -> time.struct_time- Get current date/time from RTCasync set_datetime(dt: time.struct_time)- Set RTC date/timeasync set_alarm(minute: int, hour: int)- Set RTC alarm (minute precision)
Types
from dreader_hal import GestureType, TouchEvent, PowerStats, Orientation
# Touch event
event = TouchEvent(
gesture=GestureType.TAP,
x=400,
y=600,
timestamp_ms=1234567890.0
)
# Power statistics
stats = PowerStats(
voltage=3.7,
current=150.0,
power=555.0,
battery_percent=85.0,
time_remaining=180, # minutes
is_charging=False
)
# Orientation
orientation = Orientation.PORTRAIT_0 # or LANDSCAPE_90, etc.
Real-Time Clock (RTC)
The HAL includes a PCF8523 RTC for accurate timekeeping with battery backup:
Features
- Battery-backed timekeeping - Maintains time when device is off
- Alarm support - Minute-precision alarms for wake events
- Auto-sync - Syncs with system time after power loss
- Calibration - Clock accuracy adjustment support
Usage
import time
# Get current time
current_time = await hal.get_datetime()
print(time.strftime("%Y-%m-%d %H:%M:%S", current_time))
# Set time
new_time = time.localtime() # Use current system time
await hal.set_datetime(new_time)
# Set alarm for 7:30 AM
await hal.set_alarm(hour=7, minute=30)
See examples/rtc_demo.py for a complete example.
Touchscreen Calibration
The HAL includes touchscreen calibration to align touch coordinates with display pixels:
# Run calibration (displays touch targets)
cd examples
python3 calibrate_touch.py
# Test calibration
python3 test_calibration.py
Calibration uses affine transformation with least-squares fitting to achieve sub-5-pixel accuracy. See CALIBRATION.md for detailed documentation.
Examples
See the examples/ directory for complete examples:
- simple_display.py - Basic display and touch demo
- battery_monitor.py - Power monitoring demo
- rtc_demo.py - RTC timekeeping and alarms demo
- calibrate_touch.py - Touchscreen calibration utility
- test_calibration.py - Test calibration accuracy
Run examples:
cd examples
python3 simple_display.py
Hardware Setup
Connections
All components connect via I2C and SPI:
| Component | Interface | Default Address | Pins |
|---|---|---|---|
| IT8951 | SPI | N/A | MOSI, MISO, SCK, CS, HRDY, RESET |
| FT5316 | I2C | 0x38 | SDA, SCL |
| BMA400 | I2C | 0x14 | SDA, SCL |
| PCF8523 | I2C | 0x68 | SDA, SCL |
| INA219 | I2C | 0x40 | SDA, SCL |
Enable I2C and SPI
On Raspberry Pi:
sudo raspi-config
# Navigate to: Interface Options > I2C > Enable
# Navigate to: Interface Options > SPI > Enable
Verify Devices
# Check I2C devices
i2cdetect -y 1
# Expected output shows addresses: 0x14, 0x38, 0x40, 0x68
Testing
The project uses pytest for testing:
# Install dev dependencies
pip install -e .[dev]
# Run tests
pytest tests/
# Run with coverage
pytest --cov=dreader_hal tests/
Note: Hardware-specific tests require actual devices connected. Mock tests run without hardware.
Development
Project Structure
dreader-hal/
├── src/
│ └── dreader_hal/
│ ├── __init__.py
│ ├── hal.py # DisplayHAL abstract class
│ ├── types.py # Type definitions
│ ├── gesture.py # Gesture detection
│ ├── ereader_hal.py # Main HAL implementation
│ ├── display/
│ │ └── it8951.py # Display driver wrapper
│ ├── touch/
│ │ └── ft5xx6.py # Touch driver wrapper
│ ├── sensors/
│ │ └── bma400.py # Accelerometer wrapper
│ ├── rtc/
│ │ └── pcf8523.py # RTC wrapper
│ └── power/
│ └── ina219.py # Power monitor wrapper
├── external/ # External driver libraries
│ ├── IT8951/
│ ├── PyFTtxx6/ # Our repo - can modify
│ ├── PyBMA400/ # Our repo - can modify
│ ├── PyPCF8523/ # Our repo - can modify
│ └── pi_ina219/
├── examples/ # Example scripts
├── tests/ # Test suite
├── setup.py
├── requirements.txt
└── README.md
Adding Tests
Since PyBMA400, PyFTtxx6, and PyPCF8523 are our repos, we can add tests directly to them as needed.
For HAL-level tests, use the tests/ directory:
# tests/unit/test_hal.py
import pytest
from dreader_hal import EReaderDisplayHAL
@pytest.mark.asyncio
async def test_hal_initialization():
hal = EReaderDisplayHAL(virtual_display=True)
await hal.initialize()
assert hal._initialized
await hal.cleanup()
Performance
Target performance metrics (from HAL spec):
| Metric | Target | Critical |
|---|---|---|
| Image display latency | < 100ms | < 500ms |
| Touch event latency | < 50ms | < 200ms |
| E-ink refresh (fast) | < 200ms | < 500ms |
| E-ink refresh (full) | < 1000ms | < 2000ms |
Troubleshooting
Display not working
- Check SPI is enabled:
ls /dev/spi* - Verify connections (MOSI, MISO, SCK, CS, HRDY, RESET)
- Check VCOM voltage matches your device
Touch not detecting
- Check I2C device:
i2cdetect -y 1(should see 0x38) - Verify SDA/SCL connections
- Check polling interval (default 10ms)
Power monitor errors
- Verify shunt resistor value (default 0.1Ω)
- Check I2C address (default 0x40)
- Ensure max_expected_amps is appropriate
Contributing
Contributions are welcome! Please:
- Follow PEP 8 style guidelines
- Add tests for new features
- Update documentation
- Submit pull requests to our Gitea repo
License
MIT License - see LICENSE file for details.
Credits
- IT8951 Driver: GregDMeyer/IT8951
- FT5xx6 Driver: PyFTtxx6 (our repo)
- BMA400 Driver: PyBMA400 (our repo)
- PCF8523 Driver: PyPCF8523 (our repo)
- INA219 Driver: chrisb2/pi_ina219
Related Projects
- dreader-application - Main e-reader application
- pyWebLayout - HTML/EPUB rendering engine
Support
For issues and questions: