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
|
## 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
|
### Supported Hardware
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
class DisplayHAL(ABC):
|
- **Display**: IT8951 e-ink controller (1872×1404)
|
||||||
"""Abstract display interface for platform integration"""
|
- **Touch**: FT5316 capacitive touch panel
|
||||||
|
- **Buttons**: GPIO buttons (configurable)
|
||||||
|
- **Sensors**: BMA400 accelerometer, PCF8523 RTC, INA219 power monitor
|
||||||
|
|
||||||
@abstractmethod
|
### Quick Setup on Raspberry Pi
|
||||||
def show_image(self, image: Image.Image):
|
|
||||||
"""Display a PIL Image on the screen"""
|
|
||||||
|
|
||||||
@abstractmethod
|
```bash
|
||||||
def get_touch_events(self) -> Iterator[TouchEvent]:
|
# 1. Clone and install
|
||||||
"""Get iterator of touch events from hardware"""
|
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
|
# 2. Interactive hardware setup
|
||||||
def set_brightness(self, level: int):
|
sudo python3 setup_rpi.py
|
||||||
"""Set display brightness (0-10)"""
|
|
||||||
|
# 3. Run on hardware
|
||||||
|
python examples/run_on_hardware_config.py
|
||||||
```
|
```
|
||||||
|
|
||||||
**Example HAL Implementations:**
|
### Hardware Configuration
|
||||||
- **E-ink Displays**: IT8951, Remarkable device SDK
|
|
||||||
- **Desktop**: pygame, tkinter, Qt
|
|
||||||
- **Web**: Flask + HTML canvas
|
|
||||||
- **Embedded**: Device-specific framebuffer
|
|
||||||
|
|
||||||
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
|
## Documentation
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,10 @@ class GestureType(Enum):
|
|||||||
DRAG_MOVE = "drag_move" # Continue dragging
|
DRAG_MOVE = "drag_move" # Continue dragging
|
||||||
DRAG_END = "drag_end" # End dragging/selection
|
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
|
@dataclass
|
||||||
class TouchEvent:
|
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)
|
return self._handle_selection_move(event.x, event.y)
|
||||||
elif event.gesture == GestureType.DRAG_END:
|
elif event.gesture == GestureType.DRAG_END:
|
||||||
return self._handle_selection_end(event.x, event.y)
|
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, {})
|
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