mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 18:18:43 +00:00
[epaper_spi] Add support for the Inkplate 2 (#16856)
This commit is contained in:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user