# DReader Hardware Guide Everything needed to build, wire, configure and run DReader on real e-ink hardware. This guide covers the reference device: a Raspberry Pi driving an IT8951 e-ink controller with an FT5316 touch panel, optional sensors and three physical buttons. If you are *implementing a HAL for different hardware*, see [HAL_IMPLEMENTATION_SPEC.md](HAL_IMPLEMENTATION_SPEC.md) instead. ## Contents - [Quick Start](#quick-start) - [1. Hardware Requirements](#1-hardware-requirements) - [2. Wiring](#2-wiring) - [3. Software Installation](#3-software-installation) - [4. Configuration](#4-configuration) - [5. Running DReader](#5-running-dreader) - [6. Input Reference](#6-input-reference) - [7. Accelerometer Page Flipping](#7-accelerometer-page-flipping) - [8. Verifying Hardware](#8-verifying-hardware) - [9. Troubleshooting](#9-troubleshooting) - [10. Performance Notes](#10-performance-notes) - [Appendix: Programmatic Usage](#appendix-programmatic-usage) --- ## Quick Start On a Raspberry Pi with the hardware already wired: ```bash # 1. Clone and set up git clone https://gitea.tourolle.paris/dtourolle/dreader-application.git cd dreader-application git submodule update --init --recursive python3 -m venv venv source venv/bin/activate # 2. Install the application and hardware drivers pip install -e . ./install_hardware_drivers.sh # 3. Interactive setup — detects hardware, writes hardware_config.json sudo python3 setup_rpi.py # 4. Run python examples/run_on_hardware_config.py ``` > ⚠️ **Before first run, check your display's VCOM voltage.** See > [VCOM Voltage](#vcom-voltage) — an incorrect value can damage the panel. --- ## 1. Hardware Requirements ### Required | Component | Notes | |-----------|-------| | **Raspberry Pi** (or compatible SBC) | 512MB RAM minimum | | **IT8951 E-ink Display Controller** | 1872 × 1404, SPI | | **FT5316 Capacitive Touch Panel** | I2C, address `0x38` | ### Optional | Component | Purpose | I2C Address | |-----------|---------|-------------| | **BMA400 Accelerometer** | Auto-rotation and tilt page-flipping | `0x14` (or `0x15`) | | **PCF8523 RTC** | Timekeeping with battery backup | `0x68` | | **INA219 Power Monitor** | Battery level monitoring | `0x40` | | **Momentary pushbuttons** ×3 | Page turns and power off | — | ### Power Requirements - **Input:** 5V via USB-C or GPIO header - **Display:** ~3.3V, peak 500mA during refresh - **Touch panel:** 3.3V, ~20mA - **Total (active):** ~1–2W - **Total (sleep):** ~50–100mW --- ## 2. Wiring ### Pin Assignment Summary All GPIO numbers in this guide use **BCM numbering**, not physical pin numbers. | GPIO | Physical Pin | Function | |------|--------------|----------| | 2 | 3 | I2C1 SDA — touch, accelerometer, RTC, power monitor | | 3 | 5 | I2C1 SCL — all I2C devices | | 8 | 24 | SPI0 CE0 — display chip select | | 9 | 21 | SPI0 MISO | | 10 | 19 | SPI0 MOSI | | 11 | 23 | SPI0 SCLK | | 17 | 11 | Display RST | | 21 | 40 | Power off button | | 22 | 15 | Previous page button | | 24 | 18 | Display HRDY | | 27 | 13 | Next page button | ### IT8951 E-ink Display (SPI) | IT8951 Pin | Raspberry Pi | Description | |------------|--------------|-------------| | VCC | 3.3V (Pin 1) | Power supply | | GND | GND (Pin 6) | Ground | | MISO | GPIO 9 (Pin 21) | SPI MISO | | MOSI | GPIO 10 (Pin 19) | SPI MOSI | | SCK | GPIO 11 (Pin 23) | SPI Clock | | CS | GPIO 8 (Pin 24) | SPI Chip Select | | RST | GPIO 17 (Pin 11) | Reset | | HRDY | GPIO 24 (Pin 18) | Ready signal | ### I2C Devices All I2C devices share SDA (GPIO 2, Pin 3) and SCL (GPIO 3, Pin 5), plus 3.3V and GND. Each must have a unique address. | Device | Extra connections | |--------|-------------------| | **FT5316 Touch Panel** | INT is optional and unused in the reference build — see note below | | **BMA400 Accelerometer** | None. Confirm the address on your module (`0x14` or `0x15`) | | **PCF8523 RTC** | BAT → CR2032 backup battery | | **INA219 Power Monitor** | VIN+ → battery positive; VIN- → through shunt resistor to load | > **Note on the FT5316 INT pin:** some wiring guides route it to GPIO 27, which > collides with the next-page button on this device. The reference build leaves INT > unconnected and polls the touch panel instead. If you want to use INT, move it to a > free GPIO (see [safe pins](#choosing-gpio-pins)) or relocate the next-page button. ### GPIO Buttons The reference device has three buttons: ``` ┌─────────────────────┐ │ │ │ [Power Off] │ ← Side button (GPIO 21) │ │ │ E-INK │ │ DISPLAY │ │ 1872x1404 │ │ │ │ [Prev] [Next] │ ← Bottom edge (GPIO 22, GPIO 27) │ │ └─────────────────────┘ ``` | GPIO | Function | Gesture Generated | Wiring | |------|----------|-------------------|--------| | 22 | Previous Page | `swipe_right` | GPIO → button → **GND**, internal pull-up, active LOW | | 27 | Next Page | `swipe_left` | GPIO → button → **GND**, internal pull-up, active LOW | | 21 | Power Off (long press) | `long_press` | GPIO → button → **3.3V**, pull-up disabled, active HIGH | > ⚠️ **The power button is wired differently from the page buttons.** Prev/Next pull > LOW to GND; the power button pulls HIGH when pressed and sets `"pull_up": false` in > the config. Wiring it to GND like the others will make it appear permanently pressed. Active-low page button wiring: ``` +3.3V | R (internal pull-up) | GPIO --|------ Button ------ GND | (to BCM2835) ``` Use momentary pushbuttons (normally open) rated for at least 10,000 cycles. Tactile switches give the best feedback. Keep wires short to reduce noise, use stranded wire for flexibility, and add strain relief at the connection points. ### Physical Layout ``` Raspberry Pi GPIO Header (BCM Numbering) 3V3 (1) (2) 5V GPIO2 (3) (4) 5V ← I2C1 SDA (touch, sensors) GPIO3 (5) (6) GND ← I2C1 SCL GPIO4 (7) (8) GPIO14 GND (9) (10) GPIO15 GPIO17 (11) (12) GPIO18 ← Display RST GPIO27 (13) (14) GND ← Next page button GPIO22 (15) (16) GPIO23 ← Previous page button 3V3 (17) (18) GPIO24 ← Display HRDY GPIO10 (19) (20) GND ← SPI0 MOSI GPIO9 (21) (22) GPIO25 ← SPI0 MISO GPIO11 (23) (24) GPIO8 ← SPI0 SCLK, CE0 GND (25) (26) GPIO7 GPIO0 (27) (28) GPIO1 GPIO5 (29) (30) GND GPIO6 (31) (32) GPIO12 GPIO13 (33) (34) GND GPIO19 (35) (36) GPIO16 GPIO26 (37) (38) GPIO20 GND (39) (40) GPIO21 ← Power off button ``` ### Choosing GPIO Pins If you are remapping buttons: **Safe for buttons:** GPIO 5, 6, 12, 13, 16, 18, 19, 20, 21, 22, 23, 25, 26, 27 **Avoid:** | Pins | Reserved for | |------|--------------| | GPIO 0, 1 | ID EEPROM | | GPIO 2, 3 | I2C — touch and sensors | | GPIO 7–11 | SPI — e-ink display | | GPIO 14, 15 | UART — serial console | | GPIO 17, 24 | Display RST and HRDY on this device | --- ## 3. Software Installation ### 1. System Dependencies ```bash sudo apt update sudo apt install -y python3-dev python3-pip python3-venv sudo apt install -y i2c-tools python3-smbus # Enable I2C and SPI sudo raspi-config # Navigate to: Interface Options -> Enable I2C and SPI ``` ### 2. Clone and Set Up the Application ```bash git clone https://gitea.tourolle.paris/dtourolle/dreader-application.git cd dreader-application # dreader-hal is a submodule git submodule update --init --recursive python3 -m venv venv source venv/bin/activate pip install -e . ``` ### 3. Install dreader-hal and Drivers The simplest route is the bundled script: ```bash ./install_hardware_drivers.sh ``` To do it by hand — `dreader-hal` keeps its driver dependencies in its own `external/` directory: ```bash pip install -e external/dreader-hal cd external/dreader-hal/external pip install -e IT8951 pip install -e PyFTtxx6 pip install -e PyBMA400 pip install -e PyPCF8523 pip install -e pi_ina219 cd ../../.. ``` ### 4. Raspberry Pi GPIO ```bash pip install RPi.GPIO spidev ``` ### 5. Permissions ```bash sudo usermod -a -G spi,i2c,gpio $USER # Log out and back in for group changes to take effect ``` --- ## 4. Configuration DReader uses two configuration files: | File | Purpose | |------|---------| | `hardware_config.json` | Display, buttons, sensors, application defaults | | `accelerometer_config.json` | Tilt calibration — see [section 7](#7-accelerometer-page-flipping) | ### Generating the Config The interactive setup script detects your hardware and writes `hardware_config.json`: ```bash sudo python3 setup_rpi.py ``` It will detect connected I2C devices, enable I2C/SPI if needed, configure the GPIO button mapping and set the VCOM voltage. ### VCOM Voltage ⚠️ **CRITICAL**: Every e-ink panel has a unique VCOM voltage printed on a label, usually on the ribbon cable or the back of the panel. - Read the value from your display's label (e.g. `VCOM = -2.06V`) - Set it as `display.vcom` in the config, or pass `--vcom -2.06` on the command line - **Using the wrong VCOM can damage the display** ### hardware_config.json Reference ```json { "display": { "width": 1872, "height": 1404, "vcom": -1.7, "spi_hz": 24000000, "rotate": "CW", "auto_sleep": true }, "gpio_buttons": { "enabled": true, "pull_up": true, "bounce_time_ms": 200, "buttons": [ {"name": "prev_page", "gpio": 22, "gesture": "swipe_right"}, {"name": "next_page", "gpio": 27, "gesture": "swipe_left"}, {"name": "power_off", "gpio": 21, "gesture": "long_press", "pull_up": false} ] }, "accelerometer": { "enabled": true, "tilt_enabled": false, "orientation_enabled": true, "calibration_file": "accelerometer_config.json" }, "rtc": {"enabled": true}, "power_monitor": { "enabled": true, "shunt_ohms": 0.1, "battery_capacity_mah": 3000, "low_battery_threshold": 20.0, "show_battery_interval": 100 }, "application": { "library_path": "/home/pi/Books", "auto_save_interval": 60, "force_library_mode": false, "log_level": "INFO" } } ``` #### `gpio_buttons` | Key | Type | Description | |-----|------|-------------| | `enabled` | bool | Enable or disable all GPIO buttons | | `pull_up` | bool | Default pull-up for all buttons; override per button | | `bounce_time_ms` | int | Debounce time in milliseconds (default 200) | | `buttons` | array | Button definitions | Each button takes: | Key | Type | Description | |-----|------|-------------| | `name` | string | Unique identifier | | `gpio` | int | BCM pin number | | `gesture` | string | Gesture emitted on press — see [gesture table](#gestures) | | `pull_up` | bool | Optional per-button override of the group default | | `description` | string | Optional human-readable note | Buttons can be mapped to *any* gesture, so custom layouts are just config. For example, a four-button reading layout: ```json { "gpio_buttons": { "enabled": true, "buttons": [ {"name": "next", "gpio": 27, "gesture": "swipe_left"}, {"name": "prev", "gpio": 22, "gesture": "swipe_right"}, {"name": "zoom_in", "gpio": 25, "gesture": "pinch_out"}, {"name": "zoom_out", "gpio": 23, "gesture": "pinch_in"} ] } } ``` Button handling lives in [dreader/gpio_buttons.py](../dreader/gpio_buttons.py). --- ## 5. Running DReader ### With a Config File (Recommended) ```bash python examples/run_on_hardware_config.py ``` ### Direct Command Line ```bash source venv/bin/activate python examples/run_on_hardware.py /path/to/books --vcom -2.06 ``` ### Without Hardware (Virtual Display) Test the integration on a development machine — this opens a Tkinter window simulating the e-ink panel: ```bash python examples/run_on_hardware.py /path/to/books \ --virtual --no-orientation --no-rtc --no-power ``` ### Common Options | Option | Effect | |--------|--------| | `--vcom ` | Display VCOM voltage (**check your label**) | | `--virtual` | Tkinter virtual display instead of real hardware | | `--no-orientation` | Disable the accelerometer | | `--no-rtc` | Disable the real-time clock | | `--no-power` | Disable the battery monitor | | `--show-battery` | Print battery level periodically | | `--battery-capacity ` | Set battery capacity (default 3000) | | `--force-library` | Always start in library mode, ignoring saved state | | `--verbose` | Verbose debug logging | Full reference: `python examples/run_on_hardware.py --help` --- ## 6. Input Reference ### Gestures Every input source — touch, buttons, accelerometer — is normalised into the same gesture stream, so any source can trigger any action. | Gesture | Default Action | |---------|----------------| | `swipe_left` | Next page | | `swipe_right` | Previous page | | `swipe_up` (from bottom) | Open navigation/TOC overlay | | `swipe_down` (from top) | Open settings overlay | | `tap` | Select book, word, or link | | `long_press` | Word definition / context menu | | `pinch_in` | Decrease font size | | `pinch_out` | Increase font size | | `tilt_forward` | Next page — see [section 7](#7-accelerometer-page-flipping) | | `tilt_backward` | Previous page — see [section 7](#7-accelerometer-page-flipping) | --- ## 7. Accelerometer Page Flipping The BMA400 can detect device tilt and turn pages hands-free — useful while eating, holding the device one-handed, when it is mounted on a stand, or for accessibility. > Tilt is **off by default** (`accelerometer.tilt_enabled: false`). Enable it in > `hardware_config.json` after calibrating. ### Calibration Calibration establishes which direction is "up" for your device: ```bash python examples/calibrate_accelerometer.py ``` 1. The display shows an arrow pointing in the direction of gravity 2. Rotate the device until the arrow points up 3. Tap the screen to save 4. Calibration is written to `accelerometer_config.json` ### Calibration File Format ```json { "up_vector": {"x": 0.0, "y": 9.8, "z": 0.0}, "tilt_threshold": 0.3, "debounce_time": 0.5 } ``` | Key | Description | |-----|-------------| | `up_vector` | Gravity vector (m/s²) when the device is upright | | `tilt_threshold` | Tilt angle threshold in radians | | `debounce_time` | Minimum seconds between gestures | **Tuning `tilt_threshold`:** | Value | Behaviour | |-------|-----------| | 0.1 rad (~6°) | Very sensitive — small tilts turn pages | | 0.3 rad (~17°) | Default | | 0.5 rad (~29°) | Requires a deliberate, larger tilt | **Tuning `debounce_time`:** | Value | Behaviour | |-------|-----------| | 0.2s | Fast — flip several pages quickly | | 0.5s | Default — prevents accidental double-flips | | 1.0s | Slow — requires a pause between flips | ### How Tilt Detection Works 1. **Read** the accelerometer for (x, y, z) acceleration in m/s² 2. **Normalise** both the current gravity vector and the calibrated up vector 3. **Compute the tilt angle** by projecting gravity onto the plane perpendicular to the up vector 4. **Compare** against `tilt_threshold` 5. **Determine direction** from the sign of the perpendicular y-component 6. **Debounce** to suppress repeat triggers Given a calibrated up vector `U = (ux, uy, uz)` and current gravity `G = (gx, gy, gz)`: ```python # Component of G along U dot = gx*ux + gy*uy + gz*uz # Perpendicular component perp = G - dot*U perp_magnitude = |perp| # Tilt angle angle = atan2(perp_magnitude, |dot|) # Direction if perp_y > 0: gesture = TILT_FORWARD # next page else: gesture = TILT_BACKWARD # previous page ``` The implementation lives in [dreader/hal_hardware.py](../dreader/hal_hardware.py) (`get_tilt_gesture`), with the gesture types defined in [dreader/gesture.py](../dreader/gesture.py) and handled in [dreader/handlers/gestures.py](../dreader/handlers/gestures.py). ### Using Tilt in Your Own Code ```python from dreader.hal_hardware import HardwareDisplayHAL hal = HardwareDisplayHAL( width=1872, height=1404, enable_orientation=True, # required for tilt ) await hal.initialize() if hal.load_accelerometer_calibration("accelerometer_config.json"): print("Accelerometer calibrated!") else: print("No calibration found - tilt gestures disabled") ``` **Unified event loop (recommended)** — `get_event()` polls both touch and accelerometer, prioritising touch: ```python while running: event = await hal.get_event() if event: response = gesture_router.handle_touch(event) await asyncio.sleep(0.01) ``` **Separate polling** — more control over each source: ```python while running: touch_event = await hal.get_touch_event() tilt_event = await hal.get_tilt_gesture() # None if uncalibrated if touch_event: response = gesture_router.handle_touch(touch_event) if tilt_event: response = gesture_router.handle_touch(tilt_event) await asyncio.sleep(0.05) # ~20Hz ``` ### Tilt API Reference | Method | Description | |--------|-------------| | `load_accelerometer_calibration(config_path="accelerometer_config.json") -> bool` | Load calibration from JSON. Returns `True` on success. | | `async get_event() -> Optional[TouchEvent]` | **Recommended.** Next event from any input source; touch takes priority over tilt. `None` if nothing pending. | | `async get_tilt_gesture() -> Optional[TouchEvent]` | Poll the accelerometer only. Returns a `TILT_FORWARD`/`TILT_BACKWARD` event, or `None` if there is no tilt, no calibration, or the debounce window is still open. | ### Demos and Tests ```bash # Simple demo using the unified get_event() API python examples/demo_accelerometer_simple.py ~/Books/mybook.epub # Full-featured demo with separate polling python examples/demo_accelerometer_page_flip.py ~/Books/mybook.epub # Tests python -m pytest tests/test_accelerometer_gestures.py -v ``` ### Known Limitations 1. **Orientation lock** — tilt detection assumes a fixed device orientation, so auto-rotation can interfere with it. 2. **Movement** — walking may cause false positives. Raise the threshold or disable tilt while moving. 3. **Calibration drift** — the accelerometer drifts over time; re-calibrate periodically. 4. **Simplified direction heuristic** — complex orientations may be misread. 5. **Single axis** — only tilt in one plane is detected; left/right tilts are not distinguished. --- ## 8. Verifying Hardware ### Check I2C Devices ```bash i2cdetect -y 1 ``` Expected on a fully-populated device: ``` 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- 14 -- -- -- -- -- -- -- -- -- -- -- ← BMA400 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- 38 -- -- -- -- -- -- -- ← FT5316 40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- ← INA219 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- -- ← PCF8523 70: -- -- -- -- -- -- -- -- ``` ### Check SPI ```bash ls /dev/spi* # Should show: /dev/spidev0.0 /dev/spidev0.1 ``` ### Check Buttons ```bash sudo apt install gpiod gpioget gpiochip0 22 # Previous page — 1 idle, 0 when pressed gpioget gpiochip0 27 # Next page — 1 idle, 0 when pressed gpioget gpiochip0 21 # Power off — 0 idle, 1 when pressed (active high) ``` Or watch the application logs: ```bash python examples/run_on_hardware_config.py --verbose ``` Pressing a button should log something like: ``` Button pressed: next_page (GPIO 27) Button event queued: next_page -> swipe_left ``` --- ## 9. Troubleshooting ### Display Not Working 1. **Check VCOM voltage** — it must match the label on your display 2. **Check SPI is enabled** — `ls /dev/spi*` should list two devices 3. **Enable SPI if missing** — `sudo raspi-config` → Interface Options → SPI → Enable 4. **Check permissions** — `sudo usermod -a -G spi $USER`, then log out and back in 5. **Isolate the problem** — run with `--virtual` to test the software without hardware ### Touch Not Working 1. **Check I2C connections** — `i2cdetect -y 1` should show `0x38` 2. **Enable I2C if missing** — `sudo raspi-config` → Interface Options → I2C → Enable 3. **Confirm the module loaded** — `lsmod | grep i2c` should show `i2c_dev` and `i2c_bcm2835` 4. **Check permissions** — `sudo usermod -a -G i2c $USER`, then log out and back in 5. **Calibrate touch** — see the dreader-hal calibration docs ### Buttons Not Working 1. **Check the wiring polarity** — prev/next go to GND; the power button goes to 3.3V 2. **Verify the pin numbers** are BCM, not physical pin numbers 3. **Check permissions** — `sudo usermod -a -G gpio $USER`, then log out and back in 4. **Test the pin directly** with `gpioget` (see [section 8](#check-buttons)) 5. **Check for conflicts** — make sure no other program holds those GPIOs ### Buttons Trigger Multiple Times 1. Increase `bounce_time_ms` in the config (try 300–500ms) 2. Add a hardware debounce capacitor (0.1µF between GPIO and GND) 3. Check for loose connections ### Button Performs the Wrong Action 1. Check the `gesture` field for that button in `hardware_config.json` 2. Run with `--verbose` to see which gesture is actually emitted ### Tilt Gestures Not Working 1. **Calibrate first** — `python examples/calibrate_accelerometer.py` 2. **Enable the accelerometer** — `HardwareDisplayHAL(enable_orientation=True)` 3. **Verify the calibration loaded** — `hal.load_accelerometer_calibration()` returns `True` 4. **Confirm you are polling** — call `get_event()` or `get_tilt_gesture()` in your loop 5. **Adjust sensitivity** — tune `tilt_threshold` and `debounce_time` If tilting forward turns the page *backward*, the direction heuristic is inverted for your device's mounting. Flip the comparison in `get_tilt_gesture()` in [dreader/hal_hardware.py](../dreader/hal_hardware.py): ```python if perp_y < 0: # was: perp_y > 0 gesture = AppGestureType.TILT_FORWARD else: gesture = AppGestureType.TILT_BACKWARD ``` ### Import Errors If you see `ModuleNotFoundError` for the drivers, reinstall them all: ```bash cd external/dreader-hal/external for dir in */; do pip install -e "$dir"; done cd ../../.. ``` ### Display Ghosting E-ink panels retain a faint previous image. The HAL performs a full refresh every 10 page turns automatically to clear it. See the dreader-hal documentation for manual refresh control. --- ## 10. Performance Notes ### E-ink Refresh Modes dreader-hal selects the refresh mode automatically: | Mode | Time | Used for | |------|------|----------| | **Fast** (DU) | ~200ms | Text updates | | **Quality** (GC16) | ~1000ms | Images | | **Full** (INIT) | ~1000ms | Every 10 pages, to clear ghosting | ### Battery Life With default settings: - Active reading: ~10–20 hours - Standby (display sleeping): ~1–2 weeks ### Memory Usage - Base application: ~30–50MB - Per book: ~10–30MB depending on size - Minimum 512MB RAM recommended --- ## Appendix: Programmatic Usage Driving the hardware HAL directly from your own script: ```python import asyncio from dreader.hal_hardware import HardwareDisplayHAL from dreader.main import DReaderApplication, AppConfig async def main(): hal = HardwareDisplayHAL( width=1872, height=1404, vcom=-2.06, # YOUR DISPLAY'S VCOM! virtual_display=False, enable_orientation=True, enable_rtc=True, enable_power_monitor=True, ) config = AppConfig( display_hal=hal, library_path="/home/pi/Books", page_size=(1872, 1404), ) app = DReaderApplication(config) try: await hal.initialize() await app.start() while app.is_running(): event = await hal.get_touch_event() if event: await app.handle_touch(event) await asyncio.sleep(0.01) finally: await app.shutdown() await hal.cleanup() if __name__ == '__main__': asyncio.run(main()) ``` --- ## See Also - [README.md](../README.md) — application features and usage - [ARCHITECTURE.md](ARCHITECTURE.md) — system architecture and design - [HAL_IMPLEMENTATION_SPEC.md](HAL_IMPLEMENTATION_SPEC.md) — writing a HAL for other hardware - [examples/](../examples/) — runnable examples - [external/dreader-hal/README.md](../external/dreader-hal/README.md) — HAL library details ### Support - Hardware issues: [dreader-hal issues](https://gitea.tourolle.paris/dtourolle/dreader-hal/issues) - Application issues: [dreader-application issues](https://gitea.tourolle.paris/dtourolle/dreader-application/issues)