cleishm/idfxx_epaper

2.0.0

Latest
uploaded 4 hours ago
Abstract ePaper panel interface, framebuffers, and shared types

Readme

# idfxx_epaper

Abstract ePaper panel interface, framebuffers, and shared types.

πŸ“š **[Full API Documentation](https://cleishm.github.io/idfxx/group__idfxx__epaper.html)**

## Features

- Abstract `idfxx::epaper::panel` base class β€” drivers for any ePaper
  controller or panel (SSD1680, UC8179, EL133UF1, ...) implement this single interface
  so application code can target any of them.
- ePaper-native vocabulary: `write` uploads pixels to the controller's RAM
  (invisible), `refresh` drives the physical ink update and blocks until the
  panel's BUSY line releases β€” or `start_refresh` returns an `idfxx::future`
  so the calling task can do other work while the glass updates.
- Three refresh styles: `full` (highest quality, clears ghosting), `fast`
  (shortened full-screen waveform), and `partial` (flicker-free differential
  update of changed pixels only).
- 4-level grayscale via `set_color_mode(color_mode::gray4)` and six-color
  (E Ink Spectra 6) operation via `set_color_mode(color_mode::color6)` on
  drivers that support them; `supports(color_mode)` reports which do.
- Selectable border drive (`set_border`: white, black, or held) on drivers
  whose glass supports it; `supports(border)` reports which do.
- Deep-sleep power management (`sleep` / `wake`) with state tracking β€” writes
  and refreshes while asleep report `errc::invalid_state` instead of hanging
  on a dead controller.
- Automatic partial-refresh baseline tracking: the first refresh after
  construction, `wake`, or a color-mode change is silently promoted to a
  full refresh, unless `restore_baseline` re-establishes the baseline from
  a saved frame (e.g. after an ESP32 deep sleep).
- `mono_framebuffer` (1 bpp), `gray4_framebuffer` (2 bpp, two 1-bpp
  planes), and `color6_framebuffer` (4 bpp, two pixels per byte) value types
  in the row-major layouts ePaper controllers consume, uploadable at a pixel
  offset for banded rendering.
- All framebuffers satisfy the `idfxx::gfx::pixel_surface` concept, so the
  `idfxx_gfx` drawing primitives (rectangles, lines, text) work on them
  directly.
- `write_inks` puts a frame of panel inks, converted elsewhere and read from
  any `idfxx::byte_source`, on a panel or in a framebuffer: a band of rows at a
  time, checking its length and six-color values on the way.

## Requirements

- ESP-IDF 5.5 or later
- C++23 compiler
- `idfxx_core` (error handling)
- `idfxx_gpio` (BUSY and reset lines)

## Installation

### ESP-IDF Component Manager

Add to your project's `idf_component.yml`:

```yaml
dependencies:
  cleishm/idfxx_epaper:
    version: "^2.0.0"
```

Or add `idfxx_epaper` to the `REQUIRES` list in your component's
`CMakeLists.txt`.

This component is rarely used on its own β€” depend on a concrete driver such
as `idfxx_epaper_ssd1680`, `idfxx_epaper_uc8179`, or `idfxx_epaper_el133uf1`,
which pulls in `idfxx_epaper` automatically.

## Usage

`idfxx_epaper` defines no concrete driver; it's an interface. Application
code that talks to *any* ePaper display receives an
`idfxx::epaper::panel&`:

```cpp
#include <idfxx/epaper/mono_framebuffer>
#include <idfxx/epaper/panel>

void show_message(idfxx::epaper::panel& display) {
    idfxx::epaper::mono_framebuffer fb(display.width(), display.height());
    fb.set_pixel(10, 20, true); // true = black ink; the buffer starts white

    display.write(fb);   // upload to controller RAM β€” nothing visible yet
    display.refresh();   // drive the ink update; blocks until BUSY releases
    display.sleep();     // deep sleep between updates
}
```

Construct a concrete driver (e.g. `idfxx::epaper::ssd1680`) and pass it
where a `panel&` is expected.

The same flow with the result-based API:

```cpp
idfxx::result<void> show_message(idfxx::epaper::panel& display) {
    auto fb = idfxx::epaper::mono_framebuffer::make(display.width(), display.height());
    if (!fb) {
        return idfxx::error(fb.error());
    }
    fb->set_pixel(10, 20, true);

    if (auto r = display.try_write(*fb); !r) {
        return r;
    }
    if (auto r = display.try_refresh(); !r) {
        return r;
    }
    return display.try_sleep();
}
```

### Partial refresh

A partial refresh flips only the pixels that changed since the previous
refresh β€” no full-screen flash:

```cpp
display.write(fb);
display.refresh(idfxx::epaper::refresh_mode::partial);
```

Partial updates accumulate ghosting; issue a `full` refresh periodically
(every ~10 partials is a good rule of thumb) to clean the panel. The first
refresh after construction, `wake`, or a color-mode change has no baseline
image to diff against and is silently promoted to `full` β€” unless
`restore_baseline` re-establishes one (see Important Notes).

### Refreshing without blocking

`refresh` blocks for the duration of the update β€” seconds for a full
refresh. `start_refresh` starts the same update and returns an
`idfxx::future<void>` instead, so the calling task can do other work
meanwhile and wait, poll, or simply drop the handle:

```cpp
display.write(fb);
auto done = display.start_refresh();
read_sensors();                 // the glass updates meanwhile
done.wait();                    // or: if (done.done()) { ... }
display.sleep();
```

The panel cannot be commanded while the glass is updating, so the next
panel operation (`write`, `clear`, `refresh`, `set_color_mode`, `sleep`)
first completes an outstanding update. A task that refreshes once a minute
and then sleeps the panel therefore only ever blocks in `sleep()`, for
whatever part of the update is still running; dropping the future is safe.
To ask "done yet?" later, keep the future.

### Grayscale

```cpp
display.set_color_mode(idfxx::epaper::color_mode::gray4);

idfxx::epaper::gray4_framebuffer fb(display.width(), display.height());
fb.set_pixel(10, 20, idfxx::epaper::gray4::dark);
display.write(fb);
display.refresh(); // gray4 supports full refresh only
```

### Six-color

Spectra 6 panels take a 4-bit color code per pixel, packed two per byte, so
`color6_framebuffer` uploads must land on an even column (rather than the
multiple of 8 the 1-bpp formats need):

```cpp
display.set_color_mode(idfxx::epaper::color_mode::color6);

idfxx::epaper::color6_framebuffer fb(display.width(), display.height());
fb.set_pixel(10, 20, idfxx::epaper::color6::red);
display.write(fb);
display.refresh(); // color6 supports full refresh only
```

A full 1200x1600 frame is 960 KB, so this framebuffer is usually PSRAM
resident: with `CONFIG_SPIRAM_USE_MALLOC=y` (and the default
`CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL`) `std::vector` places it there
automatically. Without PSRAM, draw through a band-sized framebuffer with
`idfxx::gfx::render_banded` instead.

### Drawing with idfxx_gfx

All framebuffers are `idfxx::gfx::pixel_surface`s, so the gfx canvas and
text primitives draw on them directly; upload the framebuffer the canvas
draws on with `display.write(fb)`:

```cpp
idfxx::epaper::mono_framebuffer fb(display.width(), display.height());
idfxx::gfx::canvas canvas(fb);
canvas.draw_text(idfxx::font::spleen_8x16, 4, 4, "23.7Β°C", true, 2);
display.write(fb);
display.refresh();
```

### Writing a frame of inks from a byte source

A phone or browser can convert a picture to a panel's inks and push the result
to the device, which only has to check it and put it on the panel:

```cpp
#include <idfxx/epaper/ink_writer>

void show_frame(idfxx::epaper::panel& display, idfxx::byte_source auto& connection) {
    display.set_color_mode(idfxx::epaper::color_mode::color6);
    idfxx::epaper::write_inks(display, connection); // the whole panel
    display.refresh();
}
```

The same flow with the result-based API:

```cpp
idfxx::result<void> show_frame(idfxx::epaper::panel& display, idfxx::byte_source auto& connection) {
    if (auto r = display.try_set_color_mode(idfxx::epaper::color_mode::color6); !r) {
        return r;
    }
    if (auto r = idfxx::epaper::try_write_inks(display, connection); !r) {
        return r;
    }
    return display.try_refresh();
}
```

A frame is rows from top to bottom with no header, each row padded to a whole
byte. The panel's `color_mode()`, or the framebuffer's type, picks the format,
where `w` is the width of the rectangle being written:

| Mode     | Bits per pixel | Order             | Pixel values                      | Bytes in a row |
| -------- | -------------- | ----------------- | --------------------------------- | -------------- |
| `color6` | 4              | high nibble first | `color6` values: 0, 1, 2, 3, 5, 6 | `(w + 1) / 2`  |
| `gray4`  | 2              | high bits first   | 0 is darkest, 3 is lightest (the reverse of `gray4` values) | `(w + 3) / 4`  |
| `mono`   | 1              | high bit first    | 1 is white, 0 is black            | `(w + 7) / 8`  |

The `cleishm/epaper-image` library's `epi_pack_row` produces these rows.

`ink_options` puts a frame on part of the destination. Without a `width` or
`height`, it reaches the destination's edge:

```cpp
// A 400x300 frame at (80, 40).
idfxx::epaper::write_inks(display, connection, {.x = 80, .y = 40, .width = 400, .height = 300});
```

To verify a frame, wrap the source in a `byte_source` of your own that computes
a CRC as the bytes pass, and compare it before refreshing:

```cpp
#include <esp_rom_crc.h>

template<idfxx::byte_source Source>
class crc_source {
public:
    explicit crc_source(Source& source) : _source(source) {}

    idfxx::result<size_t> try_read(std::span<uint8_t> buf) {
        auto n = _source.try_read(buf);
        if (n) {
            _crc = esp_rom_crc32_le(_crc, buf.data(), *n);
        }
        return n;
    }

    uint32_t crc() const noexcept { return _crc; }

private:
    Source& _source;
    uint32_t _crc = 0;
};

crc_source checked(connection);
idfxx::epaper::write_inks(display, checked);
// The bytes after the frame are still unread, so the CRC can follow it.
if (checked.crc() == read_expected_crc(connection)) {
    display.refresh();
}
```

## API Overview

### `idfxx::epaper::panel` (abstract)

**Accessors:** `width`, `height`, `color_mode`, `asleep`.

**RAM upload:** `write(fb, x = 0, y = 0)` (overloaded for
`mono_framebuffer`, `gray4_framebuffer`, and `color6_framebuffer`) uploads
at a pixel offset; `write_rows(fb, row_start, row_end, x = 0, y = 0)`
uploads a horizontal band. Destination columns must be a multiple of the
framebuffer's `x_alignment` β€” 8 for the 1-bpp formats (controller RAM is
byte-packed along the row), 2 for `color6_framebuffer` (two pixels per
byte). Rows are written in whole bytes too, so a framebuffer whose width is
not a multiple of the `x_alignment` also writes its white row padding over
the panel columns up to the next byte boundary; size partial framebuffers
to a multiple of it to leave the neighbouring columns untouched. `clear()` fills the RAM with white without needing a framebuffer,
and promotes the next partial refresh to full.

**Partial-refresh baseline:** `restore_baseline(fb)` /
`try_restore_baseline(fb)` load a panel-sized `mono_framebuffer` holding
the image already on the glass, so the next partial refresh diffs against
it instead of being promoted to full. Drivers without support report
`errc::not_supported`.

**Refresh:** `refresh(mode = refresh_mode::full)` blocks until the update
completes; `start_refresh(mode = refresh_mode::full)` returns an
`idfxx::future<void>` that completes when it does.

**Color mode:** `set_color_mode(color_mode)` switches between `mono`,
`gray4`, and `color6` operation. Every driver supports `mono`;
`supports(color_mode)` reports whether it supports the others, and
`set_color_mode` rejects a mode it doesn't support.

**Border:** `set_border(border)` / `try_set_border(border)` select how the
border is driven on full and fast refreshes (`border()` reports it);
`supports(border)` reports which values the driver accepts.

**Power:** `sleep` enters deep sleep (a no-op when already asleep); `wake`
hardware-resets and re-initializes the controller.

Every method has a result-returning `try_*` form; the exception-throwing
forms above are inline wrappers guarded by `CONFIG_COMPILER_CXX_EXCEPTIONS`.

### Framebuffers

- `mono_framebuffer` β€” 1 bpp, row-major, MSB-first, byte-padded rows;
  `pixel_type` is `bool` (`true` = black ink). Starts all-white.
- `gray4_framebuffer` β€” 2 bpp as two 1-bpp planes (plane 0 = bit 0 of the
  level, plane 1 = bit 1); `pixel_type` is `epaper::gray4` (`white`,
  `light`, `dark`, `black`). Starts all-white.
- `color6_framebuffer` β€” 4 bpp, row-major, two pixels per byte (high nibble
  = the even/left pixel), byte-padded rows; `pixel_type` is
  `epaper::color6` (`black`, `white`, `yellow`, `red`, `blue`, `green`).
  Starts all-white.

All provide `set_pixel` / `get_pixel` / `fill` / `clear`, an `x_alignment`
constant, and raw data access (`data()` / `row(y)` on mono and color6,
`plane(i)` / `plane_row(i, y)` on gray4; `data()` and `plane(i)` are also
writable, e.g. to load a saved frame in one copy). Upload them with the
panel's `write` / `write_rows`.

### Writing inks from a byte source

`write_inks(destination, source, options = {})` and
`try_write_inks(destination, source, options = {})` read a frame of inks from
any `idfxx::byte_source` and write it to a `panel`, `mono_framebuffer`,
`gray4_framebuffer`, or `color6_framebuffer`. `ink_options` has the rectangle
to fill (`x`, `y`, and optional `width` and `height`) and `band_rows`, the rows
read at a time: more mean fewer panel writes and more memory.

See `examples/inks_ee05` for a four-gray frame shown on the XIAO EE05.

### Types

- `refresh_mode` β€” `full`, `fast`, `partial`.
- `color_mode` β€” `mono`, `gray4`, `color6`.
- `border` β€” `white` (default), `black`, `hold`.
- `gray4` β€” `white` (0) through `black` (3); a value-initialized `gray4{}` is
  white so `gfx::canvas::clear()` blanks to paper.
- `color6` β€” the panel-native Spectra 6 codes: `black` (0), `white` (1),
  `yellow` (2), `red` (3), `blue` (5), `green` (6). Note that a
  value-initialized `color6{}` is **black** (see Important Notes).

## Error Handling

The interface uses `idfxx::result<T>` and `idfxx::errc` from `idfxx_core`:

- `errc::invalid_arg` β€” unaligned destination column, out-of-bounds
  placement, invalid row range, invalid framebuffer dimensions, or a
  `restore_baseline` framebuffer that isn't panel-sized.
- `errc::invalid_state` β€” writing or refreshing while asleep, writing a
  framebuffer that doesn't match the panel's color mode, restoring a
  baseline while asleep or outside `color_mode::mono`, or waking a driver
  with no reset line configured.
- `errc::not_supported` β€” `fast`/`partial` refresh in gray4 or color6 mode,
  a color mode the concrete driver doesn't support (`supports` returns
  false), or another capability it lacks.
- `errc::timeout` β€” the panel's BUSY line did not release in time.

`write_inks` checks everything it can before reading any byte from the source:

- `errc::invalid_state` β€” the panel is asleep.
- `errc::invalid_arg` β€” `band_rows` is zero, the rectangle's size is zero, or the
  rectangle goes past the destination's edge: a frame has a fixed number of
  bytes, so it is never clipped. Also a left edge that isn't a multiple of the
  mode's `x_alignment`, or a right edge that isn't one unless it is the
  destination's own, on a framebuffer too, because the input is packed.

And while reading:

- `errc::invalid_size` β€” the stream ended before the frame did.
- `errc::invalid_arg` β€” a nibble in a `color6` frame isn't a `color6` value,
  including a row's padding nibble. Nothing of that band is written.
- Any error from the source or the panel stops the frame and is returned
  unchanged.

Using a moved-from driver object is undefined behavior.

## Important Notes

- **Write, then refresh.** `write` only stages pixels in the controller's
  RAM; nothing changes on the glass until `refresh`. This is inherent to
  ePaper, not a quirk of the API.
- **Refresh blocks; start_refresh doesn't.** A full refresh takes on the
  order of seconds (waveform-dependent). `refresh` sleeps the calling task
  in a poll loop on the BUSY line (once per RTOS tick, not busy-spinning);
  `start_refresh` returns immediately and the next panel operation absorbs
  whatever remains of the update.
- **Partial refreshes are full-scan differentials**: `write` places data
  anywhere in RAM, and `refresh(partial)` updates whatever changed across
  the whole panel without flashing. There is no region-limited refresh in
  this version of the interface.
- **Sleep between updates.** Leaving an ePaper controller active degrades
  the glass over time and wastes power; call `sleep()` whenever the display
  will sit idle, and `wake()` before the next update.
- **Keeping partial refreshes across deep sleep.** An ESP32 deep sleep is a
  reboot, and both it and `wake()` lose the partial-refresh baseline, so the
  next partial refresh would be promoted to full. To avoid that, persist the
  frame on the glass (flash, SD card, ...) before sleeping, and hand it back
  with `restore_baseline` after constructing the driver (mono only):

  ```cpp
  // Before sleeping:
  save_frame(fb.data());   // application storage
  display.sleep();

  // After the reboot, once the driver is constructed:
  idfxx::epaper::mono_framebuffer fb(display.width(), display.height());
  if (load_frame(fb.data())) {   // false if nothing valid was saved
      display.restore_baseline(fb);
  }
  fb.set_pixel(10, 20, true);    // draw the changes
  display.write(fb);
  display.refresh(idfxx::epaper::refresh_mode::partial);
  ```

  The saved frame must match the glass: a mismatch does no damage, but the
  differing pixels are not driven correctly until the next full refresh.
  `idfxx_epaper_ssd1680`'s `examples/deep_sleep_partial` shows the full
  cycle, keeping the frame in RTC memory across timer wakes.
- **`color6{}` is black, not white.** The six-color codes are the panel's
  own, and code 0 is black. `color6_framebuffer` starts white and its
  `clear()` blanks to white, and a `gfx::canvas` covering the whole
  framebuffer clears through it β€” but a *windowed* canvas (one placed at an
  offset, or narrower than its surface) has no whole-surface clear to call
  and fills with the value-initialized pixel, inking the window black. Use
  `canvas.fill(color6::white)` there.
- **`write_inks` can only check so much.** A `color6` frame has invalid values,
  but in 1 bpp and 2 bpp frames every value is a valid ink, so they can only be
  length-checked: corruption isn't visible. Verify a frame with a CRC if it
  matters (see Usage).
- **A frame for other glass can't be detected.** One made for a different panel of
  the same size and mode is accepted. Checking which panel a frame is for is the
  application's job, for instance on connecting.
- **`write_inks` stops at the end of the frame.** Bytes after it aren't read or
  checked, and the end of the stream isn't looked for, so an open connection
  doesn't block and a CRC can follow the frame.
- **After a failed `write_inks`, write the frame again before refreshing.** Earlier
  bands stay in the panel's memory, and after a panel error so may part of the
  band being written. The glass doesn't change until a refresh, but any later
  refresh shows them. The partial-refresh baseline isn't affected: it remains what
  is on the glass, so a partial refresh after the frame has been written again is
  correct. Writing the frame to a framebuffer first, and only then the framebuffer
  to the panel, gives all-or-nothing at the cost of a frame's memory.
- **A row's padding is always written as white,** whatever arrives, so a frame
  can't break a framebuffer's padding. A `color6` padding nibble must still be a
  valid value.
- Drivers keep an internal shadow copy of the last-written frame to
  maintain the controller's previous-image plane for partial refreshes β€”
  budget roughly one frame of heap per panel (see each driver's README for
  exact numbers).

## License

Apache License 2.0 - see [LICENSE](LICENSE) for details.

Links

Supports all targets

To add this component to your project, run:

idf.py add-dependency "cleishm/idfxx_epaper^2.0.0"

download archive

Stats

  • Archive size
    Archive size ~ 109.10 KB
  • Downloaded in total
    Downloaded in total 24 times
  • Weekly Downloads Weekly Downloads (All Versions)
  • Downloaded this version
    This version: 0 times

Badge

cleishm/idfxx_epaper version: 2.0.0
|