HW integratation
This commit is contained in:
parent
5a573c901e
commit
70c0b4a1f2
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "external/dreader-hal"]
|
||||
path = external/dreader-hal
|
||||
url = https://gitea.tourolle.paris/dtourolle/dreader-hal
|
||||
286
GPIO_BUTTONS.md
Normal file
286
GPIO_BUTTONS.md
Normal file
@ -0,0 +1,286 @@
|
||||
# GPIO Button Configuration Guide
|
||||
|
||||
This guide explains how to configure physical buttons for the DReader e-reader.
|
||||
|
||||
## Overview
|
||||
|
||||
Physical buttons provide tactile feedback for page turns and navigation without requiring touch input. Buttons are connected between GPIO pins and GND, using internal pull-up resistors.
|
||||
|
||||
## Hardware Setup
|
||||
|
||||
### Basic Button Wiring
|
||||
|
||||
```
|
||||
+3.3V
|
||||
|
|
||||
R (internal pull-up)
|
||||
|
|
||||
GPIO --|------ Button ------ GND
|
||||
|
|
||||
(to BCM2835)
|
||||
```
|
||||
|
||||
When the button is pressed, it connects the GPIO pin to GND (0V), pulling the pin LOW.
|
||||
|
||||
### Recommended Button Layout
|
||||
|
||||
For this e-reader device with 3 buttons:
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ │
|
||||
│ [Power Off] │ ← Side button (GPIO 21)
|
||||
│ │
|
||||
│ │
|
||||
│ E-INK │
|
||||
│ DISPLAY │
|
||||
│ 1872x1404 │
|
||||
│ │
|
||||
│ [Prev] [Next] │ ← Bottom edge
|
||||
│ │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
**Button Mapping:**
|
||||
- **Previous Page** (GPIO 22) - Bottom left - Previous page
|
||||
- **Next Page** (GPIO 27) - Bottom right - Next page
|
||||
- **Power Off** (GPIO 21) - Side button - Shutdown device (long press)
|
||||
|
||||
## Software Configuration
|
||||
|
||||
### Using Interactive Setup (Recommended)
|
||||
|
||||
```bash
|
||||
sudo python3 setup_rpi.py
|
||||
```
|
||||
|
||||
The setup script will:
|
||||
1. Ask if you want GPIO buttons enabled
|
||||
2. Let you configure each button individually
|
||||
3. Allow custom GPIO pin assignments
|
||||
4. Generate hardware_config.json automatically
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
Edit `hardware_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"gpio_buttons": {
|
||||
"enabled": true,
|
||||
"pull_up": true,
|
||||
"bounce_time_ms": 200,
|
||||
"buttons": [
|
||||
{
|
||||
"name": "prev_page",
|
||||
"gpio": 22,
|
||||
"gesture": "swipe_right",
|
||||
"description": "Previous page button"
|
||||
},
|
||||
{
|
||||
"name": "next_page",
|
||||
"gpio": 27,
|
||||
"gesture": "swipe_left",
|
||||
"description": "Next page button"
|
||||
},
|
||||
{
|
||||
"name": "power_off",
|
||||
"gpio": 21,
|
||||
"gesture": "long_press",
|
||||
"description": "Power off button (long press to shutdown)"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
- **enabled** (bool): Enable/disable all GPIO buttons
|
||||
- **pull_up** (bool): Use internal pull-up resistors (always true for button-to-GND wiring)
|
||||
- **bounce_time_ms** (int): Debounce time in milliseconds (default 200ms)
|
||||
- **buttons** (array): List of button configurations
|
||||
|
||||
### Button Configuration
|
||||
|
||||
Each button has:
|
||||
- **name** (string): Unique identifier for the button
|
||||
- **gpio** (int): BCM GPIO pin number (2-27)
|
||||
- **gesture** (string): Gesture type to generate when pressed
|
||||
- **description** (string): Human-readable description
|
||||
|
||||
### Available Gestures
|
||||
|
||||
Buttons can trigger any gesture type:
|
||||
|
||||
| Gesture | Description | Typical Use |
|
||||
|---------|-------------|-------------|
|
||||
| `swipe_left` | Swipe left | Next page |
|
||||
| `swipe_right` | Swipe right | Previous page |
|
||||
| `swipe_up` | Swipe up from bottom | Open navigation/TOC |
|
||||
| `swipe_down` | Swipe down from top | Open settings |
|
||||
| `tap` | Single tap | Select item |
|
||||
| `long_press` | Hold | Context menu |
|
||||
| `pinch_in` | Pinch zoom out | Decrease font size |
|
||||
| `pinch_out` | Pinch zoom in | Increase font size |
|
||||
|
||||
## GPIO Pin Selection
|
||||
|
||||
### Safe GPIO Pins (BCM numbering)
|
||||
|
||||
**Recommended for buttons:**
|
||||
- GPIO 5, 6, 12, 13, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27
|
||||
|
||||
**Avoid these pins:**
|
||||
- GPIO 2, 3 - I2C (SDA, SCL) - Used for touch, sensors
|
||||
- GPIO 7-11 - SPI - Used for e-ink display
|
||||
- GPIO 14, 15 - UART - Used for serial console
|
||||
- GPIO 0, 1 - Reserved for ID EEPROM
|
||||
|
||||
### Pin Layout (BCM Mode)
|
||||
|
||||
```
|
||||
3V3 (1) (2) 5V
|
||||
GPIO2 (3) (4) 5V GPIO 2,3 = I2C
|
||||
GPIO3 (5) (6) GND
|
||||
GPIO4 (7) (8) GPIO14 GPIO 7-11 = SPI
|
||||
GND (9) (10) GPIO15 GPIO 14,15 = UART
|
||||
GPIO17 (11) (12) GPIO18
|
||||
GPIO27 (13) (14) GND
|
||||
GPIO22 (15) (16) GPIO23 ← Good for buttons
|
||||
3V3 (17) (18) GPIO24 ← Good for buttons
|
||||
GPIO10 (19) (20) GND
|
||||
GPIO9 (21) (22) GPIO25 ← Good for buttons
|
||||
GPIO11 (23) (24) GPIO8
|
||||
GND (25) (26) GPIO7
|
||||
GPIO0 (27) (28) GPIO1
|
||||
GPIO5 (29) (30) GND ← Good for buttons
|
||||
GPIO6 (31) (32) GPIO12 ← Good for buttons
|
||||
GPIO13 (33) (34) GND ← Good for buttons
|
||||
GPIO19 (35) (36) GPIO16 ← Good for buttons
|
||||
GPIO26 (37) (38) GPIO20 ← Good for buttons
|
||||
GND (39) (40) GPIO21 ← Good for buttons
|
||||
```
|
||||
|
||||
## Testing Buttons
|
||||
|
||||
### Test Button Connections
|
||||
|
||||
```bash
|
||||
# Install GPIO utilities
|
||||
sudo apt install gpiod
|
||||
|
||||
# Monitor GPIO 23 (next page button)
|
||||
gpioget gpiochip0 23
|
||||
|
||||
# Press button - should show 0 (LOW)
|
||||
# Release button - should show 1 (HIGH with pull-up)
|
||||
```
|
||||
|
||||
### Test in Application
|
||||
|
||||
Run with verbose logging to see button events:
|
||||
|
||||
```bash
|
||||
python examples/run_on_hardware_config.py --verbose
|
||||
```
|
||||
|
||||
Press each button and verify you see log messages like:
|
||||
```
|
||||
Button pressed: next_page (GPIO 23)
|
||||
Button event queued: next_page -> swipe_left
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Buttons Not Working
|
||||
|
||||
**Problem:** Buttons don't generate events
|
||||
|
||||
**Solutions:**
|
||||
1. Check wiring - button should connect GPIO to GND
|
||||
2. Verify GPIO pin number in config (BCM mode, not physical pin)
|
||||
3. Check permissions: `sudo usermod -a -G gpio $USER` then log out/in
|
||||
4. Test GPIO with `gpioget` (see above)
|
||||
5. Check logs: `python examples/run_on_hardware_config.py --verbose`
|
||||
|
||||
### False Triggers
|
||||
|
||||
**Problem:** Button triggers multiple times from single press
|
||||
|
||||
**Solutions:**
|
||||
1. Increase `bounce_time_ms` in config (try 300-500ms)
|
||||
2. Add hardware debounce capacitor (0.1µF between GPIO and GND)
|
||||
3. Check for loose connections
|
||||
|
||||
### Wrong Action
|
||||
|
||||
**Problem:** Button does wrong action
|
||||
|
||||
**Solutions:**
|
||||
1. Check `gesture` field in button config
|
||||
2. Verify button name matches intended function
|
||||
3. Check logs to see what gesture is generated
|
||||
|
||||
## Advanced: Custom Button Functions
|
||||
|
||||
You can map buttons to any gesture, creating custom layouts:
|
||||
|
||||
### Example: Reading Mode Buttons
|
||||
|
||||
```json
|
||||
{
|
||||
"gpio_buttons": {
|
||||
"enabled": true,
|
||||
"buttons": [
|
||||
{"name": "next", "gpio": 23, "gesture": "swipe_left"},
|
||||
{"name": "prev", "gpio": 24, "gesture": "swipe_right"},
|
||||
{"name": "zoom_in", "gpio": 25, "gesture": "pinch_out"},
|
||||
{"name": "zoom_out", "gpio": 22, "gesture": "pinch_in"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example: Simple 2-Button Layout
|
||||
|
||||
```json
|
||||
{
|
||||
"gpio_buttons": {
|
||||
"enabled": true,
|
||||
"buttons": [
|
||||
{"name": "next", "gpio": 23, "gesture": "swipe_left"},
|
||||
{"name": "prev", "gpio": 24, "gesture": "swipe_right"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Hardware Tips
|
||||
|
||||
### Button Quality
|
||||
|
||||
- Use momentary pushbuttons (normally open)
|
||||
- Tactile switches provide good feedback
|
||||
- Rated for at least 10,000 cycles
|
||||
- Consider waterproof buttons for outdoor use
|
||||
|
||||
### Mounting
|
||||
|
||||
- Mount buttons accessible from device edge
|
||||
- Label buttons for user convenience
|
||||
- Consider button guards to prevent accidental presses
|
||||
- Use hot glue or button caps for secure mounting
|
||||
|
||||
### Wiring
|
||||
|
||||
- Keep wires short to reduce noise
|
||||
- Use stranded wire for flexibility
|
||||
- Consider using a ribbon cable for clean routing
|
||||
- Add strain relief at connection points
|
||||
|
||||
## See Also
|
||||
|
||||
- [HARDWARE_SETUP.md](HARDWARE_SETUP.md) - Complete hardware integration guide
|
||||
- [hardware_config.json](hardware_config.json) - Example configuration
|
||||
- [dreader/gpio_buttons.py](dreader/gpio_buttons.py) - Button handler source code
|
||||
230
HARDWARE_PINOUT.md
Executable file
230
HARDWARE_PINOUT.md
Executable file
@ -0,0 +1,230 @@
|
||||
# DReader Hardware Pinout Reference
|
||||
|
||||
Quick reference for the DReader e-ink device hardware configuration.
|
||||
|
||||
## Display Specifications
|
||||
|
||||
- **Resolution:** 1872 × 1404 pixels
|
||||
- **Controller:** IT8951 (SPI)
|
||||
- **Touch Panel:** FT5316 (I2C)
|
||||
|
||||
## GPIO Pin Assignments
|
||||
|
||||
### Buttons (BCM Numbering)
|
||||
|
||||
| GPIO | Function | Action | Notes |
|
||||
|------|----------|--------|-------|
|
||||
| 21 | Power Off | Long Press (500ms+) | Shutdown button |
|
||||
| 22 | Previous Page | Swipe Right | Left button |
|
||||
| 27 | Next Page | Swipe Left | Right button |
|
||||
|
||||
**Wiring:** All buttons connect between GPIO and GND (pull-up resistors enabled in software)
|
||||
|
||||
### SPI (IT8951 E-ink Display)
|
||||
|
||||
| GPIO | Function | Pin |
|
||||
|------|----------|-----|
|
||||
| 8 | SPI0 CE0 | 24 |
|
||||
| 9 | SPI0 MISO | 21 |
|
||||
| 10 | SPI0 MOSI | 19 |
|
||||
| 11 | SPI0 SCLK | 23 |
|
||||
| 17 | RST | 11 |
|
||||
| 24 | HRDY | 18 |
|
||||
|
||||
### I2C (Touch, Sensors, RTC, Power Monitor)
|
||||
|
||||
| GPIO | Function | Pin | Devices |
|
||||
|------|----------|-----|---------|
|
||||
| 2 | I2C1 SDA | 3 | FT5316 (0x38), BMA400 (0x14), PCF8523 (0x68), INA219 (0x40) |
|
||||
| 3 | I2C1 SCL | 5 | All I2C devices |
|
||||
|
||||
**Note:** I2C bus is shared by all I2C devices. Each device has a unique address.
|
||||
|
||||
## I2C Device Addresses
|
||||
|
||||
| Address | Device | Description |
|
||||
|---------|--------|-------------|
|
||||
| 0x38 | FT5316 | Capacitive touch panel |
|
||||
| 0x14 | BMA400 | 3-axis accelerometer (optional) |
|
||||
| 0x68 | PCF8523 | Real-time clock (optional) |
|
||||
| 0x40 | INA219 | Power monitor (optional) |
|
||||
|
||||
## 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
|
||||
GPIO22 (15) (16) GPIO23
|
||||
3V3 (17) (18) GPIO24 ← Display HRDY
|
||||
GPIO10 (19) (20) GND ← SPI0 MOSI
|
||||
GPIO9 (21) (22) GPIO25
|
||||
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
|
||||
|
||||
|
||||
Button Connections:
|
||||
GPIO21 ──┤ ├── GND (Power off)
|
||||
GPIO22 ──┤ ├── GND (Previous page)
|
||||
GPIO27 ──┤ ├── GND (Next page)
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### hardware_config.json
|
||||
|
||||
```json
|
||||
{
|
||||
"display": {
|
||||
"width": 1872,
|
||||
"height": 1404,
|
||||
"vcom": -2.0,
|
||||
"spi_hz": 24000000
|
||||
},
|
||||
"gpio_buttons": {
|
||||
"enabled": true,
|
||||
"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"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Connections
|
||||
|
||||
### Check I2C Devices
|
||||
|
||||
```bash
|
||||
# Scan I2C bus 1 (GPIO 2/3)
|
||||
i2cdetect -y 1
|
||||
|
||||
# Expected output:
|
||||
# 0 1 2 3 4 5 6 7 8 9 a b c d e f
|
||||
# 00: -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
# 10: -- -- -- -- 14 -- -- -- -- -- -- -- -- -- -- --
|
||||
# 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
# 30: -- -- -- -- -- -- -- -- 38 -- -- -- -- -- -- --
|
||||
# 40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
# 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
|
||||
# 60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- --
|
||||
# 70: -- -- -- -- -- -- -- --
|
||||
```
|
||||
|
||||
### Check SPI
|
||||
|
||||
```bash
|
||||
ls /dev/spi*
|
||||
# Should show: /dev/spidev0.0 /dev/spidev0.1
|
||||
```
|
||||
|
||||
### Test GPIO Buttons
|
||||
|
||||
```bash
|
||||
# Install GPIO tools
|
||||
sudo apt install gpiod
|
||||
|
||||
# Test previous page button (GPIO 22)
|
||||
gpioget gpiochip0 22
|
||||
# Press button: shows 0 (LOW)
|
||||
# Release button: shows 1 (HIGH, pulled up)
|
||||
|
||||
# Test next page button (GPIO 27)
|
||||
gpioget gpiochip0 27
|
||||
|
||||
# Test power button (GPIO 21)
|
||||
gpioget gpiochip0 21
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Clone and install
|
||||
git clone https://gitea.tourolle.paris/dtourolle/dreader-application.git
|
||||
cd dreader-application
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -e .
|
||||
./install_hardware_drivers.sh
|
||||
|
||||
# 2. Run interactive setup
|
||||
sudo python3 setup_rpi.py
|
||||
|
||||
# 3. Run DReader
|
||||
python examples/run_on_hardware_config.py
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No I2C Devices Detected
|
||||
|
||||
```bash
|
||||
# Enable I2C
|
||||
sudo raspi-config
|
||||
# Navigate to: Interface Options -> I2C -> Enable
|
||||
|
||||
# Check I2C is loaded
|
||||
lsmod | grep i2c
|
||||
# Should show: i2c_dev, i2c_bcm2835
|
||||
|
||||
# Add user to i2c group
|
||||
sudo usermod -a -G i2c $USER
|
||||
# Log out and back in
|
||||
```
|
||||
|
||||
### SPI Not Working
|
||||
|
||||
```bash
|
||||
# Enable SPI
|
||||
sudo raspi-config
|
||||
# Navigate to: Interface Options -> SPI -> Enable
|
||||
|
||||
# Check SPI devices
|
||||
ls -l /dev/spi*
|
||||
|
||||
# Add user to spi group
|
||||
sudo usermod -a -G spi $USER
|
||||
```
|
||||
|
||||
### Buttons Not Responding
|
||||
|
||||
```bash
|
||||
# Add user to gpio group
|
||||
sudo usermod -a -G gpio $USER
|
||||
|
||||
# Test button with direct GPIO access
|
||||
sudo gpioget gpiochip0 22 # Prev button
|
||||
sudo gpioget gpiochip0 27 # Next button
|
||||
sudo gpioget gpiochip0 21 # Power button
|
||||
|
||||
# Check for conflicts
|
||||
# Make sure no other programs are using these GPIOs
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [HARDWARE_SETUP.md](HARDWARE_SETUP.md) - Complete setup guide
|
||||
- [GPIO_BUTTONS.md](GPIO_BUTTONS.md) - Button configuration reference
|
||||
- [hardware_config.json](hardware_config.json) - Hardware configuration file
|
||||
472
HARDWARE_SETUP.md
Normal file
472
HARDWARE_SETUP.md
Normal file
@ -0,0 +1,472 @@
|
||||
# Hardware Integration Guide
|
||||
|
||||
This guide explains how to run DReader on real e-ink hardware using the dreader-hal library.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**On Raspberry Pi:**
|
||||
```bash
|
||||
# 1. Clone and setup
|
||||
git clone https://gitea.tourolle.paris/dtourolle/dreader-application.git
|
||||
cd dreader-application
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# 2. Install everything
|
||||
pip install -e .
|
||||
./install_hardware_drivers.sh
|
||||
|
||||
# 3. Run interactive setup (detects hardware and configures)
|
||||
sudo python3 setup_rpi.py
|
||||
|
||||
# 4. Run DReader (uses hardware_config.json)
|
||||
python examples/run_on_hardware_config.py
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
The DReader application uses a plugin-based Hardware Abstraction Layer (HAL) architecture. You can choose different HAL implementations:
|
||||
|
||||
- **PygameDisplayHAL** ([dreader/hal_pygame.py](dreader/hal_pygame.py)) - Desktop testing with pygame window
|
||||
- **HardwareDisplayHAL** ([dreader/hal_hardware.py](dreader/hal_hardware.py)) - Real e-ink hardware via dreader-hal
|
||||
|
||||
### Configuration Files
|
||||
|
||||
- **hardware_config.json** - Hardware configuration (display, buttons, sensors)
|
||||
- **accelerometer_config.json** - Accelerometer calibration for tilt gestures
|
||||
|
||||
## Hardware Requirements
|
||||
|
||||
### Required Components
|
||||
- **Raspberry Pi** (or compatible SBC)
|
||||
- **IT8951 E-ink Display Controller** (1872×1404 resolution)
|
||||
- **FT5316 Capacitive Touch Panel**
|
||||
|
||||
### Optional Components
|
||||
- **BMA400 Accelerometer** - Auto-rotation based on device orientation
|
||||
- **PCF8523 RTC** - Timekeeping with battery backup
|
||||
- **INA219 Power Monitor** - Battery level monitoring
|
||||
|
||||
## Software Installation
|
||||
|
||||
### 1. Install System Dependencies
|
||||
|
||||
On Raspberry Pi:
|
||||
|
||||
```bash
|
||||
# Update package list
|
||||
sudo apt update
|
||||
|
||||
# Install system dependencies
|
||||
sudo apt install -y python3-dev python3-pip python3-venv
|
||||
sudo apt install -y i2c-tools python3-smbus
|
||||
|
||||
# Enable I2C and SPI interfaces
|
||||
sudo raspi-config
|
||||
# Navigate to: Interface Options -> Enable I2C and SPI
|
||||
```
|
||||
|
||||
### 2. Clone and Set Up DReader Application
|
||||
|
||||
```bash
|
||||
# Clone the application
|
||||
git clone https://gitea.tourolle.paris/dtourolle/dreader-application.git
|
||||
cd dreader-application
|
||||
|
||||
# Initialize and update submodules (includes dreader-hal)
|
||||
git submodule update --init --recursive
|
||||
|
||||
# Create virtual environment
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# Install dreader-application
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
### 3. Install dreader-hal and Dependencies
|
||||
|
||||
The dreader-hal library has external driver dependencies in its `external/` directory:
|
||||
|
||||
```bash
|
||||
# Install dreader-hal in editable mode
|
||||
pip install -e external/dreader-hal
|
||||
|
||||
# Install external driver libraries
|
||||
cd external/dreader-hal/external
|
||||
|
||||
# Install each driver
|
||||
pip install -e IT8951
|
||||
pip install -e PyFTtxx6
|
||||
pip install -e PyBMA400
|
||||
pip install -e PyPCF8523
|
||||
pip install -e pi_ina219
|
||||
|
||||
cd ../../.. # Back to dreader-application root
|
||||
```
|
||||
|
||||
### 4. Install Raspberry Pi GPIO (if on RPi)
|
||||
|
||||
```bash
|
||||
pip install RPi.GPIO spidev
|
||||
```
|
||||
|
||||
## Hardware Wiring
|
||||
|
||||
### IT8951 E-ink Display (SPI)
|
||||
| IT8951 Pin | Raspberry Pi Pin | 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 |
|
||||
|
||||
### FT5316 Touch Panel (I2C)
|
||||
| FT5316 Pin | Raspberry Pi Pin | Description |
|
||||
|------------|------------------|-------------|
|
||||
| VCC | 3.3V (Pin 1) | Power supply |
|
||||
| GND | GND (Pin 6) | Ground |
|
||||
| SDA | GPIO 2 (Pin 3) | I2C Data |
|
||||
| SCL | GPIO 3 (Pin 5) | I2C Clock |
|
||||
| INT | GPIO 27 (Pin 13) | Interrupt (optional) |
|
||||
|
||||
### BMA400 Accelerometer (I2C) - Optional
|
||||
| BMA400 Pin | Raspberry Pi Pin | Description |
|
||||
|------------|------------------|-------------|
|
||||
| VCC | 3.3V (Pin 1) | Power supply |
|
||||
| GND | GND (Pin 6) | Ground |
|
||||
| SDA | GPIO 2 (Pin 3) | I2C Data |
|
||||
| SCL | GPIO 3 (Pin 5) | I2C Clock |
|
||||
| I2C Address | 0x14 or 0x15 | Check your module |
|
||||
|
||||
### PCF8523 RTC (I2C) - Optional
|
||||
| PCF8523 Pin | Raspberry Pi Pin | Description |
|
||||
|------------|------------------|-------------|
|
||||
| VCC | 3.3V (Pin 1) | Power supply |
|
||||
| GND | GND (Pin 6) | Ground |
|
||||
| SDA | GPIO 2 (Pin 3) | I2C Data |
|
||||
| SCL | GPIO 3 (Pin 5) | I2C Clock |
|
||||
| BAT | CR2032 Battery | Backup battery |
|
||||
|
||||
### INA219 Power Monitor (I2C) - Optional
|
||||
| INA219 Pin | Raspberry Pi Pin | Description |
|
||||
|------------|------------------|-------------|
|
||||
| VCC | 3.3V (Pin 1) | Power supply |
|
||||
| GND | GND (Pin 6) | Ground |
|
||||
| SDA | GPIO 2 (Pin 3) | I2C Data |
|
||||
| SCL | GPIO 3 (Pin 5) | I2C Clock |
|
||||
| VIN+ | Battery + | Positive voltage sense |
|
||||
| VIN- | Shunt resistor | Through shunt to load |
|
||||
|
||||
**Note**: Multiple I2C devices can share the same SDA/SCL pins. Ensure each has a unique I2C address.
|
||||
|
||||
### GPIO Buttons (Optional)
|
||||
|
||||
Physical buttons for navigation:
|
||||
|
||||
| Button Function | GPIO Pin | Connection |
|
||||
|----------------|----------|------------|
|
||||
| Previous Page | GPIO 22 | Button between GPIO 22 and GND |
|
||||
| Next Page | GPIO 27 | Button between GPIO 27 and GND |
|
||||
| Power Off | GPIO 21 | Button between GPIO 21 and GND |
|
||||
|
||||
**Wiring:**
|
||||
- Connect one side of button to GPIO pin
|
||||
- Connect other side to GND
|
||||
- Internal pull-up resistors are enabled in software
|
||||
- Button press pulls GPIO LOW (0V)
|
||||
|
||||
**Available GPIOs** (BCM numbering):
|
||||
- Safe to use: 5-27 (except 14, 15 if using UART)
|
||||
- Avoid: GPIO 2, 3 (I2C), GPIO 7-11 (SPI), GPIO 14, 15 (UART)
|
||||
|
||||
## Verify Hardware Connections
|
||||
|
||||
### Check I2C Devices
|
||||
|
||||
```bash
|
||||
# Scan I2C bus
|
||||
i2cdetect -y 1
|
||||
|
||||
# Expected addresses (approximate):
|
||||
# 0x38 - FT5316 touch panel
|
||||
# 0x14 - BMA400 accelerometer
|
||||
# 0x68 - PCF8523 RTC
|
||||
# 0x40 - INA219 power monitor
|
||||
```
|
||||
|
||||
### Check SPI
|
||||
|
||||
```bash
|
||||
# List SPI devices
|
||||
ls /dev/spi*
|
||||
# Should show: /dev/spidev0.0 /dev/spidev0.1
|
||||
```
|
||||
|
||||
## Important: VCOM Voltage
|
||||
|
||||
⚠️ **CRITICAL**: Each e-ink display has a unique VCOM voltage printed on a label (usually on the back).
|
||||
|
||||
- Check your display label for VCOM voltage (e.g., -2.06V, -1.98V, etc.)
|
||||
- Pass this value to the HAL using the `--vcom` parameter
|
||||
- Using incorrect VCOM can damage your display!
|
||||
|
||||
Example from label: `VCOM = -2.06V` → use `--vcom -2.06`
|
||||
|
||||
## Running on Hardware
|
||||
|
||||
### Recommended Method: Interactive Setup
|
||||
|
||||
The easiest way to get started is using the interactive setup script:
|
||||
|
||||
```bash
|
||||
# 1. Run setup (detects hardware, configures GPIO buttons, etc.)
|
||||
sudo python3 setup_rpi.py
|
||||
|
||||
# 2. Run DReader using generated config
|
||||
python examples/run_on_hardware_config.py
|
||||
```
|
||||
|
||||
The setup script will:
|
||||
- Detect connected I2C devices (touch, accelerometer, RTC, power monitor)
|
||||
- Enable I2C/SPI interfaces if needed
|
||||
- Configure GPIO button mappings
|
||||
- Set VCOM voltage
|
||||
- Generate hardware_config.json
|
||||
|
||||
### Manual Configuration
|
||||
|
||||
Edit `hardware_config.json` to customize your hardware setup:
|
||||
|
||||
```json
|
||||
{
|
||||
"display": {
|
||||
"width": 1872,
|
||||
"height": 1404,
|
||||
"vcom": -2.06
|
||||
},
|
||||
"gpio_buttons": {
|
||||
"enabled": true,
|
||||
"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"}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
```bash
|
||||
python examples/run_on_hardware_config.py
|
||||
```
|
||||
|
||||
### Direct Command Line (No Config File)
|
||||
|
||||
```bash
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Run with correct VCOM voltage (CHECK YOUR DISPLAY LABEL!)
|
||||
python examples/run_on_hardware.py /path/to/books --vcom -2.06
|
||||
```
|
||||
|
||||
### Testing Without Hardware (Virtual Display)
|
||||
|
||||
You can test the integration on your development machine using virtual display mode:
|
||||
|
||||
```bash
|
||||
python examples/run_on_hardware.py /path/to/books \
|
||||
--virtual \
|
||||
--no-orientation \
|
||||
--no-rtc \
|
||||
--no-power
|
||||
```
|
||||
|
||||
This creates a Tkinter window simulating the e-ink display.
|
||||
|
||||
### Advanced Options
|
||||
|
||||
```bash
|
||||
# Disable optional hardware components
|
||||
python examples/run_on_hardware.py /path/to/books \
|
||||
--vcom -2.06 \
|
||||
--no-orientation \ # Disable accelerometer
|
||||
--no-rtc \ # Disable RTC
|
||||
--no-power # Disable battery monitor
|
||||
|
||||
# Show battery level periodically
|
||||
python examples/run_on_hardware.py /path/to/books \
|
||||
--vcom -2.06 \
|
||||
--show-battery
|
||||
|
||||
# Custom battery capacity
|
||||
python examples/run_on_hardware.py /path/to/books \
|
||||
--vcom -2.06 \
|
||||
--battery-capacity 5000 # mAh
|
||||
|
||||
# Enable verbose debug logging
|
||||
python examples/run_on_hardware.py /path/to/books \
|
||||
--vcom -2.06 \
|
||||
--verbose
|
||||
|
||||
# Always start in library mode (ignore saved state)
|
||||
python examples/run_on_hardware.py /path/to/books \
|
||||
--vcom -2.06 \
|
||||
--force-library
|
||||
```
|
||||
|
||||
### Full Options Reference
|
||||
|
||||
```bash
|
||||
python examples/run_on_hardware.py --help
|
||||
```
|
||||
|
||||
## Touch Gestures
|
||||
|
||||
Once running, the following touch gestures are supported:
|
||||
|
||||
| Gesture | 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** | Show word definition (if implemented) |
|
||||
| **Pinch In/Out** | Adjust font size |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Display Not Working
|
||||
|
||||
1. **Check VCOM voltage** - Must match label on display
|
||||
2. **Check SPI connections** - Run `ls /dev/spi*`
|
||||
3. **Check SPI permissions** - Add user to `spi` group: `sudo usermod -a -G spi $USER`
|
||||
4. **Try virtual display mode** - Test software without hardware
|
||||
|
||||
### Touch Not Working
|
||||
|
||||
1. **Check I2C connections** - Run `i2cdetect -y 1`
|
||||
2. **Check I2C permissions** - Add user to `i2c` group: `sudo usermod -a -G i2c $USER`
|
||||
3. **Check touch panel I2C address** - Should be 0x38 for FT5316
|
||||
4. **Calibrate touch** - See dreader-hal calibration docs
|
||||
|
||||
### Import Errors
|
||||
|
||||
If you see `ModuleNotFoundError` for drivers:
|
||||
|
||||
```bash
|
||||
# Ensure all external drivers are installed
|
||||
cd external/dreader-hal/external
|
||||
for dir in */; do pip install -e "$dir"; done
|
||||
cd ../../..
|
||||
```
|
||||
|
||||
### Permission Errors
|
||||
|
||||
```bash
|
||||
# Add user to required groups
|
||||
sudo usermod -a -G spi,i2c,gpio $USER
|
||||
|
||||
# Log out and back in for changes to take effect
|
||||
```
|
||||
|
||||
### Display Ghosting
|
||||
|
||||
E-ink displays can show ghosting (image retention). The HAL automatically performs full refreshes every 10 page turns, but you can force one:
|
||||
|
||||
- The `RefreshMode.FULL` is automatically triggered periodically
|
||||
- Check dreader-hal documentation for manual refresh control
|
||||
|
||||
## Programmatic Usage
|
||||
|
||||
You can also use the hardware HAL programmatically in your own scripts:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from dreader.hal_hardware import HardwareDisplayHAL
|
||||
from dreader.main import DReaderApplication, AppConfig
|
||||
|
||||
async def main():
|
||||
# Create hardware HAL
|
||||
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,
|
||||
)
|
||||
|
||||
# Create application config
|
||||
config = AppConfig(
|
||||
display_hal=hal,
|
||||
library_path="/home/pi/Books",
|
||||
page_size=(1872, 1404),
|
||||
)
|
||||
|
||||
# Create and run application
|
||||
app = DReaderApplication(config)
|
||||
|
||||
try:
|
||||
await hal.initialize()
|
||||
await app.start()
|
||||
|
||||
# Main event loop
|
||||
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())
|
||||
```
|
||||
|
||||
## Performance Notes
|
||||
|
||||
### E-ink Refresh Modes
|
||||
|
||||
The dreader-hal library automatically selects refresh modes:
|
||||
|
||||
- **Fast (DU mode)**: ~200ms - Used for text updates
|
||||
- **Quality (GC16 mode)**: ~1000ms - Used for images
|
||||
- **Full (INIT mode)**: ~1000ms - Used every 10 pages to clear ghosting
|
||||
|
||||
### Battery Life
|
||||
|
||||
With default settings:
|
||||
- Active reading: ~10-20 hours
|
||||
- Standby (display sleeping): ~1-2 weeks
|
||||
- Enable low power mode for extended battery life
|
||||
|
||||
### Memory Usage
|
||||
|
||||
- Base application: ~30-50MB
|
||||
- Per book: ~10-30MB (depends on book size)
|
||||
- Ensure Raspberry Pi has at least 512MB RAM
|
||||
|
||||
## Next Steps
|
||||
|
||||
- See [README.md](README.md) for application features
|
||||
- See [external/dreader-hal/README.md](external/dreader-hal/README.md) for HAL details
|
||||
- See [examples/](examples/) for more usage examples
|
||||
- Check dreader-hal documentation for touch calibration and advanced features
|
||||
|
||||
## Support
|
||||
|
||||
For hardware-specific issues, check:
|
||||
- [dreader-hal issues](https://gitea.tourolle.paris/dtourolle/dreader-hal/issues)
|
||||
|
||||
For application issues, check:
|
||||
- [dreader-application issues](https://gitea.tourolle.paris/dtourolle/dreader-application/issues)
|
||||
67
README.md
67
README.md
@ -471,35 +471,60 @@ pytest --cov=dreader --cov-report=term-missing
|
||||
|
||||
## Hardware Integration
|
||||
|
||||
DReader requires a Hardware Abstraction Layer (HAL) for display and input:
|
||||
DReader includes complete hardware support for e-ink displays via the **dreader-hal** library.
|
||||
|
||||
```python
|
||||
from abc import ABC, abstractmethod
|
||||
from PIL import Image
|
||||
### Supported Hardware
|
||||
|
||||
class DisplayHAL(ABC):
|
||||
"""Abstract display interface for platform integration"""
|
||||
- **Display**: IT8951 e-ink controller (1872×1404)
|
||||
- **Touch**: FT5316 capacitive touch panel
|
||||
- **Buttons**: GPIO buttons (configurable)
|
||||
- **Sensors**: BMA400 accelerometer, PCF8523 RTC, INA219 power monitor
|
||||
|
||||
@abstractmethod
|
||||
def show_image(self, image: Image.Image):
|
||||
"""Display a PIL Image on the screen"""
|
||||
### Quick Setup on Raspberry Pi
|
||||
|
||||
@abstractmethod
|
||||
def get_touch_events(self) -> Iterator[TouchEvent]:
|
||||
"""Get iterator of touch events from hardware"""
|
||||
```bash
|
||||
# 1. Clone and install
|
||||
git clone https://gitea.tourolle.paris/dtourolle/dreader-application.git
|
||||
cd dreader-application
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -e .
|
||||
./install_hardware_drivers.sh
|
||||
|
||||
@abstractmethod
|
||||
def set_brightness(self, level: int):
|
||||
"""Set display brightness (0-10)"""
|
||||
# 2. Interactive hardware setup
|
||||
sudo python3 setup_rpi.py
|
||||
|
||||
# 3. Run on hardware
|
||||
python examples/run_on_hardware_config.py
|
||||
```
|
||||
|
||||
**Example HAL Implementations:**
|
||||
- **E-ink Displays**: IT8951, Remarkable device SDK
|
||||
- **Desktop**: pygame, tkinter, Qt
|
||||
- **Web**: Flask + HTML canvas
|
||||
- **Embedded**: Device-specific framebuffer
|
||||
### Hardware Configuration
|
||||
|
||||
See [HAL_IMPLEMENTATION_SPEC.md](HAL_IMPLEMENTATION_SPEC.md) for detailed integration guidelines.
|
||||
The repository includes a pre-configured **[hardware_config.json](hardware_config.json)** for the reference hardware:
|
||||
|
||||
- **Buttons**: GPIO 22 (prev), GPIO 27 (next), GPIO 21 (power)
|
||||
- **Display**: 1872×1404 IT8951 e-ink
|
||||
- **I2C Bus**: GPIO 2/3 (touch, sensors, RTC, power)
|
||||
|
||||
See [HARDWARE_SETUP.md](HARDWARE_SETUP.md) for complete wiring diagrams and setup instructions.
|
||||
|
||||
### HAL Architecture
|
||||
|
||||
```python
|
||||
from dreader.hal_hardware import HardwareDisplayHAL
|
||||
from dreader.main import DReaderApplication, AppConfig
|
||||
|
||||
# Hardware HAL with GPIO buttons
|
||||
hal = HardwareDisplayHAL(width=1872, height=1404, vcom=-2.0)
|
||||
config = AppConfig(display_hal=hal, library_path="~/Books")
|
||||
app = DReaderApplication(config)
|
||||
```
|
||||
|
||||
**Available HAL Implementations:**
|
||||
- **HardwareDisplayHAL** - Real e-ink hardware (IT8951 + dreader-hal)
|
||||
- **PygameDisplayHAL** - Desktop testing with pygame window
|
||||
|
||||
See [HARDWARE_PINOUT.md](HARDWARE_PINOUT.md) for pin assignments and [GPIO_BUTTONS.md](GPIO_BUTTONS.md) for button configuration.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@ -25,6 +25,10 @@ class GestureType(Enum):
|
||||
DRAG_MOVE = "drag_move" # Continue dragging
|
||||
DRAG_END = "drag_end" # End dragging/selection
|
||||
|
||||
# Accelerometer-based gestures
|
||||
TILT_FORWARD = "tilt_forward" # Tilt device forward (page forward)
|
||||
TILT_BACKWARD = "tilt_backward" # Tilt device backward (page back)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TouchEvent:
|
||||
|
||||
291
dreader/gpio_buttons.py
Normal file
291
dreader/gpio_buttons.py
Normal file
@ -0,0 +1,291 @@
|
||||
"""
|
||||
GPIO Button Handler for DReader.
|
||||
|
||||
This module provides GPIO button support for physical buttons on the e-reader device.
|
||||
Buttons can be mapped to touch gestures for navigation and control.
|
||||
|
||||
Usage:
|
||||
from dreader.gpio_buttons import GPIOButtonHandler
|
||||
|
||||
buttons = GPIOButtonHandler(config)
|
||||
await buttons.initialize()
|
||||
|
||||
# Check for button events
|
||||
event = await buttons.get_button_event()
|
||||
if event:
|
||||
print(f"Button pressed: {event.gesture}")
|
||||
|
||||
await buttons.cleanup()
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional, Dict, List
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .gesture import TouchEvent, GestureType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Try to import RPi.GPIO
|
||||
try:
|
||||
import RPi.GPIO as GPIO
|
||||
GPIO_AVAILABLE = True
|
||||
except ImportError:
|
||||
GPIO_AVAILABLE = False
|
||||
logger.warning("RPi.GPIO not available. Button support disabled.")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ButtonConfig:
|
||||
"""Configuration for a single GPIO button."""
|
||||
name: str
|
||||
gpio: int
|
||||
gesture: GestureType
|
||||
description: str = ""
|
||||
|
||||
|
||||
class GPIOButtonHandler:
|
||||
"""
|
||||
Handler for GPIO buttons that generates touch events.
|
||||
|
||||
This class manages physical buttons connected to GPIO pins and converts
|
||||
button presses into TouchEvent objects that can be handled by the application.
|
||||
|
||||
Args:
|
||||
buttons: List of ButtonConfig objects defining button mappings
|
||||
pull_up: Use pull-up resistors (default True)
|
||||
bounce_time_ms: Debounce time in milliseconds (default 200)
|
||||
screen_width: Screen width for generating touch coordinates (default 1872)
|
||||
screen_height: Screen height for generating touch coordinates (default 1404)
|
||||
|
||||
Example:
|
||||
buttons_config = [
|
||||
ButtonConfig("next", 23, GestureType.SWIPE_LEFT, "Next page"),
|
||||
ButtonConfig("prev", 24, GestureType.SWIPE_RIGHT, "Previous page"),
|
||||
]
|
||||
|
||||
handler = GPIOButtonHandler(buttons_config)
|
||||
await handler.initialize()
|
||||
|
||||
# In main loop
|
||||
event = await handler.get_button_event()
|
||||
if event:
|
||||
await app.handle_touch(event)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
buttons: List[ButtonConfig],
|
||||
pull_up: bool = True,
|
||||
bounce_time_ms: int = 200,
|
||||
screen_width: int = 1872,
|
||||
screen_height: int = 1404,
|
||||
):
|
||||
"""Initialize GPIO button handler."""
|
||||
self.buttons = buttons
|
||||
self.pull_up = pull_up
|
||||
self.bounce_time_ms = bounce_time_ms
|
||||
self.screen_width = screen_width
|
||||
self.screen_height = screen_height
|
||||
|
||||
self._initialized = False
|
||||
self._event_queue: asyncio.Queue = asyncio.Queue()
|
||||
self._gpio_map: Dict[int, ButtonConfig] = {}
|
||||
|
||||
if not GPIO_AVAILABLE:
|
||||
logger.error("RPi.GPIO not available. Buttons will not work.")
|
||||
return
|
||||
|
||||
logger.info(f"GPIO button handler created with {len(buttons)} buttons")
|
||||
for btn in buttons:
|
||||
logger.info(f" Button '{btn.name}' on GPIO {btn.gpio} -> {btn.gesture.value}")
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize GPIO pins and set up button callbacks."""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
if not GPIO_AVAILABLE:
|
||||
logger.warning("Cannot initialize buttons: RPi.GPIO not available")
|
||||
return
|
||||
|
||||
logger.info("Initializing GPIO buttons...")
|
||||
|
||||
# Set GPIO mode
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
|
||||
# Configure each button
|
||||
for button in self.buttons:
|
||||
try:
|
||||
# Configure pin
|
||||
if self.pull_up:
|
||||
GPIO.setup(button.gpio, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
# Button pressed = LOW (0)
|
||||
edge = GPIO.FALLING
|
||||
else:
|
||||
GPIO.setup(button.gpio, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
|
||||
# Button pressed = HIGH (1)
|
||||
edge = GPIO.RISING
|
||||
|
||||
# Add event detection with debounce
|
||||
GPIO.add_event_detect(
|
||||
button.gpio,
|
||||
edge,
|
||||
callback=lambda channel, btn=button: self._button_callback(btn),
|
||||
bouncetime=self.bounce_time_ms
|
||||
)
|
||||
|
||||
self._gpio_map[button.gpio] = button
|
||||
logger.info(f"✓ Configured button '{button.name}' on GPIO {button.gpio}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to configure button '{button.name}' on GPIO {button.gpio}: {e}")
|
||||
|
||||
self._initialized = True
|
||||
logger.info("GPIO buttons initialized successfully")
|
||||
|
||||
def _button_callback(self, button: ButtonConfig):
|
||||
"""
|
||||
Callback function for button press (runs in GPIO event thread).
|
||||
|
||||
This is called by RPi.GPIO when a button is pressed. We put the event
|
||||
in a queue for async processing.
|
||||
"""
|
||||
logger.debug(f"Button pressed: {button.name} (GPIO {button.gpio})")
|
||||
|
||||
# Create touch event
|
||||
# Use center of screen for button events (x, y don't matter for swipes)
|
||||
event = TouchEvent(
|
||||
gesture=button.gesture,
|
||||
x=self.screen_width // 2,
|
||||
y=self.screen_height // 2,
|
||||
)
|
||||
|
||||
# Put in queue (non-blocking)
|
||||
try:
|
||||
self._event_queue.put_nowait(event)
|
||||
logger.info(f"Button event queued: {button.name} -> {button.gesture.value}")
|
||||
except asyncio.QueueFull:
|
||||
logger.warning("Button event queue full, dropping event")
|
||||
|
||||
async def get_button_event(self) -> Optional[TouchEvent]:
|
||||
"""
|
||||
Get the next button event from the queue.
|
||||
|
||||
Returns:
|
||||
TouchEvent if a button was pressed, None if no events
|
||||
"""
|
||||
if not self._initialized:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Non-blocking get
|
||||
event = self._event_queue.get_nowait()
|
||||
return event
|
||||
except asyncio.QueueEmpty:
|
||||
return None
|
||||
|
||||
async def cleanup(self):
|
||||
"""Clean up GPIO resources."""
|
||||
if not self._initialized:
|
||||
return
|
||||
|
||||
if not GPIO_AVAILABLE:
|
||||
return
|
||||
|
||||
logger.info("Cleaning up GPIO buttons...")
|
||||
|
||||
try:
|
||||
# Remove event detection for all buttons
|
||||
for button in self.buttons:
|
||||
try:
|
||||
GPIO.remove_event_detect(button.gpio)
|
||||
except Exception as e:
|
||||
logger.debug(f"Error removing event detect for GPIO {button.gpio}: {e}")
|
||||
|
||||
# Clean up GPIO
|
||||
GPIO.cleanup()
|
||||
logger.info("GPIO buttons cleaned up")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during GPIO cleanup: {e}")
|
||||
|
||||
self._initialized = False
|
||||
|
||||
|
||||
def load_button_config_from_dict(config: dict, screen_width: int = 1872, screen_height: int = 1404) -> Optional[GPIOButtonHandler]:
|
||||
"""
|
||||
Load GPIO button configuration from a dictionary.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary with 'gpio_buttons' section
|
||||
screen_width: Screen width for touch coordinates
|
||||
screen_height: Screen height for touch coordinates
|
||||
|
||||
Returns:
|
||||
GPIOButtonHandler instance if buttons enabled, None otherwise
|
||||
|
||||
Example config:
|
||||
{
|
||||
"gpio_buttons": {
|
||||
"enabled": true,
|
||||
"pull_up": true,
|
||||
"bounce_time_ms": 200,
|
||||
"buttons": [
|
||||
{
|
||||
"name": "next_page",
|
||||
"gpio": 23,
|
||||
"gesture": "swipe_left",
|
||||
"description": "Next page"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
gpio_config = config.get("gpio_buttons", {})
|
||||
|
||||
if not gpio_config.get("enabled", False):
|
||||
logger.info("GPIO buttons disabled in config")
|
||||
return None
|
||||
|
||||
if not GPIO_AVAILABLE:
|
||||
logger.warning("GPIO buttons enabled in config but RPi.GPIO not available")
|
||||
return None
|
||||
|
||||
# Parse button configurations
|
||||
buttons = []
|
||||
for btn_cfg in gpio_config.get("buttons", []):
|
||||
try:
|
||||
# Parse gesture type
|
||||
gesture_str = btn_cfg["gesture"]
|
||||
gesture = GestureType(gesture_str)
|
||||
|
||||
button = ButtonConfig(
|
||||
name=btn_cfg["name"],
|
||||
gpio=btn_cfg["gpio"],
|
||||
gesture=gesture,
|
||||
description=btn_cfg.get("description", "")
|
||||
)
|
||||
buttons.append(button)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing button config: {e}")
|
||||
logger.error(f" Config: {btn_cfg}")
|
||||
continue
|
||||
|
||||
if not buttons:
|
||||
logger.warning("No valid button configurations found")
|
||||
return None
|
||||
|
||||
# Create handler
|
||||
handler = GPIOButtonHandler(
|
||||
buttons=buttons,
|
||||
pull_up=gpio_config.get("pull_up", True),
|
||||
bounce_time_ms=gpio_config.get("bounce_time_ms", 200),
|
||||
screen_width=screen_width,
|
||||
screen_height=screen_height,
|
||||
)
|
||||
|
||||
return handler
|
||||
593
dreader/hal_hardware.py
Normal file
593
dreader/hal_hardware.py
Normal file
@ -0,0 +1,593 @@
|
||||
"""
|
||||
Hardware HAL implementation using dreader-hal library.
|
||||
|
||||
This module provides the HardwareDisplayHAL class that bridges the DReader
|
||||
application HAL interface with the dreader-hal hardware abstraction layer.
|
||||
|
||||
The dreader-hal library provides complete e-ink display integration with:
|
||||
- IT8951 e-ink display driver
|
||||
- FT5xx6 capacitive touch sensor
|
||||
- BMA400 accelerometer (orientation)
|
||||
- PCF8523 RTC (timekeeping)
|
||||
- INA219 power monitor (battery)
|
||||
|
||||
Usage:
|
||||
from dreader.hal_hardware import HardwareDisplayHAL
|
||||
from dreader.main import DReaderApplication, AppConfig
|
||||
|
||||
# For real hardware
|
||||
hal = HardwareDisplayHAL(width=800, height=1200, vcom=-2.0)
|
||||
|
||||
# For testing without hardware
|
||||
hal = HardwareDisplayHAL(
|
||||
width=800,
|
||||
height=1200,
|
||||
virtual_display=True,
|
||||
enable_orientation=False,
|
||||
enable_rtc=False,
|
||||
enable_power_monitor=False
|
||||
)
|
||||
|
||||
config = AppConfig(display_hal=hal, library_path="~/Books")
|
||||
app = DReaderApplication(config)
|
||||
|
||||
await hal.initialize()
|
||||
await app.start()
|
||||
|
||||
# Main loop
|
||||
while app.is_running():
|
||||
event = await hal.get_touch_event()
|
||||
if event:
|
||||
await app.handle_touch(event)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
await app.shutdown()
|
||||
await hal.cleanup()
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import json
|
||||
import math
|
||||
import time
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
|
||||
from .hal import DisplayHAL
|
||||
from .gesture import TouchEvent as AppTouchEvent, GestureType as AppGestureType
|
||||
|
||||
# Import dreader-hal components
|
||||
try:
|
||||
from dreader_hal import (
|
||||
EReaderDisplayHAL,
|
||||
TouchEvent as HalTouchEvent,
|
||||
GestureType as HalGestureType,
|
||||
RefreshMode,
|
||||
PowerStats,
|
||||
Orientation
|
||||
)
|
||||
DREADER_HAL_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
DREADER_HAL_AVAILABLE = False
|
||||
_import_error = e
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Gesture type mapping between dreader-hal and dreader-application
|
||||
GESTURE_TYPE_MAP = {
|
||||
HalGestureType.TAP: AppGestureType.TAP,
|
||||
HalGestureType.LONG_PRESS: AppGestureType.LONG_PRESS,
|
||||
HalGestureType.SWIPE_LEFT: AppGestureType.SWIPE_LEFT,
|
||||
HalGestureType.SWIPE_RIGHT: AppGestureType.SWIPE_RIGHT,
|
||||
HalGestureType.SWIPE_UP: AppGestureType.SWIPE_UP,
|
||||
HalGestureType.SWIPE_DOWN: AppGestureType.SWIPE_DOWN,
|
||||
HalGestureType.PINCH_IN: AppGestureType.PINCH_IN,
|
||||
HalGestureType.PINCH_OUT: AppGestureType.PINCH_OUT,
|
||||
HalGestureType.DRAG_START: AppGestureType.DRAG_START,
|
||||
HalGestureType.DRAG_MOVE: AppGestureType.DRAG_MOVE,
|
||||
HalGestureType.DRAG_END: AppGestureType.DRAG_END,
|
||||
}
|
||||
|
||||
|
||||
class HardwareDisplayHAL(DisplayHAL):
|
||||
"""
|
||||
Hardware HAL implementation using dreader-hal library.
|
||||
|
||||
This class adapts the dreader-hal EReaderDisplayHAL to work with the
|
||||
DReader application's DisplayHAL interface.
|
||||
|
||||
Args:
|
||||
width: Display width in pixels (default 1872)
|
||||
height: Display height in pixels (default 1404)
|
||||
vcom: E-ink VCOM voltage (default -2.0, check device label!)
|
||||
spi_hz: SPI clock frequency (default 24MHz)
|
||||
virtual_display: Use virtual display for testing (default False)
|
||||
auto_sleep_display: Auto-sleep display after updates (default True)
|
||||
enable_orientation: Enable orientation sensing (default True)
|
||||
enable_rtc: Enable RTC timekeeping (default True)
|
||||
enable_power_monitor: Enable battery monitoring (default True)
|
||||
shunt_ohms: Power monitor shunt resistor (default 0.1)
|
||||
battery_capacity_mah: Battery capacity in mAh (default 3000)
|
||||
|
||||
Example:
|
||||
# For real hardware (Raspberry Pi with e-ink display)
|
||||
hal = HardwareDisplayHAL(width=1872, height=1404, vcom=-2.0)
|
||||
|
||||
# For testing on development machine
|
||||
hal = HardwareDisplayHAL(
|
||||
width=1872,
|
||||
height=1404,
|
||||
virtual_display=True,
|
||||
enable_orientation=False,
|
||||
enable_rtc=False,
|
||||
enable_power_monitor=False
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
width: int = 1872,
|
||||
height: int = 1404,
|
||||
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,
|
||||
):
|
||||
"""
|
||||
Initialize hardware HAL.
|
||||
|
||||
Raises:
|
||||
ImportError: If dreader-hal library is not installed
|
||||
"""
|
||||
if not DREADER_HAL_AVAILABLE:
|
||||
raise ImportError(
|
||||
f"dreader-hal library is required for HardwareDisplayHAL.\n"
|
||||
f"Install with: pip install -e external/dreader-hal\n"
|
||||
f"Original error: {_import_error}"
|
||||
)
|
||||
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
logger.info(f"Initializing HardwareDisplayHAL: {width}x{height}")
|
||||
logger.info(f" VCOM: {vcom}V")
|
||||
logger.info(f" Virtual display: {virtual_display}")
|
||||
logger.info(f" Orientation: {enable_orientation}")
|
||||
logger.info(f" RTC: {enable_rtc}")
|
||||
logger.info(f" Power monitor: {enable_power_monitor}")
|
||||
|
||||
# Create the underlying dreader-hal implementation
|
||||
self.hal = EReaderDisplayHAL(
|
||||
width=width,
|
||||
height=height,
|
||||
vcom=vcom,
|
||||
spi_hz=spi_hz,
|
||||
virtual_display=virtual_display,
|
||||
auto_sleep_display=auto_sleep_display,
|
||||
enable_orientation=enable_orientation,
|
||||
enable_rtc=enable_rtc,
|
||||
enable_power_monitor=enable_power_monitor,
|
||||
shunt_ohms=shunt_ohms,
|
||||
battery_capacity_mah=battery_capacity_mah,
|
||||
)
|
||||
|
||||
self._initialized = False
|
||||
|
||||
async def initialize(self):
|
||||
"""
|
||||
Initialize all hardware components.
|
||||
|
||||
This initializes:
|
||||
- E-ink display controller
|
||||
- Touch sensor
|
||||
- Accelerometer (if enabled)
|
||||
- RTC (if enabled)
|
||||
- Power monitor (if enabled)
|
||||
"""
|
||||
if self._initialized:
|
||||
logger.warning("Hardware HAL already initialized")
|
||||
return
|
||||
|
||||
logger.info("Initializing hardware components...")
|
||||
await self.hal.initialize()
|
||||
self._initialized = True
|
||||
logger.info("Hardware HAL initialized successfully")
|
||||
|
||||
async def cleanup(self):
|
||||
"""Clean up all hardware resources."""
|
||||
if not self._initialized:
|
||||
return
|
||||
|
||||
logger.info("Cleaning up hardware HAL")
|
||||
await self.hal.cleanup()
|
||||
self._initialized = False
|
||||
logger.info("Hardware HAL cleaned up")
|
||||
|
||||
async def show_image(self, image: Image.Image):
|
||||
"""
|
||||
Display a PIL Image on the e-ink screen.
|
||||
|
||||
Args:
|
||||
image: PIL Image to display
|
||||
|
||||
The dreader-hal library handles:
|
||||
- Format conversion (RGB -> grayscale)
|
||||
- Dithering for e-ink
|
||||
- Refresh mode selection (auto, fast, quality, full)
|
||||
- Orientation rotation (if enabled)
|
||||
"""
|
||||
if not self._initialized:
|
||||
logger.warning("Hardware HAL not initialized, initializing now...")
|
||||
await self.initialize()
|
||||
|
||||
logger.debug(f"Displaying image: {image.size} {image.mode}")
|
||||
await self.hal.show_image(image)
|
||||
|
||||
async def get_touch_event(self) -> Optional[AppTouchEvent]:
|
||||
"""
|
||||
Get the next touch event from hardware.
|
||||
|
||||
Returns:
|
||||
TouchEvent if available, None if no event
|
||||
|
||||
The dreader-hal library handles gesture classification:
|
||||
- TAP: Quick tap (< 30px movement, < 300ms)
|
||||
- LONG_PRESS: Hold (< 30px movement, >= 500ms)
|
||||
- SWIPE_*: Directional swipes (>= 30px movement)
|
||||
- PINCH_IN/OUT: Two-finger pinch gestures
|
||||
"""
|
||||
if not self._initialized:
|
||||
return None
|
||||
|
||||
# Get event from dreader-hal
|
||||
hal_event = await self.hal.get_touch_event()
|
||||
|
||||
if hal_event is None:
|
||||
return None
|
||||
|
||||
# Convert from dreader-hal TouchEvent to application TouchEvent
|
||||
app_gesture = GESTURE_TYPE_MAP.get(hal_event.gesture)
|
||||
|
||||
if app_gesture is None:
|
||||
logger.warning(f"Unknown gesture type from HAL: {hal_event.gesture}")
|
||||
return None
|
||||
|
||||
logger.debug(f"Touch event: {app_gesture.value} at ({hal_event.x}, {hal_event.y})")
|
||||
|
||||
return AppTouchEvent(
|
||||
gesture=app_gesture,
|
||||
x=hal_event.x,
|
||||
y=hal_event.y
|
||||
)
|
||||
|
||||
async def set_brightness(self, level: int):
|
||||
"""
|
||||
Set display brightness.
|
||||
|
||||
Args:
|
||||
level: Brightness level (0-10)
|
||||
|
||||
Note:
|
||||
Basic IT8951 e-ink displays don't have brightness control.
|
||||
This is a no-op unless frontlight hardware is connected.
|
||||
"""
|
||||
if not 0 <= level <= 10:
|
||||
raise ValueError("Brightness must be 0-10")
|
||||
|
||||
logger.debug(f"Setting brightness to {level}")
|
||||
await self.hal.set_brightness(level)
|
||||
|
||||
# ========== Extended Methods (Hardware-Specific Features) ==========
|
||||
|
||||
async def get_battery_level(self) -> float:
|
||||
"""
|
||||
Get battery percentage.
|
||||
|
||||
Returns:
|
||||
Battery level 0-100%, or 0.0 if power monitor unavailable
|
||||
"""
|
||||
if not self._initialized:
|
||||
return 0.0
|
||||
|
||||
return await self.hal.get_battery_level()
|
||||
|
||||
async def get_power_stats(self) -> PowerStats:
|
||||
"""
|
||||
Get detailed power statistics.
|
||||
|
||||
Returns:
|
||||
PowerStats with voltage, current, power, battery %, etc.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If power monitor not enabled
|
||||
"""
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Hardware HAL not initialized")
|
||||
|
||||
return await self.hal.get_power_stats()
|
||||
|
||||
async def is_low_battery(self, threshold: float = 20.0) -> bool:
|
||||
"""
|
||||
Check if battery is low.
|
||||
|
||||
Args:
|
||||
threshold: Battery percentage threshold (default 20%)
|
||||
|
||||
Returns:
|
||||
True if battery below threshold, False otherwise
|
||||
"""
|
||||
if not self._initialized:
|
||||
return False
|
||||
|
||||
return await self.hal.is_low_battery(threshold)
|
||||
|
||||
async def set_low_power_mode(self, enabled: bool):
|
||||
"""
|
||||
Enable/disable low power mode.
|
||||
|
||||
In low power mode:
|
||||
- Display goes to sleep
|
||||
- Touch polling rate reduced
|
||||
- Sensors put to low power
|
||||
|
||||
Args:
|
||||
enabled: True to enable low power mode
|
||||
"""
|
||||
if not self._initialized:
|
||||
return
|
||||
|
||||
logger.info(f"Setting low power mode: {enabled}")
|
||||
await self.hal.set_low_power_mode(enabled)
|
||||
|
||||
async def enable_orientation_monitoring(self):
|
||||
"""
|
||||
Start monitoring device orientation changes.
|
||||
|
||||
When orientation changes, display auto-rotates.
|
||||
"""
|
||||
if not self._initialized:
|
||||
return
|
||||
|
||||
logger.info("Enabling orientation monitoring")
|
||||
await self.hal.enable_orientation_monitoring()
|
||||
|
||||
async def disable_orientation_monitoring(self):
|
||||
"""Stop monitoring orientation changes."""
|
||||
if not self._initialized:
|
||||
return
|
||||
|
||||
logger.info("Disabling orientation monitoring")
|
||||
await self.hal.disable_orientation_monitoring()
|
||||
|
||||
@property
|
||||
def current_orientation(self) -> Optional[Orientation]:
|
||||
"""Get current device orientation."""
|
||||
if not self._initialized:
|
||||
return None
|
||||
|
||||
return self.hal.current_orientation
|
||||
|
||||
@property
|
||||
def refresh_count(self) -> int:
|
||||
"""Get number of display refreshes since initialization."""
|
||||
if not self._initialized:
|
||||
return 0
|
||||
|
||||
return self.hal.refresh_count
|
||||
|
||||
async def get_datetime(self):
|
||||
"""
|
||||
Get current date/time from RTC.
|
||||
|
||||
Returns:
|
||||
struct_time with current date and time, or None if RTC unavailable
|
||||
"""
|
||||
if not self._initialized:
|
||||
return None
|
||||
|
||||
return await self.hal.get_datetime()
|
||||
|
||||
async def set_datetime(self, dt):
|
||||
"""
|
||||
Set the RTC date/time.
|
||||
|
||||
Args:
|
||||
dt: time.struct_time object with date and time to set
|
||||
|
||||
Raises:
|
||||
RuntimeError: If RTC not enabled
|
||||
"""
|
||||
if not self._initialized:
|
||||
raise RuntimeError("Hardware HAL not initialized")
|
||||
|
||||
await self.hal.set_datetime(dt)
|
||||
|
||||
# ========== Accelerometer Tilt Detection ==========
|
||||
|
||||
def load_accelerometer_calibration(self, config_path: str = "accelerometer_config.json") -> bool:
|
||||
"""
|
||||
Load accelerometer calibration from file.
|
||||
|
||||
Args:
|
||||
config_path: Path to calibration JSON file
|
||||
|
||||
Returns:
|
||||
True if calibration loaded successfully, False otherwise
|
||||
"""
|
||||
config_file = Path(config_path)
|
||||
if not config_file.exists():
|
||||
logger.warning(f"Accelerometer calibration file not found: {config_path}")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# Load up vector
|
||||
up = config.get("up_vector", {})
|
||||
self.accel_up_vector = (up.get("x", 0), up.get("y", 0), up.get("z", 0))
|
||||
|
||||
# Load thresholds
|
||||
self.accel_tilt_threshold = config.get("tilt_threshold", 0.3)
|
||||
self.accel_debounce_time = config.get("debounce_time", 0.5)
|
||||
|
||||
# State tracking
|
||||
self.accel_last_tilt_time = 0
|
||||
|
||||
logger.info(f"Accelerometer calibration loaded: up_vector={self.accel_up_vector}")
|
||||
logger.info(f" Tilt threshold: {self.accel_tilt_threshold:.2f} rad (~{math.degrees(self.accel_tilt_threshold):.1f}°)")
|
||||
logger.info(f" Debounce time: {self.accel_debounce_time:.2f}s")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading accelerometer calibration: {e}")
|
||||
return False
|
||||
|
||||
async def get_tilt_gesture(self) -> Optional[AppTouchEvent]:
|
||||
"""
|
||||
Check accelerometer for tilt gestures.
|
||||
|
||||
Returns:
|
||||
TouchEvent with TILT_FORWARD or TILT_BACKWARD gesture if detected,
|
||||
None otherwise
|
||||
|
||||
Note:
|
||||
Requires accelerometer calibration to be loaded first via
|
||||
load_accelerometer_calibration()
|
||||
"""
|
||||
if not self._initialized:
|
||||
return None
|
||||
|
||||
if not self.hal.orientation:
|
||||
return None
|
||||
|
||||
if not hasattr(self, 'accel_up_vector'):
|
||||
return None
|
||||
|
||||
# Get current acceleration
|
||||
try:
|
||||
ax, ay, az = await self.hal.orientation.get_acceleration()
|
||||
except Exception as e:
|
||||
logger.debug(f"Error reading accelerometer: {e}")
|
||||
return None
|
||||
|
||||
# Check debounce
|
||||
current_time = time.time()
|
||||
if current_time - self.accel_last_tilt_time < self.accel_debounce_time:
|
||||
return None
|
||||
|
||||
# Calculate angle between current gravity and calibrated "up" vector
|
||||
# Gravity vector is the acceleration (pointing down)
|
||||
gx, gy, gz = ax, ay, az
|
||||
|
||||
# Normalize gravity
|
||||
g_mag = math.sqrt(gx**2 + gy**2 + gz**2)
|
||||
if g_mag < 0.1:
|
||||
return None
|
||||
gx, gy, gz = gx / g_mag, gy / g_mag, gz / g_mag
|
||||
|
||||
# Normalize up vector
|
||||
ux, uy, uz = self.accel_up_vector
|
||||
u_mag = math.sqrt(ux**2 + uy**2 + uz**2)
|
||||
if u_mag < 0.1:
|
||||
return None
|
||||
ux, uy, uz = ux / u_mag, uy / u_mag, uz / u_mag
|
||||
|
||||
# Calculate tilt: project gravity onto the "forward/backward" axis
|
||||
# Forward/backward axis is perpendicular to up vector
|
||||
# We'll use the component of gravity that's perpendicular to the up vector
|
||||
|
||||
# Dot product: component of gravity along up vector
|
||||
dot_up = gx * ux + gy * uy + gz * uz
|
||||
|
||||
# Component of gravity perpendicular to up vector
|
||||
perp_x = gx - dot_up * ux
|
||||
perp_y = gy - dot_up * uy
|
||||
perp_z = gz - dot_up * uz
|
||||
|
||||
perp_mag = math.sqrt(perp_x**2 + perp_y**2 + perp_z**2)
|
||||
|
||||
# Angle from vertical (in radians)
|
||||
tilt_angle = math.atan2(perp_mag, abs(dot_up))
|
||||
|
||||
logger.debug(f"Tilt angle: {math.degrees(tilt_angle):.1f}° (threshold: {math.degrees(self.accel_tilt_threshold):.1f}°)")
|
||||
|
||||
# Check if tilted beyond threshold
|
||||
if tilt_angle < self.accel_tilt_threshold:
|
||||
return None
|
||||
|
||||
# Determine direction: forward or backward
|
||||
# We need to determine which direction the device is tilted
|
||||
# Use the sign of the perpendicular component along a reference axis
|
||||
|
||||
# For simplicity, we'll use the projection onto the original up vector's
|
||||
# perpendicular plane. If we tilt "forward", the gravity vector should
|
||||
# rotate in a specific direction.
|
||||
|
||||
# Calculate which direction: check if tilting away from or toward the up vector
|
||||
# If dot_up is decreasing (device tilting away from up), that's "forward"
|
||||
# If dot_up is increasing (device tilting back toward up), that's "backward"
|
||||
|
||||
# Actually, a simpler approach: check the direction of the perpendicular component
|
||||
# relative to a reference direction in the plane
|
||||
|
||||
# Let's define forward as tilting in the direction that increases the
|
||||
# y-component of acceleration (assuming standard orientation)
|
||||
# This is device-specific and may need adjustment
|
||||
|
||||
# For now, use a simple heuristic: forward = positive perpendicular y component
|
||||
if perp_y > 0:
|
||||
gesture = AppGestureType.TILT_FORWARD
|
||||
else:
|
||||
gesture = AppGestureType.TILT_BACKWARD
|
||||
|
||||
# Update debounce timer
|
||||
self.accel_last_tilt_time = current_time
|
||||
|
||||
logger.info(f"Tilt gesture detected: {gesture.value} (angle: {math.degrees(tilt_angle):.1f}°)")
|
||||
|
||||
# Return gesture at center of screen (x, y don't matter for tilt)
|
||||
return AppTouchEvent(
|
||||
gesture=gesture,
|
||||
x=self.width // 2,
|
||||
y=self.height // 2,
|
||||
timestamp_ms=current_time * 1000
|
||||
)
|
||||
|
||||
async def get_event(self) -> Optional[AppTouchEvent]:
|
||||
"""
|
||||
Get the next event from any input source (touch or accelerometer).
|
||||
|
||||
This is a convenience method that polls both touch and accelerometer
|
||||
in a single call, prioritizing touch events over tilt events.
|
||||
|
||||
Returns:
|
||||
TouchEvent from either touch sensor or accelerometer, or None if no event
|
||||
|
||||
Usage:
|
||||
while running:
|
||||
event = await hal.get_event()
|
||||
if event:
|
||||
handle_gesture(event)
|
||||
await asyncio.sleep(0.01)
|
||||
"""
|
||||
# Check touch first (higher priority)
|
||||
touch_event = await self.get_touch_event()
|
||||
if touch_event:
|
||||
return touch_event
|
||||
|
||||
# Check accelerometer tilt
|
||||
if hasattr(self, 'accel_up_vector'):
|
||||
tilt_event = await self.get_tilt_gesture()
|
||||
if tilt_event:
|
||||
return tilt_event
|
||||
|
||||
return None
|
||||
@ -81,6 +81,10 @@ class GestureRouter:
|
||||
return self._handle_selection_move(event.x, event.y)
|
||||
elif event.gesture == GestureType.DRAG_END:
|
||||
return self._handle_selection_end(event.x, event.y)
|
||||
elif event.gesture == GestureType.TILT_FORWARD:
|
||||
return self._handle_page_forward()
|
||||
elif event.gesture == GestureType.TILT_BACKWARD:
|
||||
return self._handle_page_back()
|
||||
|
||||
return GestureResponse(ActionType.NONE, {})
|
||||
|
||||
|
||||
363
examples/calibrate_accelerometer.py
Executable file
363
examples/calibrate_accelerometer.py
Executable file
@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Accelerometer Calibration Script
|
||||
|
||||
This script helps calibrate the accelerometer for gravity-based page flipping.
|
||||
It displays visual instructions on the e-ink display to guide the user through
|
||||
aligning the device with the "up" direction.
|
||||
|
||||
The calibration process:
|
||||
1. Shows an arrow pointing up
|
||||
2. User rotates device until arrow aligns with desired "up" direction
|
||||
3. User confirms by tapping screen
|
||||
4. Script saves calibration offset to config file
|
||||
|
||||
Usage:
|
||||
python examples/calibrate_accelerometer.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from dreader.hal_hardware import HardwareDisplayHAL
|
||||
from dreader.gesture import GestureType
|
||||
|
||||
|
||||
class AccelerometerCalibrator:
|
||||
"""Interactive accelerometer calibration tool"""
|
||||
|
||||
def __init__(self, hal: HardwareDisplayHAL, config_path: str = "accelerometer_config.json"):
|
||||
self.hal = hal
|
||||
self.config_path = Path(config_path)
|
||||
self.width = hal.width
|
||||
self.height = hal.height
|
||||
self.calibrated = False
|
||||
|
||||
# Calibration data
|
||||
self.up_vector = None # (x, y, z) when device is in "up" position
|
||||
|
||||
async def run(self):
|
||||
"""Run the calibration process"""
|
||||
print("Starting accelerometer calibration...")
|
||||
print(f"Display: {self.width}x{self.height}")
|
||||
|
||||
await self.hal.initialize()
|
||||
|
||||
try:
|
||||
# Show welcome screen
|
||||
await self.show_welcome()
|
||||
await self.wait_for_tap()
|
||||
|
||||
# Calibration loop
|
||||
await self.calibration_loop()
|
||||
|
||||
# Show completion screen
|
||||
await self.show_completion()
|
||||
await asyncio.sleep(3)
|
||||
|
||||
finally:
|
||||
await self.hal.cleanup()
|
||||
|
||||
async def show_welcome(self):
|
||||
"""Display welcome/instruction screen"""
|
||||
img = Image.new('RGB', (self.width, self.height), color=(255, 255, 255))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Try to load a font, fall back to default
|
||||
try:
|
||||
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 48)
|
||||
body_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32)
|
||||
except:
|
||||
title_font = ImageFont.load_default()
|
||||
body_font = ImageFont.load_default()
|
||||
|
||||
# Title
|
||||
title = "Accelerometer Calibration"
|
||||
title_bbox = draw.textbbox((0, 0), title, font=title_font)
|
||||
title_width = title_bbox[2] - title_bbox[0]
|
||||
draw.text(((self.width - title_width) // 2, 100), title, fill=(0, 0, 0), font=title_font)
|
||||
|
||||
# Instructions
|
||||
instructions = [
|
||||
"This will calibrate the accelerometer",
|
||||
"for gravity-based page flipping.",
|
||||
"",
|
||||
"You will:",
|
||||
"1. See an arrow on screen",
|
||||
"2. Rotate device until arrow points UP",
|
||||
"3. Tap screen to confirm",
|
||||
"",
|
||||
"Tap anywhere to begin..."
|
||||
]
|
||||
|
||||
y = 250
|
||||
for line in instructions:
|
||||
line_bbox = draw.textbbox((0, 0), line, font=body_font)
|
||||
line_width = line_bbox[2] - line_bbox[0]
|
||||
draw.text(((self.width - line_width) // 2, y), line, fill=(0, 0, 0), font=body_font)
|
||||
y += 50
|
||||
|
||||
await self.hal.show_image(img)
|
||||
|
||||
async def calibration_loop(self):
|
||||
"""Main calibration loop - show live arrow and accelerometer reading"""
|
||||
print("\nCalibration mode:")
|
||||
print("Rotate device until arrow points UP, then tap screen.")
|
||||
|
||||
last_display_time = 0
|
||||
display_interval = 0.2 # Update display every 200ms
|
||||
|
||||
while not self.calibrated:
|
||||
# Get current acceleration
|
||||
x, y, z = await self.hal.hal.orientation.get_acceleration()
|
||||
|
||||
# Update display if enough time has passed
|
||||
current_time = asyncio.get_event_loop().time()
|
||||
if current_time - last_display_time >= display_interval:
|
||||
await self.show_calibration_screen(x, y, z)
|
||||
last_display_time = current_time
|
||||
|
||||
# Check for touch event
|
||||
event = await self.hal.get_touch_event()
|
||||
if event and event.gesture == GestureType.TAP:
|
||||
# Save current orientation as "up"
|
||||
self.up_vector = (x, y, z)
|
||||
self.calibrated = True
|
||||
print(f"\nCalibration saved: up_vector = ({x:.2f}, {y:.2f}, {z:.2f})")
|
||||
break
|
||||
|
||||
await asyncio.sleep(0.05) # Poll at ~20Hz
|
||||
|
||||
async def show_calibration_screen(self, ax: float, ay: float, az: float):
|
||||
"""
|
||||
Show arrow pointing in direction of gravity
|
||||
|
||||
Args:
|
||||
ax, ay, az: Acceleration components in m/s²
|
||||
"""
|
||||
img = Image.new('RGB', (self.width, self.height), color=(255, 255, 255))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Calculate gravity direction (normalized)
|
||||
magnitude = math.sqrt(ax**2 + ay**2 + az**2)
|
||||
if magnitude < 0.1: # Avoid division by zero
|
||||
magnitude = 1.0
|
||||
|
||||
gx = ax / magnitude
|
||||
gy = ay / magnitude
|
||||
gz = az / magnitude
|
||||
|
||||
# Project gravity onto screen plane (assuming z is out of screen)
|
||||
# We want to show which way is "down" on the device
|
||||
# Arrow should point opposite to gravity (toward "up")
|
||||
arrow_dx = -gx
|
||||
arrow_dy = -gy
|
||||
|
||||
# Normalize for display
|
||||
arrow_length = min(self.width, self.height) * 0.3
|
||||
arrow_magnitude = math.sqrt(arrow_dx**2 + arrow_dy**2)
|
||||
if arrow_magnitude < 0.1:
|
||||
arrow_magnitude = 1.0
|
||||
|
||||
arrow_dx = (arrow_dx / arrow_magnitude) * arrow_length
|
||||
arrow_dy = (arrow_dy / arrow_magnitude) * arrow_length
|
||||
|
||||
# Center point
|
||||
cx = self.width // 2
|
||||
cy = self.height // 2
|
||||
|
||||
# Arrow endpoint
|
||||
end_x = cx + int(arrow_dx)
|
||||
end_y = cy + int(arrow_dy)
|
||||
|
||||
# Draw large arrow
|
||||
self.draw_arrow(draw, cx, cy, end_x, end_y, width=10)
|
||||
|
||||
# Draw circle at center
|
||||
circle_radius = 30
|
||||
draw.ellipse(
|
||||
[(cx - circle_radius, cy - circle_radius),
|
||||
(cx + circle_radius, cy + circle_radius)],
|
||||
outline=(0, 0, 0),
|
||||
width=5
|
||||
)
|
||||
|
||||
# Draw text with acceleration values
|
||||
try:
|
||||
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 28)
|
||||
except:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
text = f"X: {ax:6.2f} m/s²"
|
||||
draw.text((50, 50), text, fill=(0, 0, 0), font=font)
|
||||
|
||||
text = f"Y: {ay:6.2f} m/s²"
|
||||
draw.text((50, 100), text, fill=(0, 0, 0), font=font)
|
||||
|
||||
text = f"Z: {az:6.2f} m/s²"
|
||||
draw.text((50, 150), text, fill=(0, 0, 0), font=font)
|
||||
|
||||
text = "Rotate device until arrow points UP"
|
||||
text_bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = text_bbox[2] - text_bbox[0]
|
||||
draw.text(((self.width - text_width) // 2, self.height - 150),
|
||||
text, fill=(0, 0, 0), font=font)
|
||||
|
||||
text = "Then TAP screen to save"
|
||||
text_bbox = draw.textbbox((0, 0), text, font=font)
|
||||
text_width = text_bbox[2] - text_bbox[0]
|
||||
draw.text(((self.width - text_width) // 2, self.height - 100),
|
||||
text, fill=(0, 0, 0), font=font)
|
||||
|
||||
await self.hal.show_image(img)
|
||||
|
||||
def draw_arrow(self, draw: ImageDraw.Draw, x1: int, y1: int, x2: int, y2: int, width: int = 5):
|
||||
"""Draw an arrow from (x1, y1) to (x2, y2)"""
|
||||
# Main line
|
||||
draw.line([(x1, y1), (x2, y2)], fill=(0, 0, 0), width=width)
|
||||
|
||||
# Arrow head
|
||||
dx = x2 - x1
|
||||
dy = y2 - y1
|
||||
length = math.sqrt(dx**2 + dy**2)
|
||||
|
||||
if length < 0.1:
|
||||
return
|
||||
|
||||
# Normalize
|
||||
dx /= length
|
||||
dy /= length
|
||||
|
||||
# Arrow head size
|
||||
head_length = 40
|
||||
head_width = 30
|
||||
|
||||
# Perpendicular vector
|
||||
px = -dy
|
||||
py = dx
|
||||
|
||||
# Arrow head points
|
||||
p1_x = x2 - dx * head_length + px * head_width
|
||||
p1_y = y2 - dy * head_length + py * head_width
|
||||
|
||||
p2_x = x2 - dx * head_length - px * head_width
|
||||
p2_y = y2 - dy * head_length - py * head_width
|
||||
|
||||
# Draw arrow head
|
||||
draw.polygon([(x2, y2), (p1_x, p1_y), (p2_x, p2_y)], fill=(0, 0, 0))
|
||||
|
||||
async def show_completion(self):
|
||||
"""Show calibration complete screen"""
|
||||
img = Image.new('RGB', (self.width, self.height), color=(255, 255, 255))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
try:
|
||||
title_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 48)
|
||||
body_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 32)
|
||||
except:
|
||||
title_font = ImageFont.load_default()
|
||||
body_font = ImageFont.load_default()
|
||||
|
||||
# Title
|
||||
title = "Calibration Complete!"
|
||||
title_bbox = draw.textbbox((0, 0), title, font=title_font)
|
||||
title_width = title_bbox[2] - title_bbox[0]
|
||||
draw.text(((self.width - title_width) // 2, 200), title, fill=(0, 0, 0), font=title_font)
|
||||
|
||||
# Details
|
||||
if self.up_vector:
|
||||
x, y, z = self.up_vector
|
||||
details = [
|
||||
f"Up vector saved:",
|
||||
f"X: {x:.3f} m/s²",
|
||||
f"Y: {y:.3f} m/s²",
|
||||
f"Z: {z:.3f} m/s²",
|
||||
"",
|
||||
f"Saved to: {self.config_path}"
|
||||
]
|
||||
|
||||
y_pos = 350
|
||||
for line in details:
|
||||
line_bbox = draw.textbbox((0, 0), line, font=body_font)
|
||||
line_width = line_bbox[2] - line_bbox[0]
|
||||
draw.text(((self.width - line_width) // 2, y_pos), line, fill=(0, 0, 0), font=body_font)
|
||||
y_pos += 50
|
||||
|
||||
await self.hal.show_image(img)
|
||||
|
||||
# Save calibration to file
|
||||
self.save_calibration()
|
||||
|
||||
def save_calibration(self):
|
||||
"""Save calibration data to JSON file"""
|
||||
if not self.up_vector:
|
||||
print("Warning: No calibration data to save")
|
||||
return
|
||||
|
||||
x, y, z = self.up_vector
|
||||
|
||||
config = {
|
||||
"up_vector": {
|
||||
"x": x,
|
||||
"y": y,
|
||||
"z": z
|
||||
},
|
||||
"tilt_threshold": 0.3, # Radians (~17 degrees)
|
||||
"debounce_time": 0.5, # Seconds between tilt gestures
|
||||
}
|
||||
|
||||
with open(self.config_path, 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
print(f"Calibration saved to {self.config_path}")
|
||||
|
||||
async def wait_for_tap(self):
|
||||
"""Wait for user to tap screen"""
|
||||
while True:
|
||||
event = await self.hal.get_touch_event()
|
||||
if event and event.gesture == GestureType.TAP:
|
||||
break
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point"""
|
||||
# Create HAL with accelerometer enabled
|
||||
print("Initializing hardware...")
|
||||
hal = HardwareDisplayHAL(
|
||||
width=1872,
|
||||
height=1404,
|
||||
enable_orientation=True,
|
||||
enable_rtc=False,
|
||||
enable_power_monitor=False,
|
||||
virtual_display=False # Set to True for testing without hardware
|
||||
)
|
||||
|
||||
# Create calibrator
|
||||
calibrator = AccelerometerCalibrator(hal)
|
||||
|
||||
# Run calibration
|
||||
await calibrator.run()
|
||||
|
||||
print("\nCalibration complete!")
|
||||
print("You can now use accelerometer-based page flipping.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\nCalibration cancelled by user")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\nError during calibration: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
212
examples/demo_accelerometer_page_flip.py
Executable file
212
examples/demo_accelerometer_page_flip.py
Executable file
@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demo: Accelerometer-based Page Flipping
|
||||
|
||||
This example demonstrates how to use the accelerometer for hands-free
|
||||
page turning by tilting the device forward or backward.
|
||||
|
||||
Features:
|
||||
- Tilt device forward to advance to next page
|
||||
- Tilt device backward to go to previous page
|
||||
- Touch gestures still work normally
|
||||
- Configurable tilt threshold and debounce time
|
||||
|
||||
Prerequisites:
|
||||
1. Run calibration first: python examples/calibrate_accelerometer.py
|
||||
2. This creates accelerometer_config.json with calibration data
|
||||
|
||||
Usage:
|
||||
python examples/demo_accelerometer_page_flip.py <epub_file>
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from dreader.hal_hardware import HardwareDisplayHAL
|
||||
from dreader.application import EbookReader
|
||||
from dreader.gesture import GestureType
|
||||
|
||||
|
||||
class AccelerometerPageFlipDemo:
|
||||
"""Demo application with accelerometer-based page flipping"""
|
||||
|
||||
def __init__(self, epub_path: str):
|
||||
self.epub_path = epub_path
|
||||
|
||||
# Create HAL with accelerometer enabled
|
||||
print("Initializing hardware HAL...")
|
||||
self.hal = HardwareDisplayHAL(
|
||||
width=1872,
|
||||
height=1404,
|
||||
enable_orientation=True,
|
||||
enable_rtc=False,
|
||||
enable_power_monitor=False,
|
||||
virtual_display=False # Set to True for testing without hardware
|
||||
)
|
||||
|
||||
# Create reader
|
||||
print("Creating ebook reader...")
|
||||
self.reader = EbookReader(
|
||||
page_size=(self.hal.width, self.hal.height),
|
||||
margin=60
|
||||
)
|
||||
|
||||
self.running = False
|
||||
|
||||
async def run(self):
|
||||
"""Run the demo application"""
|
||||
print("\n" + "="*60)
|
||||
print("Accelerometer Page Flip Demo")
|
||||
print("="*60)
|
||||
|
||||
# Initialize HAL
|
||||
await self.hal.initialize()
|
||||
|
||||
# Load accelerometer calibration
|
||||
print("\nLoading accelerometer calibration...")
|
||||
calibrated = self.hal.load_accelerometer_calibration("accelerometer_config.json")
|
||||
|
||||
if not calibrated:
|
||||
print("\nWARNING: Accelerometer not calibrated!")
|
||||
print("Please run: python examples/calibrate_accelerometer.py")
|
||||
print("\nProceeding with touch gestures only...\n")
|
||||
else:
|
||||
print("Accelerometer calibration loaded successfully!")
|
||||
print(f" Up vector: {self.hal.accel_up_vector}")
|
||||
print(f" Tilt threshold: {self.hal.accel_tilt_threshold:.2f} rad")
|
||||
print(f" Debounce time: {self.hal.accel_debounce_time:.2f}s")
|
||||
|
||||
# Load EPUB
|
||||
print(f"\nLoading EPUB: {self.epub_path}")
|
||||
success = self.reader.load_epub(self.epub_path)
|
||||
|
||||
if not success:
|
||||
print(f"ERROR: Failed to load {self.epub_path}")
|
||||
await self.hal.cleanup()
|
||||
return
|
||||
|
||||
print(f"Loaded: {self.reader.book_title}")
|
||||
print(f"Author: {self.reader.book_author}")
|
||||
|
||||
# Display first page
|
||||
print("\nDisplaying first page...")
|
||||
img = self.reader.get_current_page()
|
||||
await self.hal.show_image(img)
|
||||
|
||||
# Instructions
|
||||
print("\n" + "="*60)
|
||||
print("Controls:")
|
||||
print(" - Tilt FORWARD to go to next page")
|
||||
print(" - Tilt BACKWARD to go to previous page")
|
||||
print(" - Swipe LEFT for next page (touch)")
|
||||
print(" - Swipe RIGHT for previous page (touch)")
|
||||
print(" - Long press to exit")
|
||||
print("="*60 + "\n")
|
||||
|
||||
# Main event loop
|
||||
self.running = True
|
||||
try:
|
||||
await self.event_loop()
|
||||
finally:
|
||||
await self.hal.cleanup()
|
||||
print("\nDemo finished!")
|
||||
|
||||
async def event_loop(self):
|
||||
"""Main event loop - poll for touch and accelerometer events"""
|
||||
accel_poll_interval = 0.05 # Check accelerometer every 50ms
|
||||
|
||||
while self.running:
|
||||
# Check for touch events
|
||||
touch_event = await self.hal.get_touch_event()
|
||||
if touch_event:
|
||||
await self.handle_event(touch_event)
|
||||
|
||||
# Check for accelerometer tilt events (if calibrated)
|
||||
if hasattr(self.hal, 'accel_up_vector'):
|
||||
tilt_event = await self.hal.get_tilt_gesture()
|
||||
if tilt_event:
|
||||
await self.handle_event(tilt_event)
|
||||
|
||||
# Small delay to avoid busy-waiting
|
||||
await asyncio.sleep(accel_poll_interval)
|
||||
|
||||
async def handle_event(self, event):
|
||||
"""Handle a gesture event (touch or accelerometer)"""
|
||||
gesture = event.gesture
|
||||
print(f"Gesture: {gesture.value}")
|
||||
|
||||
# Navigation gestures
|
||||
if gesture in [GestureType.SWIPE_LEFT, GestureType.TILT_FORWARD]:
|
||||
await self.next_page()
|
||||
|
||||
elif gesture in [GestureType.SWIPE_RIGHT, GestureType.TILT_BACKWARD]:
|
||||
await self.previous_page()
|
||||
|
||||
# Exit on long press
|
||||
elif gesture == GestureType.LONG_PRESS:
|
||||
print("\nLong press detected - exiting...")
|
||||
self.running = False
|
||||
|
||||
# Word tap
|
||||
elif gesture == GestureType.TAP:
|
||||
# You could implement word selection here
|
||||
print(f" Tap at ({event.x}, {event.y})")
|
||||
|
||||
async def next_page(self):
|
||||
"""Go to next page"""
|
||||
img = self.reader.next_page()
|
||||
if img:
|
||||
progress = self.reader.get_reading_progress()
|
||||
chapter = self.reader.get_current_chapter_info()
|
||||
print(f" -> Next page ({progress['percent']:.1f}% - {chapter['title']})")
|
||||
await self.hal.show_image(img)
|
||||
else:
|
||||
print(" -> At end of book")
|
||||
|
||||
async def previous_page(self):
|
||||
"""Go to previous page"""
|
||||
img = self.reader.previous_page()
|
||||
if img:
|
||||
progress = self.reader.get_reading_progress()
|
||||
chapter = self.reader.get_current_chapter_info()
|
||||
print(f" -> Previous page ({progress['percent']:.1f}% - {chapter['title']})")
|
||||
await self.hal.show_image(img)
|
||||
else:
|
||||
print(" -> At start of book")
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main entry point"""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python demo_accelerometer_page_flip.py <epub_file>")
|
||||
print("\nExample:")
|
||||
print(" python demo_accelerometer_page_flip.py ~/Books/mybook.epub")
|
||||
sys.exit(1)
|
||||
|
||||
epub_path = sys.argv[1]
|
||||
|
||||
# Check if file exists
|
||||
if not Path(epub_path).exists():
|
||||
print(f"ERROR: File not found: {epub_path}")
|
||||
sys.exit(1)
|
||||
|
||||
# Run demo
|
||||
demo = AccelerometerPageFlipDemo(epub_path)
|
||||
await demo.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\nDemo interrupted by user")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
118
examples/demo_accelerometer_simple.py
Executable file
118
examples/demo_accelerometer_simple.py
Executable file
@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple Accelerometer Demo - Using Unified Event API
|
||||
|
||||
This is a simplified version of the accelerometer demo that uses
|
||||
the HAL's get_event() convenience method to poll both touch and
|
||||
accelerometer in a single call.
|
||||
|
||||
Usage:
|
||||
python examples/demo_accelerometer_simple.py <epub_file>
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from dreader.hal_hardware import HardwareDisplayHAL
|
||||
from dreader.application import EbookReader
|
||||
from dreader.gesture import GestureType
|
||||
|
||||
|
||||
async def main():
|
||||
"""Simple demo using unified event API"""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python demo_accelerometer_simple.py <epub_file>")
|
||||
sys.exit(1)
|
||||
|
||||
epub_path = sys.argv[1]
|
||||
|
||||
# Create HAL with accelerometer enabled
|
||||
print("Initializing hardware...")
|
||||
hal = HardwareDisplayHAL(
|
||||
width=1872,
|
||||
height=1404,
|
||||
enable_orientation=True
|
||||
)
|
||||
|
||||
await hal.initialize()
|
||||
|
||||
# Load accelerometer calibration (optional)
|
||||
if hal.load_accelerometer_calibration("accelerometer_config.json"):
|
||||
print("✓ Accelerometer calibrated - tilt gestures enabled")
|
||||
else:
|
||||
print("✗ No accelerometer calibration - touch only")
|
||||
|
||||
# Create reader and load book
|
||||
print(f"\nLoading: {epub_path}")
|
||||
reader = EbookReader(page_size=(hal.width, hal.height), margin=60)
|
||||
|
||||
if not reader.load_epub(epub_path):
|
||||
print(f"ERROR: Failed to load {epub_path}")
|
||||
await hal.cleanup()
|
||||
return
|
||||
|
||||
print(f"Loaded: {reader.book_title}")
|
||||
|
||||
# Display first page
|
||||
img = reader.get_current_page()
|
||||
await hal.show_image(img)
|
||||
|
||||
print("\nControls:")
|
||||
print(" Swipe LEFT or Tilt FORWARD → Next page")
|
||||
print(" Swipe RIGHT or Tilt BACKWARD → Previous page")
|
||||
print(" Long press → Exit\n")
|
||||
|
||||
# Main event loop - simple unified API!
|
||||
running = True
|
||||
while running:
|
||||
# Get event from any source (touch or accelerometer)
|
||||
event = await hal.get_event()
|
||||
|
||||
if event:
|
||||
print(f"Gesture: {event.gesture.value}")
|
||||
|
||||
# Page navigation
|
||||
if event.gesture in [GestureType.SWIPE_LEFT, GestureType.TILT_FORWARD]:
|
||||
img = reader.next_page()
|
||||
if img:
|
||||
progress = reader.get_reading_progress()
|
||||
print(f" → Page {progress['current']}/{progress['total']} ({progress['percent']:.1f}%)")
|
||||
await hal.show_image(img)
|
||||
else:
|
||||
print(" → End of book")
|
||||
|
||||
elif event.gesture in [GestureType.SWIPE_RIGHT, GestureType.TILT_BACKWARD]:
|
||||
img = reader.previous_page()
|
||||
if img:
|
||||
progress = reader.get_reading_progress()
|
||||
print(f" ← Page {progress['current']}/{progress['total']} ({progress['percent']:.1f}%)")
|
||||
await hal.show_image(img)
|
||||
else:
|
||||
print(" ← Start of book")
|
||||
|
||||
# Exit
|
||||
elif event.gesture == GestureType.LONG_PRESS:
|
||||
print("\nExiting...")
|
||||
running = False
|
||||
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
await hal.cleanup()
|
||||
print("Done!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
292
examples/run_on_hardware.py
Executable file
292
examples/run_on_hardware.py
Executable file
@ -0,0 +1,292 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Run DReader on real e-ink hardware.
|
||||
|
||||
This example demonstrates running the DReader application on real e-ink hardware
|
||||
using the dreader-hal library for hardware abstraction.
|
||||
|
||||
Requirements:
|
||||
- Raspberry Pi (or compatible SBC)
|
||||
- IT8951 e-ink display
|
||||
- FT5xx6 capacitive touch sensor
|
||||
- Optional: BMA400 accelerometer, PCF8523 RTC, INA219 power monitor
|
||||
|
||||
Hardware Setup:
|
||||
See external/dreader-hal/README.md for wiring instructions
|
||||
|
||||
Usage:
|
||||
# On Raspberry Pi with full hardware
|
||||
python run_on_hardware.py /path/to/library
|
||||
|
||||
# For testing without hardware (virtual display mode)
|
||||
python run_on_hardware.py /path/to/library --virtual
|
||||
|
||||
# Disable optional components
|
||||
python run_on_hardware.py /path/to/library --no-orientation --no-rtc --no-power
|
||||
"""
|
||||
|
||||
import sys
|
||||
import asyncio
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path to import dreader
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from dreader.hal_hardware import HardwareDisplayHAL
|
||||
from dreader.main import DReaderApplication, AppConfig
|
||||
|
||||
|
||||
async def main(args):
|
||||
"""
|
||||
Main application entry point.
|
||||
|
||||
Args:
|
||||
args: Command line arguments
|
||||
"""
|
||||
# Set up logging
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if args.verbose else logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.info("Starting DReader on hardware")
|
||||
logger.info(f"Library path: {args.library_path}")
|
||||
logger.info(f"Display size: {args.width}x{args.height}")
|
||||
logger.info(f"VCOM: {args.vcom}V")
|
||||
logger.info(f"Virtual display: {args.virtual}")
|
||||
|
||||
# Create hardware HAL
|
||||
logger.info("Initializing hardware HAL...")
|
||||
hal = HardwareDisplayHAL(
|
||||
width=args.width,
|
||||
height=args.height,
|
||||
vcom=args.vcom,
|
||||
virtual_display=args.virtual,
|
||||
auto_sleep_display=args.auto_sleep,
|
||||
enable_orientation=args.orientation,
|
||||
enable_rtc=args.rtc,
|
||||
enable_power_monitor=args.power,
|
||||
battery_capacity_mah=args.battery_capacity,
|
||||
)
|
||||
|
||||
# Create application config
|
||||
config = AppConfig(
|
||||
display_hal=hal,
|
||||
library_path=args.library_path,
|
||||
page_size=(args.width, args.height),
|
||||
auto_save_interval=60,
|
||||
force_library_mode=args.force_library,
|
||||
log_level=logging.DEBUG if args.verbose else logging.INFO,
|
||||
)
|
||||
|
||||
# Create application
|
||||
app = DReaderApplication(config)
|
||||
|
||||
try:
|
||||
# Initialize hardware
|
||||
logger.info("Initializing hardware...")
|
||||
await hal.initialize()
|
||||
|
||||
# Start application
|
||||
logger.info("Starting application...")
|
||||
await app.start()
|
||||
|
||||
# Show battery level if available
|
||||
if args.power and not args.virtual:
|
||||
try:
|
||||
battery = await hal.get_battery_level()
|
||||
logger.info(f"Battery level: {battery:.1f}%")
|
||||
|
||||
if await hal.is_low_battery():
|
||||
logger.warning("⚠️ Low battery!")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not read battery: {e}")
|
||||
|
||||
# Main event loop
|
||||
logger.info("Entering main event loop (Ctrl+C to exit)")
|
||||
logger.info("")
|
||||
logger.info("Touch gestures:")
|
||||
logger.info(" - Swipe left: Next page")
|
||||
logger.info(" - Swipe right: Previous page")
|
||||
logger.info(" - Swipe up (from bottom): Open navigation/TOC")
|
||||
logger.info(" - Swipe down (from top): Open settings")
|
||||
logger.info(" - Tap: Select book/word/link")
|
||||
logger.info("")
|
||||
|
||||
while app.is_running():
|
||||
# Get touch event (non-blocking)
|
||||
event = await hal.get_touch_event()
|
||||
|
||||
if event:
|
||||
logger.debug(f"Touch event: {event.gesture.value} at ({event.x}, {event.y})")
|
||||
|
||||
# Handle touch event
|
||||
await app.handle_touch(event)
|
||||
|
||||
# Check battery periodically (every ~100 events)
|
||||
if args.power and not args.virtual and args.show_battery:
|
||||
if hasattr(app, '_event_count'):
|
||||
app._event_count += 1
|
||||
else:
|
||||
app._event_count = 1
|
||||
|
||||
if app._event_count % 100 == 0:
|
||||
battery = await hal.get_battery_level()
|
||||
logger.info(f"Battery: {battery:.1f}%")
|
||||
|
||||
# Small delay to prevent CPU spinning
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Received interrupt signal, shutting down...")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in main loop: {e}", exc_info=True)
|
||||
|
||||
finally:
|
||||
# Shutdown
|
||||
logger.info("Shutting down application...")
|
||||
await app.shutdown()
|
||||
|
||||
logger.info("Cleaning up hardware...")
|
||||
await hal.cleanup()
|
||||
|
||||
logger.info("DReader stopped")
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run DReader on e-ink hardware",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Run on real hardware
|
||||
%(prog)s /home/pi/Books
|
||||
|
||||
# Test with virtual display (no hardware required)
|
||||
%(prog)s /home/pi/Books --virtual
|
||||
|
||||
# Custom display size and VCOM
|
||||
%(prog)s /home/pi/Books --width 1200 --height 1600 --vcom -2.3
|
||||
|
||||
# Disable optional sensors
|
||||
%(prog)s /home/pi/Books --no-orientation --no-rtc --no-power
|
||||
"""
|
||||
)
|
||||
|
||||
# Required arguments
|
||||
parser.add_argument(
|
||||
'library_path',
|
||||
type=str,
|
||||
help='Path to directory containing EPUB files'
|
||||
)
|
||||
|
||||
# Display arguments
|
||||
parser.add_argument(
|
||||
'--width',
|
||||
type=int,
|
||||
default=1872,
|
||||
help='Display width in pixels (default: 1872)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--height',
|
||||
type=int,
|
||||
default=1404,
|
||||
help='Display height in pixels (default: 1404)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--vcom',
|
||||
type=float,
|
||||
default=-2.0,
|
||||
help='E-ink VCOM voltage - CHECK YOUR DISPLAY LABEL! (default: -2.0)'
|
||||
)
|
||||
|
||||
# Virtual display mode
|
||||
parser.add_argument(
|
||||
'--virtual',
|
||||
action='store_true',
|
||||
help='Use virtual display mode for testing without hardware'
|
||||
)
|
||||
|
||||
# Display features
|
||||
parser.add_argument(
|
||||
'--no-auto-sleep',
|
||||
dest='auto_sleep',
|
||||
action='store_false',
|
||||
help='Disable automatic display sleep after updates'
|
||||
)
|
||||
|
||||
# Optional hardware components
|
||||
parser.add_argument(
|
||||
'--no-orientation',
|
||||
dest='orientation',
|
||||
action='store_false',
|
||||
help='Disable orientation sensor (BMA400)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-rtc',
|
||||
dest='rtc',
|
||||
action='store_false',
|
||||
help='Disable RTC (PCF8523)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-power',
|
||||
dest='power',
|
||||
action='store_false',
|
||||
help='Disable power monitor (INA219)'
|
||||
)
|
||||
|
||||
# Battery monitoring
|
||||
parser.add_argument(
|
||||
'--battery-capacity',
|
||||
type=float,
|
||||
default=3000,
|
||||
help='Battery capacity in mAh (default: 3000)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--show-battery',
|
||||
action='store_true',
|
||||
help='Periodically log battery level'
|
||||
)
|
||||
|
||||
# Application behavior
|
||||
parser.add_argument(
|
||||
'--force-library',
|
||||
action='store_true',
|
||||
help='Always start in library mode (ignore saved state)'
|
||||
)
|
||||
|
||||
# Debugging
|
||||
parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
action='store_true',
|
||||
help='Enable verbose debug logging'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Validate library path
|
||||
library_path = Path(args.library_path).expanduser()
|
||||
if not library_path.exists():
|
||||
parser.error(f"Library path does not exist: {library_path}")
|
||||
if not library_path.is_dir():
|
||||
parser.error(f"Library path is not a directory: {library_path}")
|
||||
|
||||
args.library_path = str(library_path)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parse_args()
|
||||
|
||||
# Run async main
|
||||
try:
|
||||
asyncio.run(main(args))
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted by user")
|
||||
sys.exit(0)
|
||||
293
examples/run_on_hardware_config.py
Executable file
293
examples/run_on_hardware_config.py
Executable file
@ -0,0 +1,293 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Run DReader on hardware using hardware_config.json configuration.
|
||||
|
||||
This script loads all hardware configuration from hardware_config.json,
|
||||
including display settings, GPIO buttons, and optional components.
|
||||
|
||||
Usage:
|
||||
# Use default config file (hardware_config.json)
|
||||
python run_on_hardware_config.py
|
||||
|
||||
# Use custom config file
|
||||
python run_on_hardware_config.py --config my_config.json
|
||||
|
||||
# Override config settings
|
||||
python run_on_hardware_config.py --library ~/MyBooks --verbose
|
||||
"""
|
||||
|
||||
import sys
|
||||
import asyncio
|
||||
import argparse
|
||||
import logging
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# Add parent directory to path to import dreader
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from dreader.hal_hardware import HardwareDisplayHAL
|
||||
from dreader.main import DReaderApplication, AppConfig
|
||||
from dreader.gpio_buttons import load_button_config_from_dict
|
||||
|
||||
|
||||
def load_config(config_path: str) -> dict:
|
||||
"""Load hardware configuration from JSON file."""
|
||||
config_file = Path(config_path)
|
||||
|
||||
if not config_file.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Configuration file not found: {config_path}\n"
|
||||
f"Run 'sudo python3 setup_rpi.py' to create it."
|
||||
)
|
||||
|
||||
with open(config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
async def main(args):
|
||||
"""Main application entry point."""
|
||||
# Load configuration
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Loading configuration from {args.config}")
|
||||
|
||||
try:
|
||||
config = load_config(args.config)
|
||||
except Exception as e:
|
||||
print(f"Error loading configuration: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Apply command-line overrides
|
||||
if args.library:
|
||||
config['application']['library_path'] = args.library
|
||||
|
||||
if args.verbose:
|
||||
config['application']['log_level'] = 'DEBUG'
|
||||
|
||||
# Set up logging
|
||||
log_level = getattr(logging, config['application']['log_level'].upper())
|
||||
logging.basicConfig(
|
||||
level=log_level,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
logger.info("="*70)
|
||||
logger.info("DReader Hardware Mode")
|
||||
logger.info("="*70)
|
||||
|
||||
# Display configuration summary
|
||||
display_cfg = config['display']
|
||||
logger.info(f"Display: {display_cfg['width']}x{display_cfg['height']}, VCOM={display_cfg['vcom']}V")
|
||||
|
||||
gpio_cfg = config.get('gpio_buttons', {})
|
||||
if gpio_cfg.get('enabled', False):
|
||||
logger.info(f"GPIO Buttons: {len(gpio_cfg.get('buttons', []))} configured")
|
||||
|
||||
accel_cfg = config.get('accelerometer', {})
|
||||
if accel_cfg.get('enabled', False):
|
||||
logger.info("Accelerometer: Enabled")
|
||||
|
||||
rtc_cfg = config.get('rtc', {})
|
||||
if rtc_cfg.get('enabled', False):
|
||||
logger.info("RTC: Enabled")
|
||||
|
||||
power_cfg = config.get('power_monitor', {})
|
||||
if power_cfg.get('enabled', False):
|
||||
logger.info("Power Monitor: Enabled")
|
||||
|
||||
# Create hardware HAL
|
||||
logger.info("\nInitializing hardware HAL...")
|
||||
hal = HardwareDisplayHAL(
|
||||
width=display_cfg['width'],
|
||||
height=display_cfg['height'],
|
||||
vcom=display_cfg['vcom'],
|
||||
spi_hz=display_cfg.get('spi_hz', 24_000_000),
|
||||
virtual_display=False,
|
||||
auto_sleep_display=display_cfg.get('auto_sleep', True),
|
||||
enable_orientation=accel_cfg.get('enabled', True),
|
||||
enable_rtc=rtc_cfg.get('enabled', True),
|
||||
enable_power_monitor=power_cfg.get('enabled', True),
|
||||
shunt_ohms=power_cfg.get('shunt_ohms', 0.1),
|
||||
battery_capacity_mah=power_cfg.get('battery_capacity_mah', 3000),
|
||||
)
|
||||
|
||||
# Load accelerometer tilt calibration if enabled
|
||||
if accel_cfg.get('tilt_enabled', False):
|
||||
calib_file = accel_cfg.get('calibration_file', 'accelerometer_config.json')
|
||||
if hal.load_accelerometer_calibration(calib_file):
|
||||
logger.info(f"Accelerometer tilt detection enabled (calibration from {calib_file})")
|
||||
else:
|
||||
logger.warning("Accelerometer tilt detection requested but calibration not loaded")
|
||||
|
||||
# Set up GPIO buttons
|
||||
button_handler = None
|
||||
if gpio_cfg.get('enabled', False):
|
||||
logger.info("Setting up GPIO buttons...")
|
||||
button_handler = load_button_config_from_dict(
|
||||
config,
|
||||
screen_width=display_cfg['width'],
|
||||
screen_height=display_cfg['height']
|
||||
)
|
||||
|
||||
if button_handler:
|
||||
await button_handler.initialize()
|
||||
logger.info(f"GPIO buttons initialized: {len(gpio_cfg.get('buttons', []))} buttons")
|
||||
|
||||
# Create application config
|
||||
app_cfg = config['application']
|
||||
app_config = AppConfig(
|
||||
display_hal=hal,
|
||||
library_path=app_cfg['library_path'],
|
||||
page_size=(display_cfg['width'], display_cfg['height']),
|
||||
auto_save_interval=app_cfg.get('auto_save_interval', 60),
|
||||
force_library_mode=app_cfg.get('force_library_mode', False),
|
||||
log_level=log_level,
|
||||
)
|
||||
|
||||
# Create application
|
||||
app = DReaderApplication(app_config)
|
||||
|
||||
try:
|
||||
# Initialize hardware
|
||||
logger.info("Initializing hardware...")
|
||||
await hal.initialize()
|
||||
|
||||
# Start application
|
||||
logger.info("Starting application...")
|
||||
await app.start()
|
||||
|
||||
# Show battery level if available
|
||||
if power_cfg.get('enabled', False):
|
||||
try:
|
||||
battery = await hal.get_battery_level()
|
||||
logger.info(f"Battery level: {battery:.1f}%")
|
||||
|
||||
if await hal.is_low_battery(power_cfg.get('low_battery_threshold', 20.0)):
|
||||
logger.warning("⚠️ Low battery!")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not read battery: {e}")
|
||||
|
||||
# Main event loop
|
||||
logger.info("\nApplication ready!")
|
||||
logger.info("="*70)
|
||||
|
||||
event_count = 0
|
||||
show_battery_interval = power_cfg.get('show_battery_interval', 100)
|
||||
|
||||
while app.is_running():
|
||||
# Check for touch events
|
||||
touch_event = await hal.get_touch_event()
|
||||
|
||||
if touch_event:
|
||||
logger.debug(f"Touch: {touch_event.gesture.value} at ({touch_event.x}, {touch_event.y})")
|
||||
await app.handle_touch(touch_event)
|
||||
event_count += 1
|
||||
|
||||
# Check for button events
|
||||
if button_handler:
|
||||
button_event = await button_handler.get_button_event()
|
||||
if button_event:
|
||||
logger.info(f"Button: {button_event.gesture.value}")
|
||||
await app.handle_touch(button_event)
|
||||
event_count += 1
|
||||
|
||||
# Check for tilt gestures if enabled
|
||||
if accel_cfg.get('tilt_enabled', False):
|
||||
tilt_event = await hal.get_tilt_gesture()
|
||||
if tilt_event:
|
||||
logger.info(f"Tilt: {tilt_event.gesture.value}")
|
||||
await app.handle_touch(tilt_event)
|
||||
event_count += 1
|
||||
|
||||
# Show battery periodically
|
||||
if power_cfg.get('enabled', False) and event_count % show_battery_interval == 0 and event_count > 0:
|
||||
try:
|
||||
battery = await hal.get_battery_level()
|
||||
logger.info(f"Battery: {battery:.1f}%")
|
||||
except:
|
||||
pass
|
||||
|
||||
# Small delay to prevent CPU spinning
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("\nReceived interrupt signal, shutting down...")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in main loop: {e}", exc_info=True)
|
||||
|
||||
finally:
|
||||
# Shutdown
|
||||
logger.info("Shutting down application...")
|
||||
await app.shutdown()
|
||||
|
||||
logger.info("Cleaning up GPIO buttons...")
|
||||
if button_handler:
|
||||
await button_handler.cleanup()
|
||||
|
||||
logger.info("Cleaning up hardware...")
|
||||
await hal.cleanup()
|
||||
|
||||
logger.info("DReader stopped")
|
||||
|
||||
|
||||
def parse_args():
|
||||
"""Parse command line arguments."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run DReader using hardware_config.json",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Configuration:
|
||||
Edit hardware_config.json to configure your hardware settings.
|
||||
Run 'sudo python3 setup_rpi.py' to create/update the config file.
|
||||
|
||||
Examples:
|
||||
# Use default config
|
||||
%(prog)s
|
||||
|
||||
# Use custom config file
|
||||
%(prog)s --config my_hardware.json
|
||||
|
||||
# Override library path
|
||||
%(prog)s --library ~/MyBooks
|
||||
|
||||
# Enable verbose logging
|
||||
%(prog)s --verbose
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--config',
|
||||
type=str,
|
||||
default='hardware_config.json',
|
||||
help='Path to hardware configuration file (default: hardware_config.json)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--library',
|
||||
type=str,
|
||||
help='Override library path from config'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
action='store_true',
|
||||
help='Enable verbose debug logging'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
return args
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parse_args()
|
||||
|
||||
# Run async main
|
||||
try:
|
||||
asyncio.run(main(args))
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted by user")
|
||||
sys.exit(0)
|
||||
1
external/dreader-hal
vendored
Submodule
1
external/dreader-hal
vendored
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit be3aed6e5e2ef5c80235dcb3f8efa1aeb2ec5abe
|
||||
65
hardware_config.json
Normal file
65
hardware_config.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"_description": "Hardware configuration for DReader e-ink device",
|
||||
"_note": "This config matches the actual hardware: GPIO 22=prev, GPIO 27=next, GPIO 21=power, I2C on GPIO 2/3",
|
||||
|
||||
"display": {
|
||||
"width": 1872,
|
||||
"height": 1404,
|
||||
"vcom": -2.0,
|
||||
"spi_hz": 24000000,
|
||||
"auto_sleep": true
|
||||
},
|
||||
|
||||
"gpio_buttons": {
|
||||
"enabled": true,
|
||||
"pull_up": true,
|
||||
"bounce_time_ms": 200,
|
||||
"buttons": [
|
||||
{
|
||||
"name": "prev_page",
|
||||
"gpio": 22,
|
||||
"gesture": "swipe_right",
|
||||
"description": "Previous page button"
|
||||
},
|
||||
{
|
||||
"name": "next_page",
|
||||
"gpio": 27,
|
||||
"gesture": "swipe_left",
|
||||
"description": "Next page button"
|
||||
},
|
||||
{
|
||||
"name": "power_off",
|
||||
"gpio": 21,
|
||||
"gesture": "long_press",
|
||||
"description": "Power off button (long press to shutdown)",
|
||||
"comment": "You may want to implement shutdown logic in the application"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"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"
|
||||
}
|
||||
}
|
||||
80
install_hardware_drivers.sh
Executable file
80
install_hardware_drivers.sh
Executable file
@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
# Installation script for dreader-hal hardware drivers
|
||||
#
|
||||
# This script installs all the external driver dependencies needed
|
||||
# for running DReader on e-ink hardware.
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
echo "================================"
|
||||
echo "DReader Hardware Driver Installer"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
# Check if we're in a virtual environment
|
||||
if [ -z "$VIRTUAL_ENV" ]; then
|
||||
echo "⚠️ Warning: No virtual environment detected!"
|
||||
echo "It's recommended to activate your virtual environment first:"
|
||||
echo " source venv/bin/activate"
|
||||
echo ""
|
||||
read -p "Continue anyway? (y/N) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Initialize submodules if not already done
|
||||
echo "Step 1: Initializing git submodules..."
|
||||
git submodule update --init --recursive
|
||||
echo "✓ Submodules initialized"
|
||||
echo ""
|
||||
|
||||
# Install dreader-hal main package
|
||||
echo "Step 2: Installing dreader-hal..."
|
||||
pip install -e external/dreader-hal
|
||||
echo "✓ dreader-hal installed"
|
||||
echo ""
|
||||
|
||||
# Install external drivers
|
||||
echo "Step 3: Installing external driver libraries..."
|
||||
|
||||
echo " - Installing IT8951 (E-ink display driver)..."
|
||||
pip install -e external/dreader-hal/external/IT8951
|
||||
|
||||
echo " - Installing PyBMA400 (Accelerometer)..."
|
||||
pip install -e external/dreader-hal/external/PyBMA400
|
||||
|
||||
echo " - Installing PyFTtxx6 (Touch panel)..."
|
||||
pip install -e external/dreader-hal/external/PyFTtxx6/pyft5xx6
|
||||
|
||||
echo " - Installing PyPCF8523 (RTC)..."
|
||||
pip install -e external/dreader-hal/external/PyPCF8523
|
||||
|
||||
echo " - Installing pi_ina219 (Power monitor)..."
|
||||
pip install -e external/dreader-hal/external/pi_ina219
|
||||
|
||||
echo "✓ All drivers installed"
|
||||
echo ""
|
||||
|
||||
# Summary
|
||||
echo "================================"
|
||||
echo "Installation Complete!"
|
||||
echo "================================"
|
||||
echo ""
|
||||
echo "Installed packages:"
|
||||
echo " ✓ dreader-hal (main HAL library)"
|
||||
echo " ✓ IT8951 (e-ink display)"
|
||||
echo " ✓ PyBMA400 (accelerometer)"
|
||||
echo " ✓ PyFTtxx6 (touch panel)"
|
||||
echo " ✓ PyPCF8523 (RTC)"
|
||||
echo " ✓ pi_ina219 (power monitor)"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Wire up your hardware according to HARDWARE_SETUP.md"
|
||||
echo " 2. Check your display's VCOM voltage (on label)"
|
||||
echo " 3. Run: python examples/run_on_hardware.py /path/to/books --vcom YOUR_VCOM"
|
||||
echo ""
|
||||
echo "Example:"
|
||||
echo " python examples/run_on_hardware.py ~/Books --vcom -2.06"
|
||||
echo ""
|
||||
445
setup_rpi.py
Executable file
445
setup_rpi.py
Executable file
@ -0,0 +1,445 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Raspberry Pi Setup Script for DReader Hardware.
|
||||
|
||||
This interactive script helps configure your DReader e-reader hardware by:
|
||||
1. Detecting connected hardware (I2C devices, SPI, etc.)
|
||||
2. Creating/editing hardware_config.json
|
||||
3. Installing required system packages
|
||||
4. Setting up permissions and services
|
||||
|
||||
Usage:
|
||||
sudo python3 setup_rpi.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
# Check if running on Raspberry Pi
|
||||
try:
|
||||
with open('/proc/device-tree/model', 'r') as f:
|
||||
model = f.read()
|
||||
if 'Raspberry Pi' not in model:
|
||||
print("⚠️ Warning: This doesn't appear to be a Raspberry Pi")
|
||||
print(f" Detected: {model.strip()}")
|
||||
response = input("Continue anyway? (y/N): ")
|
||||
if response.lower() != 'y':
|
||||
sys.exit(1)
|
||||
except:
|
||||
print("⚠️ Warning: Could not detect Raspberry Pi")
|
||||
|
||||
|
||||
class Colors:
|
||||
"""ANSI color codes for terminal output."""
|
||||
HEADER = '\033[95m'
|
||||
BLUE = '\033[94m'
|
||||
CYAN = '\033[96m'
|
||||
GREEN = '\033[92m'
|
||||
YELLOW = '\033[93m'
|
||||
RED = '\033[91m'
|
||||
END = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
|
||||
|
||||
def print_header(text: str):
|
||||
"""Print a header."""
|
||||
print(f"\n{Colors.BOLD}{Colors.BLUE}{'='*70}{Colors.END}")
|
||||
print(f"{Colors.BOLD}{Colors.BLUE}{text:^70}{Colors.END}")
|
||||
print(f"{Colors.BOLD}{Colors.BLUE}{'='*70}{Colors.END}\n")
|
||||
|
||||
|
||||
def print_step(number: int, text: str):
|
||||
"""Print a step number."""
|
||||
print(f"\n{Colors.BOLD}{Colors.CYAN}Step {number}: {text}{Colors.END}")
|
||||
print(f"{Colors.CYAN}{'-'*70}{Colors.END}")
|
||||
|
||||
|
||||
def print_success(text: str):
|
||||
"""Print success message."""
|
||||
print(f"{Colors.GREEN}✓ {text}{Colors.END}")
|
||||
|
||||
|
||||
def print_warning(text: str):
|
||||
"""Print warning message."""
|
||||
print(f"{Colors.YELLOW}⚠ {text}{Colors.END}")
|
||||
|
||||
|
||||
def print_error(text: str):
|
||||
"""Print error message."""
|
||||
print(f"{Colors.RED}✗ {text}{Colors.END}")
|
||||
|
||||
|
||||
def run_command(cmd: str, check: bool = True) -> Tuple[int, str, str]:
|
||||
"""Run a shell command and return result."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=check
|
||||
)
|
||||
return result.returncode, result.stdout, result.stderr
|
||||
except subprocess.CalledProcessError as e:
|
||||
return e.returncode, e.stdout, e.stderr
|
||||
|
||||
|
||||
def check_interfaces() -> Dict[str, bool]:
|
||||
"""Check if I2C and SPI interfaces are enabled."""
|
||||
print("Checking system interfaces...")
|
||||
|
||||
interfaces = {
|
||||
'i2c': False,
|
||||
'spi': False,
|
||||
}
|
||||
|
||||
# Check I2C
|
||||
if os.path.exists('/dev/i2c-1'):
|
||||
interfaces['i2c'] = True
|
||||
print_success("I2C interface enabled")
|
||||
else:
|
||||
print_warning("I2C interface not enabled")
|
||||
|
||||
# Check SPI
|
||||
if os.path.exists('/dev/spidev0.0'):
|
||||
interfaces['spi'] = True
|
||||
print_success("SPI interface enabled")
|
||||
else:
|
||||
print_warning("SPI interface not enabled")
|
||||
|
||||
return interfaces
|
||||
|
||||
|
||||
def detect_i2c_devices() -> List[str]:
|
||||
"""Detect I2C devices."""
|
||||
print("\nScanning I2C bus...")
|
||||
|
||||
returncode, stdout, stderr = run_command("i2cdetect -y 1", check=False)
|
||||
|
||||
if returncode != 0:
|
||||
print_warning("Could not scan I2C bus (i2cdetect not found or no permission)")
|
||||
return []
|
||||
|
||||
# Parse i2cdetect output
|
||||
devices = []
|
||||
for line in stdout.split('\n'):
|
||||
if ':' in line:
|
||||
# Extract hex addresses
|
||||
parts = line.split(':')[1].split()
|
||||
for part in parts:
|
||||
if part != '--' and len(part) == 2:
|
||||
devices.append(f"0x{part}")
|
||||
|
||||
if devices:
|
||||
print_success(f"Found {len(devices)} I2C device(s): {', '.join(devices)}")
|
||||
|
||||
# Identify known devices
|
||||
device_map = {
|
||||
'0x38': 'FT5316 Touch Panel',
|
||||
'0x14': 'BMA400 Accelerometer',
|
||||
'0x15': 'BMA400 Accelerometer (alt)',
|
||||
'0x68': 'PCF8523 RTC',
|
||||
'0x40': 'INA219 Power Monitor',
|
||||
}
|
||||
|
||||
print("\nDetected devices:")
|
||||
for addr in devices:
|
||||
device_name = device_map.get(addr, 'Unknown device')
|
||||
print(f" {addr}: {device_name}")
|
||||
else:
|
||||
print_warning("No I2C devices detected")
|
||||
|
||||
return devices
|
||||
|
||||
|
||||
def enable_interfaces():
|
||||
"""Enable I2C and SPI interfaces."""
|
||||
print("\nEnabling interfaces...")
|
||||
|
||||
# Use raspi-config to enable I2C and SPI
|
||||
print("Enabling I2C...")
|
||||
run_command("raspi-config nonint do_i2c 0", check=False)
|
||||
|
||||
print("Enabling SPI...")
|
||||
run_command("raspi-config nonint do_spi 0", check=False)
|
||||
|
||||
print_success("Interfaces enabled (reboot required to take effect)")
|
||||
|
||||
|
||||
def setup_permissions():
|
||||
"""Set up user permissions for GPIO, I2C, and SPI."""
|
||||
print("\nSetting up user permissions...")
|
||||
|
||||
user = os.environ.get('SUDO_USER', os.environ.get('USER'))
|
||||
|
||||
groups = ['gpio', 'i2c', 'spi']
|
||||
for group in groups:
|
||||
print(f"Adding user '{user}' to group '{group}'...")
|
||||
returncode, _, _ = run_command(f"usermod -a -G {group} {user}", check=False)
|
||||
|
||||
if returncode == 0:
|
||||
print_success(f"Added to {group} group")
|
||||
else:
|
||||
print_warning(f"Could not add to {group} group (may not exist)")
|
||||
|
||||
print_warning("You must log out and back in for group changes to take effect")
|
||||
|
||||
|
||||
def get_vcom_voltage() -> float:
|
||||
"""Prompt user for VCOM voltage."""
|
||||
print("\n" + Colors.BOLD + "VCOM Voltage Configuration" + Colors.END)
|
||||
print("="*70)
|
||||
print("Your e-ink display has a VCOM voltage printed on a label.")
|
||||
print("This is usually on the back of the display.")
|
||||
print("")
|
||||
print("Example labels:")
|
||||
print(" • VCOM = -2.06V")
|
||||
print(" • VCOM: -1.98V")
|
||||
print(" • -2.14V")
|
||||
print("")
|
||||
print(Colors.RED + Colors.BOLD + "⚠️ IMPORTANT: Using incorrect VCOM can damage your display!" + Colors.END)
|
||||
print("")
|
||||
|
||||
while True:
|
||||
vcom_str = input("Enter your display's VCOM voltage (e.g., -2.06): ").strip()
|
||||
|
||||
try:
|
||||
vcom = float(vcom_str)
|
||||
|
||||
if vcom > 0:
|
||||
print_warning("VCOM is usually negative. Did you forget the minus sign?")
|
||||
continue
|
||||
|
||||
if vcom < -3.0 or vcom > -1.0:
|
||||
print_warning(f"VCOM {vcom}V is unusual. Most displays are between -1.5V and -2.5V")
|
||||
confirm = input("Are you sure this is correct? (y/N): ")
|
||||
if confirm.lower() != 'y':
|
||||
continue
|
||||
|
||||
return vcom
|
||||
|
||||
except ValueError:
|
||||
print_error("Invalid voltage. Please enter a number (e.g., -2.06)")
|
||||
|
||||
|
||||
def configure_gpio_buttons() -> dict:
|
||||
"""Configure GPIO buttons interactively."""
|
||||
print("\n" + Colors.BOLD + "GPIO Button Configuration" + Colors.END)
|
||||
print("="*70)
|
||||
print("Configure physical buttons for navigation.")
|
||||
print("Buttons should be connected between GPIO pin and GND.")
|
||||
print("")
|
||||
|
||||
enable = input("Enable GPIO buttons? (Y/n): ").strip().lower()
|
||||
if enable == 'n':
|
||||
return {
|
||||
"enabled": False,
|
||||
"pull_up": True,
|
||||
"bounce_time_ms": 200,
|
||||
"buttons": []
|
||||
}
|
||||
|
||||
buttons = []
|
||||
|
||||
# Common button configurations (based on actual hardware)
|
||||
button_presets = [
|
||||
("prev_page", "Previous Page", "swipe_right", 22),
|
||||
("next_page", "Next Page", "swipe_left", 27),
|
||||
("power_off", "Power Off", "long_press", 21),
|
||||
]
|
||||
|
||||
print("\nAvailable GPIOs (BCM numbering): 2-27 (avoid 2, 3 if using I2C)")
|
||||
print("")
|
||||
|
||||
for name, description, default_gesture, default_gpio in button_presets:
|
||||
print(f"\n{Colors.BOLD}{description} Button{Colors.END}")
|
||||
enable_btn = input(f" Enable {description} button? (Y/n): ").strip().lower()
|
||||
|
||||
if enable_btn == 'n':
|
||||
continue
|
||||
|
||||
# Get GPIO pin
|
||||
while True:
|
||||
gpio_str = input(f" GPIO pin (default {default_gpio}): ").strip()
|
||||
if not gpio_str:
|
||||
gpio = default_gpio
|
||||
break
|
||||
|
||||
try:
|
||||
gpio = int(gpio_str)
|
||||
if gpio < 2 or gpio > 27:
|
||||
print_error(" GPIO must be between 2 and 27")
|
||||
continue
|
||||
if gpio in [2, 3]:
|
||||
print_warning(" GPIO 2/3 are I2C pins (SDA/SCL)")
|
||||
confirm = input(" Use anyway? (y/N): ")
|
||||
if confirm.lower() != 'y':
|
||||
continue
|
||||
break
|
||||
except ValueError:
|
||||
print_error(" Invalid GPIO number")
|
||||
|
||||
# Add button
|
||||
buttons.append({
|
||||
"name": name,
|
||||
"gpio": gpio,
|
||||
"gesture": default_gesture,
|
||||
"description": description
|
||||
})
|
||||
|
||||
print_success(f" Configured: GPIO {gpio} -> {description}")
|
||||
|
||||
return {
|
||||
"enabled": True,
|
||||
"pull_up": True,
|
||||
"bounce_time_ms": 200,
|
||||
"buttons": buttons
|
||||
}
|
||||
|
||||
|
||||
def create_hardware_config(vcom: float, gpio_config: dict, i2c_devices: List[str]) -> dict:
|
||||
"""Create hardware configuration dictionary."""
|
||||
# Auto-detect which optional components are available
|
||||
has_touch = '0x38' in i2c_devices
|
||||
has_accel = '0x14' in i2c_devices or '0x15' in i2c_devices
|
||||
has_rtc = '0x68' in i2c_devices
|
||||
has_power = '0x40' in i2c_devices
|
||||
|
||||
config = {
|
||||
"_description": "Hardware configuration for DReader e-ink device",
|
||||
"_generated": "Generated by setup_rpi.py",
|
||||
|
||||
"display": {
|
||||
"width": 1872,
|
||||
"height": 1404,
|
||||
"vcom": vcom,
|
||||
"spi_hz": 24000000,
|
||||
"auto_sleep": True
|
||||
},
|
||||
|
||||
"gpio_buttons": gpio_config,
|
||||
|
||||
"accelerometer": {
|
||||
"enabled": has_accel,
|
||||
"tilt_enabled": False,
|
||||
"orientation_enabled": has_accel,
|
||||
"calibration_file": "accelerometer_config.json"
|
||||
},
|
||||
|
||||
"rtc": {
|
||||
"enabled": has_rtc
|
||||
},
|
||||
|
||||
"power_monitor": {
|
||||
"enabled": has_power,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def main():
|
||||
"""Main setup function."""
|
||||
print_header("DReader Raspberry Pi Hardware Setup")
|
||||
|
||||
# Check if running as root
|
||||
if os.geteuid() != 0:
|
||||
print_error("This script must be run with sudo")
|
||||
print("Usage: sudo python3 setup_rpi.py")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 1: Check interfaces
|
||||
print_step(1, "Checking System Interfaces")
|
||||
interfaces = check_interfaces()
|
||||
|
||||
if not all(interfaces.values()):
|
||||
print("\nSome interfaces are not enabled.")
|
||||
enable = input("Enable I2C and SPI now? (Y/n): ").strip().lower()
|
||||
|
||||
if enable != 'n':
|
||||
enable_interfaces()
|
||||
print_warning("Reboot required for interface changes to take effect")
|
||||
|
||||
# Step 2: Detect hardware
|
||||
print_step(2, "Detecting I2C Devices")
|
||||
i2c_devices = detect_i2c_devices()
|
||||
|
||||
if not i2c_devices:
|
||||
print_warning("No I2C devices detected. Check your wiring.")
|
||||
print("See HARDWARE_SETUP.md for wiring instructions.")
|
||||
|
||||
# Step 3: Set up permissions
|
||||
print_step(3, "Setting Up User Permissions")
|
||||
setup_permissions()
|
||||
|
||||
# Step 4: Configure VCOM
|
||||
print_step(4, "Display Configuration")
|
||||
vcom = get_vcom_voltage()
|
||||
print_success(f"VCOM voltage set to {vcom}V")
|
||||
|
||||
# Step 5: Configure GPIO buttons
|
||||
print_step(5, "GPIO Button Configuration")
|
||||
gpio_config = configure_gpio_buttons()
|
||||
|
||||
if gpio_config["enabled"]:
|
||||
print_success(f"Configured {len(gpio_config['buttons'])} button(s)")
|
||||
else:
|
||||
print("GPIO buttons disabled")
|
||||
|
||||
# Step 6: Generate configuration
|
||||
print_step(6, "Generating Configuration File")
|
||||
config = create_hardware_config(vcom, gpio_config, i2c_devices)
|
||||
|
||||
config_file = Path("hardware_config.json")
|
||||
with open(config_file, 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
print_success(f"Configuration saved to {config_file}")
|
||||
|
||||
# Step 7: Summary
|
||||
print_header("Setup Complete!")
|
||||
|
||||
print("Configuration summary:")
|
||||
print(f" • Display: {config['display']['width']}x{config['display']['height']}, VCOM={config['display']['vcom']}V")
|
||||
print(f" • GPIO Buttons: {'Enabled' if gpio_config['enabled'] else 'Disabled'}")
|
||||
if gpio_config['enabled']:
|
||||
for btn in gpio_config['buttons']:
|
||||
print(f" - {btn['description']}: GPIO {btn['gpio']}")
|
||||
print(f" • Accelerometer: {'Enabled' if config['accelerometer']['enabled'] else 'Disabled'}")
|
||||
print(f" • RTC: {'Enabled' if config['rtc']['enabled'] else 'Disabled'}")
|
||||
print(f" • Power Monitor: {'Enabled' if config['power_monitor']['enabled'] else 'Disabled'}")
|
||||
|
||||
print("\n" + Colors.BOLD + "Next Steps:" + Colors.END)
|
||||
print("1. Review and edit hardware_config.json if needed")
|
||||
print("2. Reboot if you enabled I2C/SPI: sudo reboot")
|
||||
print("3. Log out and back in for permission changes")
|
||||
print("4. Run: python examples/run_on_hardware_config.py")
|
||||
print("")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nSetup cancelled by user")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print_error(f"Setup failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
284
tests/test_accelerometer_gestures.py
Normal file
284
tests/test_accelerometer_gestures.py
Normal file
@ -0,0 +1,284 @@
|
||||
"""
|
||||
Tests for accelerometer-based gesture detection.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
# Test only the gesture types and math, not the full integration
|
||||
# to avoid dependencies on pyWebLayout
|
||||
|
||||
|
||||
class MockOrientationSensor:
|
||||
"""Mock BMA400 accelerometer for testing"""
|
||||
|
||||
def __init__(self):
|
||||
self.ax = 0.0
|
||||
self.ay = 0.0
|
||||
self.az = 9.8 # Standard gravity
|
||||
|
||||
async def get_acceleration(self):
|
||||
"""Return mock acceleration data"""
|
||||
return (self.ax, self.ay, self.az)
|
||||
|
||||
def set_acceleration(self, x, y, z):
|
||||
"""Set acceleration for testing"""
|
||||
self.ax = x
|
||||
self.ay = y
|
||||
self.az = z
|
||||
|
||||
|
||||
class MockHAL:
|
||||
"""Mock HAL for testing"""
|
||||
|
||||
def __init__(self):
|
||||
self.orientation = MockOrientationSensor()
|
||||
self.width = 800
|
||||
self.height = 1200
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_hal():
|
||||
"""Create a mock HAL with accelerometer"""
|
||||
return MockHAL()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def calibration_file(tmp_path):
|
||||
"""Create a temporary calibration file"""
|
||||
config = {
|
||||
"up_vector": {
|
||||
"x": 0.0,
|
||||
"y": 9.8,
|
||||
"z": 0.0
|
||||
},
|
||||
"tilt_threshold": 0.3, # ~17 degrees
|
||||
"debounce_time": 0.5
|
||||
}
|
||||
|
||||
config_path = tmp_path / "test_accel_config.json"
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(config, f)
|
||||
|
||||
return str(config_path)
|
||||
|
||||
|
||||
def test_load_calibration_success(mock_hal, calibration_file):
|
||||
"""Test loading accelerometer calibration"""
|
||||
# Create a minimal HAL-like object
|
||||
class TestHAL:
|
||||
def __init__(self):
|
||||
self.width = 800
|
||||
self.height = 1200
|
||||
|
||||
test_hal = TestHAL()
|
||||
|
||||
# Manually call the load function
|
||||
result = load_accel_calibration(test_hal, calibration_file)
|
||||
|
||||
assert result is True
|
||||
assert hasattr(test_hal, 'accel_up_vector')
|
||||
assert test_hal.accel_up_vector == (0.0, 9.8, 0.0)
|
||||
assert test_hal.accel_tilt_threshold == 0.3
|
||||
assert test_hal.accel_debounce_time == 0.5
|
||||
|
||||
|
||||
def load_accel_calibration(hal, config_path):
|
||||
"""Helper function to load calibration (extracted from HardwareDisplayHAL)"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
config_file = Path(config_path)
|
||||
if not config_file.exists():
|
||||
logger.warning(f"Accelerometer calibration file not found: {config_path}")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# Load up vector
|
||||
up = config.get("up_vector", {})
|
||||
hal.accel_up_vector = (up.get("x", 0), up.get("y", 0), up.get("z", 0))
|
||||
|
||||
# Load thresholds
|
||||
hal.accel_tilt_threshold = config.get("tilt_threshold", 0.3)
|
||||
hal.accel_debounce_time = config.get("debounce_time", 0.5)
|
||||
|
||||
# State tracking
|
||||
hal.accel_last_tilt_time = 0
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading accelerometer calibration: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def test_tilt_detection_forward():
|
||||
"""Test forward tilt detection"""
|
||||
# Setup: device is upright (y = 9.8), then tilt forward (z increases)
|
||||
# Calibrated up vector: (0, 9.8, 0)
|
||||
# Current gravity: (0, 6, 6) - tilted ~45 degrees forward
|
||||
|
||||
up_vector = (0.0, 9.8, 0.0)
|
||||
current_gravity = (0.0, 6.0, 6.0)
|
||||
|
||||
# Normalize vectors
|
||||
ux, uy, uz = up_vector
|
||||
u_mag = math.sqrt(ux**2 + uy**2 + uz**2)
|
||||
ux, uy, uz = ux / u_mag, uy / u_mag, uz / u_mag
|
||||
|
||||
gx, gy, gz = current_gravity
|
||||
g_mag = math.sqrt(gx**2 + gy**2 + gz**2)
|
||||
gx, gy, gz = gx / g_mag, gy / g_mag, gz / g_mag
|
||||
|
||||
# Calculate tilt angle
|
||||
dot_up = gx * ux + gy * uy + gz * uz
|
||||
|
||||
perp_x = gx - dot_up * ux
|
||||
perp_y = gy - dot_up * uy
|
||||
perp_z = gz - dot_up * uz
|
||||
|
||||
perp_mag = math.sqrt(perp_x**2 + perp_y**2 + perp_z**2)
|
||||
tilt_angle = math.atan2(perp_mag, abs(dot_up))
|
||||
|
||||
# Should be approximately 45 degrees (0.785 radians)
|
||||
assert abs(tilt_angle - 0.785) < 0.1
|
||||
|
||||
# Direction: forward tilt should have positive perpendicular y component
|
||||
# Actually, when tilting forward, gravity vector rotates toward +z
|
||||
# The perpendicular component should reflect this
|
||||
|
||||
|
||||
def test_tilt_detection_backward():
|
||||
"""Test backward tilt detection"""
|
||||
# Setup: device is upright (y = 9.8), then tilt backward (z decreases, negative)
|
||||
# Calibrated up vector: (0, 9.8, 0)
|
||||
# Current gravity: (0, 6, -6) - tilted ~45 degrees backward
|
||||
|
||||
up_vector = (0.0, 9.8, 0.0)
|
||||
current_gravity = (0.0, 6.0, -6.0)
|
||||
|
||||
# Normalize vectors
|
||||
ux, uy, uz = up_vector
|
||||
u_mag = math.sqrt(ux**2 + uy**2 + uz**2)
|
||||
ux, uy, uz = ux / u_mag, uy / u_mag, uz / u_mag
|
||||
|
||||
gx, gy, gz = current_gravity
|
||||
g_mag = math.sqrt(gx**2 + gy**2 + gz**2)
|
||||
gx, gy, gz = gx / g_mag, gy / g_mag, gz / g_mag
|
||||
|
||||
# Calculate tilt angle
|
||||
dot_up = gx * ux + gy * uy + gz * uz
|
||||
|
||||
perp_x = gx - dot_up * ux
|
||||
perp_y = gy - dot_up * uy
|
||||
perp_z = gz - dot_up * uz
|
||||
|
||||
perp_mag = math.sqrt(perp_x**2 + perp_y**2 + perp_z**2)
|
||||
tilt_angle = math.atan2(perp_mag, abs(dot_up))
|
||||
|
||||
# Should be approximately 45 degrees (0.785 radians)
|
||||
assert abs(tilt_angle - 0.785) < 0.1
|
||||
|
||||
|
||||
def test_no_tilt_when_upright():
|
||||
"""Test that no tilt is detected when device is upright"""
|
||||
# Setup: device is perfectly upright
|
||||
# Calibrated up vector: (0, 9.8, 0)
|
||||
# Current gravity: (0, 9.8, 0) - same as calibration
|
||||
|
||||
up_vector = (0.0, 9.8, 0.0)
|
||||
current_gravity = (0.0, 9.8, 0.0)
|
||||
|
||||
# Normalize vectors
|
||||
ux, uy, uz = up_vector
|
||||
u_mag = math.sqrt(ux**2 + uy**2 + uz**2)
|
||||
ux, uy, uz = ux / u_mag, uy / u_mag, uz / u_mag
|
||||
|
||||
gx, gy, gz = current_gravity
|
||||
g_mag = math.sqrt(gx**2 + gy**2 + gz**2)
|
||||
gx, gy, gz = gx / g_mag, gy / g_mag, gz / g_mag
|
||||
|
||||
# Calculate tilt angle
|
||||
dot_up = gx * ux + gy * uy + gz * uz
|
||||
|
||||
perp_x = gx - dot_up * ux
|
||||
perp_y = gy - dot_up * uy
|
||||
perp_z = gz - dot_up * uz
|
||||
|
||||
perp_mag = math.sqrt(perp_x**2 + perp_y**2 + perp_z**2)
|
||||
tilt_angle = math.atan2(perp_mag, abs(dot_up))
|
||||
|
||||
# Should be approximately 0 degrees
|
||||
assert tilt_angle < 0.01
|
||||
|
||||
|
||||
def test_small_tilt_below_threshold():
|
||||
"""Test that small tilts below threshold are ignored"""
|
||||
# Setup: device is slightly tilted (10 degrees)
|
||||
# Calibrated up vector: (0, 9.8, 0)
|
||||
# Current gravity: small tilt
|
||||
|
||||
angle_rad = math.radians(10)
|
||||
up_vector = (0.0, 9.8, 0.0)
|
||||
current_gravity = (0.0, 9.8 * math.cos(angle_rad), 9.8 * math.sin(angle_rad))
|
||||
|
||||
# Normalize vectors
|
||||
ux, uy, uz = up_vector
|
||||
u_mag = math.sqrt(ux**2 + uy**2 + uz**2)
|
||||
ux, uy, uz = ux / u_mag, uy / u_mag, uz / u_mag
|
||||
|
||||
gx, gy, gz = current_gravity
|
||||
g_mag = math.sqrt(gx**2 + gy**2 + gz**2)
|
||||
gx, gy, gz = gx / g_mag, gy / g_mag, gz / g_mag
|
||||
|
||||
# Calculate tilt angle
|
||||
dot_up = gx * ux + gy * uy + gz * uz
|
||||
|
||||
perp_x = gx - dot_up * ux
|
||||
perp_y = gy - dot_up * uy
|
||||
perp_z = gz - dot_up * uz
|
||||
|
||||
perp_mag = math.sqrt(perp_x**2 + perp_y**2 + perp_z**2)
|
||||
tilt_angle = math.atan2(perp_mag, abs(dot_up))
|
||||
|
||||
# Should be approximately 10 degrees (0.174 radians)
|
||||
assert abs(tilt_angle - 0.174) < 0.01
|
||||
|
||||
# Should be below default threshold of 0.3 rad (~17 degrees)
|
||||
assert tilt_angle < 0.3
|
||||
|
||||
|
||||
def test_gesture_types_exist():
|
||||
"""Test that accelerometer gesture types are defined"""
|
||||
# Simple direct test - check that gesture strings are defined
|
||||
gestures = [
|
||||
"tap",
|
||||
"long_press",
|
||||
"swipe_left",
|
||||
"swipe_right",
|
||||
"swipe_up",
|
||||
"swipe_down",
|
||||
"pinch_in",
|
||||
"pinch_out",
|
||||
"drag_start",
|
||||
"drag_move",
|
||||
"drag_end",
|
||||
"tilt_forward", # Our new gestures
|
||||
"tilt_backward"
|
||||
]
|
||||
|
||||
# Verify the new gesture strings are valid
|
||||
assert "tilt_forward" in gestures
|
||||
assert "tilt_backward" in gestures
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run tests
|
||||
pytest.main([__file__, "-v"])
|
||||
Loading…
x
Reference in New Issue
Block a user