# Waveshare 2.9" Touch ePaper Panel

ESPHome project for the [Waveshare 2.9inch Touch e-Paper HAT](https://www.waveshare.com/2.9inch-Touch-e-Paper-HAT.htm) on an ESP32-WROOM-32 (NodeMCU-32S). Drives an LVGL UI with 2-bit grayscale rendering and touchscreen input.

## Hardware

- **MCU:** ESP32-WROOM-32 (NodeMCU-32S)
- **Display:** SSD1680, 296×128, 2-bit grayscale via custom component
- **Touchscreen:** GT1151 / ICNT86X (I2C, polling mode)

### Wiring

| Function     | ESP Pin | HAT Pin                |
|---           |---      |---                     |
| Display SCLK | GPIO18  | Pin23/GPIO11/SPI0_SCLK |
| Display MOSI | GPIO23  | Pin19/GPIO10/SPI0_MOSI |
| Display CS   | GPIO19  | Pin24/GPIO8/SPI0_CE0   |
| Display DC   | GPIO17  | Pin22/GPIO25           |
| Display BUSY | GPIO4   | Pin18/GPIO24           |
| Display RST  | GPIO5   | Pin11/GPIO17/SPI1_CE1  |
| Touch SDA    | GPIO21  | Pin3/GPIO2/I2C1_SDA    |
| Touch SCL    | GPIO22  | Pin5/GPIO3/I2C1_SCL    |
| Touch RST    | GPIO15  | Pin13/GPIO27           |
| Touch INT    | GPIO16  | Pin15/GPIO22           |

## Project Structure

```
waveshare-panel/
├── waveshare-test.yaml         # Main ESPHome config (edit LVGL layout here)
├── secrets.yaml                # WiFi/API credentials (not committed)
├── FreeSans.ttf                # Font used in LVGL
└── custom_components/
    ├── waveshare_epaper_2bit/  # 2-bit grayscale display driver
    └── icnt86x/                # ICNT86X touchscreen driver
```

## Build Environment Setup

### 1. Install Python 3.11

ESPHome 2026.1.0 requires Python 3.11. Check your version:

```bash
python3 --version
```

On Ubuntu/Debian, install it if needed:

```bash
sudo apt update
sudo apt install python3.11 python3.11-venv python3.11-dev
```

On macOS with Homebrew:

```bash
brew install python@3.11
```

### 2. Create a virtual environment for ESPHome 2026.1.0

Using a dedicated venv keeps this version isolated from other ESPHome installs.

```bash
python3.11 -m venv ~/esphome-env/esphome-2026.1.0
source ~/esphome-env/esphome-2026.1.0/bin/activate
```

### 3. Install ESPHome 2026.1.0

```bash
pip install esphome==2026.1.0
```

Verify the install:

```bash
esphome version
# Expected: Version: 2026.1.0
```

### 4. Activate the environment (each session)

```bash
source ~/esphome-env/esphome-2026.1.0/bin/activate
```

You can add this to your shell profile or run it manually before working on the project.

## Building and Flashing

All commands assume the venv is active and you are in the project directory.

### Compile only

```bash
esphome compile waveshare-test.yaml
```

### Flash via USB (first flash or if OTA fails)

```bash
esphome upload waveshare-test.yaml
```

Connect the ESP32 via USB before running. You may need to hold the BOOT button during flashing if the device doesn't enter download mode automatically.

### Flash via OTA (subsequent flashes)

Once the device is on WiFi, OTA upload works without USB:

```bash
esphome upload waveshare-test.yaml
```

ESPHome will automatically attempt OTA if the device is reachable.

### Monitor serial output

```bash
esphome logs waveshare-test.yaml
```

Or via USB:

```bash
esphome logs --device /dev/ttyUSB0 waveshare-test.yaml
```

## Secrets

Copy the secrets template and fill in your credentials:

```bash
cp secrets.yaml.example secrets.yaml   # if an example exists, otherwise create it
```

`secrets.yaml` format:

```yaml
wifi_ssid: "YourNetworkName"
wappw: "YourWiFiPassword"
enckey: "<32-byte base64 Home Assistant API key>"
apipw: "YourOTAPassword"
hotspotpw: "FallbackPassword"
```

The `fallbackssid` variable is set in `waveshare-test.yaml` and does not go in secrets.

## Making Layout Changes

The LVGL UI is defined in `waveshare-test.yaml` under the `lvgl:` block. The display renders at **296×128 pixels** in a 4-shade grayscale palette:

| LVGL color | Shade      |
|------------|------------|
| `0xFFFFFF` | White      |
| `0xAAAAAA` | Light grey |
| `0x555555` | Dark grey  |
| `0x000000` | Black      |

After editing the YAML, compile and flash:

```bash
esphome upload waveshare-test.yaml
```

The display uses a two-tier refresh:
- **Partial refresh** (fast, 1-bit) — triggers immediately after any draw event for interactive feedback
- **Full refresh** (2-bit grayscale quality) — triggers 10 seconds after the last draw event

Full refresh restores proper 4-shade rendering after the fast partial updates.
