[epaper_spi] Add support for the Inkplate 2 (#16856)

This commit is contained in:
arunderwood
2026-06-23 16:50:02 +10:00
committed by GitHub
parent c70d56807f
commit 41747c2de7
5 changed files with 271 additions and 0 deletions
+17
View File
@@ -64,4 +64,21 @@ constexpr NATIVE_COLOR color_to_bwyr(Color color, NATIVE_COLOR hw_black, NATIVE_
}
}
/** Map RGB color to discrete BWR (black/white/red) 3 color key
*
* Convenience wrapper over color_to_bwyr for panels without a yellow ink; the yellow corner is
* folded into white.
*
* @tparam NATIVE_COLOR Type of native hardware color values
* @param color RGB color to convert from
* @param hw_black Native value for black
* @param hw_white Native value for white
* @param hw_red Native value for red
* @return Converted native hardware color value
*/
template<typename NATIVE_COLOR>
constexpr NATIVE_COLOR color_to_bwr(Color color, NATIVE_COLOR hw_black, NATIVE_COLOR hw_white, NATIVE_COLOR hw_red) {
return color_to_bwyr<NATIVE_COLOR>(color, hw_black, hw_white, /*hw_yellow=*/hw_white, hw_red);
}
} // namespace esphome::epaper_spi
@@ -0,0 +1,148 @@
// Reference: https://github.com/SolderedElectronics/Inkplate-Arduino-library (src/boards/Inkplate2)
#include "epaper_spi_inkplate2.h"
#include "colorconv.h"
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.inkplate2";
// Map RGB to the panel's black/white/red via the shared converter.
enum class Inkplate2Color : uint8_t { BLACK, WHITE, RED };
static Inkplate2Color to_inkplate2_color(Color color) {
return color_to_bwr<Inkplate2Color>(color, Inkplate2Color::BLACK, Inkplate2Color::WHITE, Inkplate2Color::RED);
}
void EPaperInkplate2::power_on() {
// Power-on (0x04) leads the init sequence, so there is nothing to do here.
ESP_LOGV(TAG, "Power on");
}
void EPaperInkplate2::power_off() {
ESP_LOGV(TAG, "Power off");
this->cmd_data(0x50, {0xF7}); // VCOM and data interval
this->command(0x02); // power off
}
void EPaperInkplate2::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh screen"); // full refresh only; partial is unused
// Send 0x11 then 0x12 back-to-back: 0x11 raises busy until the refresh finishes, so waiting for idle
// between them (as the state machine does between states) would add a ~16s stall.
this->cmd_data(0x11, {0x00}); // stop data transfer
this->command(0x12); // display refresh
}
void EPaperInkplate2::deep_sleep() {
ESP_LOGV(TAG, "Deep sleep");
this->cmd_data(0x07, {0xA5});
}
void EPaperInkplate2::fill(Color color) {
if (this->get_clipping().is_set()) {
EPaperBase::fill(color); // clipping active: defer to the base per-pixel path
return;
}
const size_t half_buffer = this->buffer_length_ / 2;
// Plane encoding: B/W plane 1=white, 0=black; red plane 0=red, 1=no-red.
uint8_t bw_byte;
uint8_t red_byte;
switch (to_inkplate2_color(color)) {
case Inkplate2Color::BLACK:
bw_byte = 0x00;
red_byte = 0xFF;
break;
case Inkplate2Color::RED:
bw_byte = 0xFF;
red_byte = 0x00;
break;
case Inkplate2Color::WHITE:
default:
bw_byte = 0xFF;
red_byte = 0xFF;
break;
}
for (size_t i = 0; i < half_buffer; i++)
this->buffer_[i] = bw_byte;
for (size_t i = half_buffer; i < this->buffer_length_; i++)
this->buffer_[i] = red_byte;
this->x_low_ = 0;
this->y_low_ = 0;
this->x_high_ = this->width_;
this->y_high_ = this->height_;
}
void EPaperInkplate2::clear() { this->fill(COLOR_ON); }
void HOT EPaperInkplate2::draw_pixel_at(int x, int y, Color color) {
if (!this->rotate_coordinates_(x, y))
return;
const size_t half_buffer = this->buffer_length_ / 2;
const size_t pos = y * this->row_width_ + x / 8;
const uint8_t mask = 0x80 >> (x & 0x07); // MSB first; see fill() for plane encoding
switch (to_inkplate2_color(color)) {
case Inkplate2Color::BLACK:
this->buffer_[pos] &= ~mask;
this->buffer_[pos + half_buffer] |= mask;
break;
case Inkplate2Color::RED:
this->buffer_[pos] |= mask;
this->buffer_[pos + half_buffer] &= ~mask;
break;
case Inkplate2Color::WHITE:
default:
this->buffer_[pos] |= mask;
this->buffer_[pos + half_buffer] |= mask;
break;
}
}
bool HOT EPaperInkplate2::send_buffer_range_(size_t end, uint32_t start_time) {
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
size_t buf_idx = 0;
while (this->current_data_index_ < end) {
bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++];
if (buf_idx == sizeof bytes_to_send) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->disable();
buf_idx = 0;
if (millis() - start_time > MAX_TRANSFER_TIME)
return false; // yield; resume next loop
}
}
if (buf_idx != 0) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->disable();
}
return true;
}
bool HOT EPaperInkplate2::transfer_data() {
const uint32_t start_time = millis();
const size_t half_buffer = this->buffer_length_ / 2;
// Black/white plane (first half) then red plane (second half).
if (this->current_data_index_ == 0)
this->command(0x10);
if (this->current_data_index_ < half_buffer && !this->send_buffer_range_(half_buffer, start_time))
return false;
if (this->current_data_index_ == half_buffer)
this->command(0x13);
if (!this->send_buffer_range_(this->buffer_length_, start_time))
return false;
this->current_data_index_ = 0;
return true;
}
} // namespace esphome::epaper_spi
@@ -0,0 +1,33 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
// Soldered Inkplate 2: 104x212 black/white/red (BWR) e-paper, UC8xxx-family controller.
class EPaperInkplate2 final : public EPaperBase {
public:
EPaperInkplate2(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) {
// Dual-plane buffer: black/white plane followed by red plane, 1 bit per pixel each.
this->buffer_length_ = this->row_width_ * this->height_ * 2;
}
void fill(Color color) override;
void clear() override;
void draw_pixel_at(int x, int y, Color color) override;
protected:
void refresh_screen(bool partial) override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
bool transfer_data() override;
// Streams buffer_[current_data_index_ .. end) in chunks; returns false if it yields on MAX_TRANSFER_TIME.
bool send_buffer_range_(size_t end, uint32_t start_time);
};
} // namespace esphome::epaper_spi
@@ -0,0 +1,52 @@
# Reference: https://github.com/SolderedElectronics/Inkplate-Arduino-library
from . import EpaperModel
class Inkplate2Model(EpaperModel):
def __init__(self, name, class_name="EPaperInkplate2", **kwargs):
super().__init__(name, class_name, **kwargs)
def get_init_sequence(self, config: dict):
width, height = self.get_dimensions(config)
return (
(0x04,), # power on
(
0x00, # panel setting
0x0F, # LUT from OTP
0x89, # temperature/boost/timing
),
(
0x61, # resolution
width, # width: 1 byte
height >> 8, # height: 2 bytes, high byte first ...
height & 0xFF, # ... then low byte
),
(
0x50, # VCOM and data interval
0x77,
),
)
# Native orientation is portrait (104x212); use `rotation: 90` for the board's landscape orientation.
inkplate2 = Inkplate2Model(
"inkplate2",
width=104,
height=212,
data_rate="10MHz",
# A full 3-color refresh takes ~20s, so don't allow updates faster than that.
minimum_update_interval="30s",
# Default GPIO pins for the on-board Inkplate 2 wiring.
reset_pin=19,
dc_pin=33,
cs_pin=15,
busy_pin={
"number": 32,
"inverted": True, # hardware: LOW=busy, HIGH=idle
"mode": {
"input": True,
"pullup": True,
},
},
)
@@ -161,3 +161,24 @@ display:
busy_pin:
allow_other_uses: true
number: GPIO4
# Soldered Inkplate 2 3-color e-paper (104x212, BWR)
- platform: epaper_spi
spi_id: spi_bus
model: inkplate2
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
lambda: |-
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));