ESPHome 2026.3.0-dev
Loading...
Searching...
No Matches
epaper_weact_3c.cpp
Go to the documentation of this file.
1#include "epaper_weact_3c.h"
2#include "esphome/core/log.h"
3
4namespace esphome::epaper_spi {
5
6static constexpr const char *const TAG = "epaper_weact_3c";
7
8enum class BwrState : uint8_t {
11 BWR_RED,
12};
13
14static BwrState color_to_bwr(Color color) {
15 if (color.r > color.g + color.b && color.r > 127) {
16 return BwrState::BWR_RED;
17 }
18 if (color.r + color.g + color.b >= 382) {
20 }
22}
23// SSD1680 3-color display notes:
24// - Buffer uses 1 bit per pixel, 8 pixels per byte
25// - Buffer first half (black_offset): Black/White plane (0=black, 1=white)
26// - Buffer second half (red_offset): Red plane (1=red, 0=no red)
27// - Total buffer: width * height / 4 bytes = 2 * (width * height / 8)
28// - For 128x296: 128*296/4 = 9472 bytes total (4736 per color)
29
30void EPaperWeAct3C::draw_pixel_at(int x, int y, Color color) {
31 if (!this->rotate_coordinates_(x, y))
32 return;
33
34 // Calculate position in the 1-bit buffer
35 const uint32_t pos = (x / 8) + (y * this->row_width_);
36 const uint8_t bit = 0x80 >> (x & 0x07);
37 const uint32_t red_offset = this->buffer_length_ / 2u;
38
39 // Use luminance threshold for B/W mapping
40 // Split at halfway point (382 = (255*3)/2)
41 auto bwr = color_to_bwr(color);
42
43 // Update black/white plane (first half of buffer)
44 if (bwr == BwrState::BWR_WHITE) {
45 // White pixel - set bit in black plane
46 this->buffer_[pos] |= bit;
47 } else {
48 // Black pixel - clear bit in black plane
49 this->buffer_[pos] &= ~bit;
50 }
51
52 // Update red plane (second half of buffer)
53 // Red if red component is dominant (r > g+b)
54 if (bwr == BwrState::BWR_RED) {
55 // Red pixel - set bit in red plane
56 this->buffer_[red_offset + pos] |= bit;
57 } else {
58 // Not red - clear bit in red plane
59 this->buffer_[red_offset + pos] &= ~bit;
60 }
61}
62
64 // For 3-color e-paper with 1-bit buffer format:
65 // - Black buffer: 1=black, 0=white
66 // - Red buffer: 1=red, 0=no red
67 // The buffer is stored as two halves: [black plane][red plane]
68 const size_t half_buffer = this->buffer_length_ / 2u;
69
70 // Use luminance threshold for B/W mapping
71 auto bits = color_to_bwr(color);
72
73 // Fill both planes
74 if (bits == BwrState::BWR_BLACK) {
75 // Black - both planes = 0x00
76 this->buffer_.fill(0x00);
77 } else if (bits == BwrState::BWR_RED) {
78 // Red - black plane = 0x00, red plane = 0xFF
79 for (size_t i = 0; i < half_buffer; i++)
80 this->buffer_[i] = 0x00;
81 for (size_t i = 0; i < half_buffer; i++)
82 this->buffer_[half_buffer + i] = 0xFF;
83 } else {
84 // White - black plane = 0xFF, red plane = 0x00
85 for (size_t i = 0; i < half_buffer; i++)
86 this->buffer_[i] = 0xFF;
87 for (size_t i = 0; i < half_buffer; i++)
88 this->buffer_[half_buffer + i] = 0x00;
89 }
90}
91
93 // Clear buffer to white, just like real paper.
94 this->fill(COLOR_ON);
95}
96
98 // For full screen refresh, we always start from (0,0)
99 // The y_low_/y_high_ values track the dirty region for optimization,
100 // but for display refresh we need to write from the beginning
101 uint16_t x_start = 0;
102 uint16_t x_end = this->width_ - 1;
103 uint16_t y_start = 0;
104 uint16_t y_end = this->height_ - 1; // height = 296 for 2.9" display
105
106 // Set RAM X address boundaries (0x44)
107 // X coordinates are byte-aligned (divided by 8)
108 this->cmd_data(0x44, {(uint8_t) (x_start / 8), (uint8_t) (x_end / 8)});
109
110 // Set RAM Y address boundaries (0x45)
111 // Format: Y start (LSB, MSB), Y end (LSB, MSB)
112 this->cmd_data(0x45, {(uint8_t) y_start, (uint8_t) (y_start >> 8), (uint8_t) (y_end & 0xFF), (uint8_t) (y_end >> 8)});
113
114 // Reset RAM X counter to start (0x4E) - 1 byte
115 this->cmd_data(0x4E, {(uint8_t) (x_start / 8)});
116
117 // Reset RAM Y counter to start (0x4F) - 2 bytes (LSB, MSB)
118 this->cmd_data(0x4F, {(uint8_t) y_start, (uint8_t) (y_start >> 8)});
119}
120
122 const uint32_t start_time = millis();
123 const size_t buffer_length = this->buffer_length_;
124 const size_t half_buffer = buffer_length / 2u;
125
126 ESP_LOGV(TAG, "transfer_data: buffer_length=%u, half_buffer=%u", buffer_length, half_buffer);
127
128 // Use a local buffer for SPI transfers
129 uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
130
131 // First, send the RED buffer (0x26 = WRITE_COLOR)
132 // The red plane is in the second half of our buffer
133 // NOTE: Must set RAM window first to reset address counters!
134 if (this->current_data_index_ < half_buffer) {
135 if (this->current_data_index_ == 0) {
136 ESP_LOGV(TAG, "transfer_data: sending RED buffer (0x26)");
137 this->set_window_(); // Reset RAM X/Y counters to start position
138 this->command(0x26);
139 }
140
141 this->start_data_();
142 size_t red_offset = half_buffer;
143 while (this->current_data_index_ < half_buffer) {
144 size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, half_buffer - this->current_data_index_);
145
146 for (size_t i = 0; i < bytes_to_copy; i++) {
147 bytes_to_send[i] = this->buffer_[red_offset + this->current_data_index_ + i];
148 }
149
150 this->write_array(bytes_to_send, bytes_to_copy);
151
152 this->current_data_index_ += bytes_to_copy;
153
154 if (millis() - start_time > MAX_TRANSFER_TIME) {
155 // Let the main loop run and come back next loop
156 this->disable();
157 return false;
158 }
159 }
160 this->disable();
161 }
162
163 // Finished the red buffer, now send the BLACK buffer (0x24 = WRITE_BLACK)
164 // The black plane is in the first half of our buffer
165 if (this->current_data_index_ < buffer_length) {
166 if (this->current_data_index_ == half_buffer) {
167 ESP_LOGV(TAG, "transfer_data: finished red buffer, sending BLACK buffer (0x24)");
168
169 // Do NOT reset RAM counters here for WeAct displays (Reference implementation behavior)
170 // this->set_window();
171 this->command(0x24);
172 // Continue using current_data_index_, but we need to map it to the start of the buffer
173 }
174
175 this->start_data_();
176 while (this->current_data_index_ < buffer_length) {
177 size_t remaining = buffer_length - this->current_data_index_;
178 size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, remaining);
179
180 // Calculate offset into the BLACK buffer (which is at the start of this->buffer_)
181 // current_data_index_ goes from half_buffer to buffer_length
182 size_t buffer_offset = this->current_data_index_ - half_buffer;
183
184 for (size_t i = 0; i < bytes_to_copy; i++) {
185 bytes_to_send[i] = this->buffer_[buffer_offset + i];
186 }
187
188 this->write_array(bytes_to_send, bytes_to_copy);
189
190 this->current_data_index_ += bytes_to_copy;
191
192 if (millis() - start_time > MAX_TRANSFER_TIME) {
193 // Let the main loop run and come back next loop
194 this->disable();
195 return false;
196 }
197 }
198 this->disable();
199 }
200
201 this->current_data_index_ = 0;
202 ESP_LOGV(TAG, "transfer_data: completed (red=%u, black=%u bytes)", half_buffer, half_buffer);
203 return true;
204}
205
207 // SSD1680 refresh sequence:
208 // Reset RAM X/Y address counters to 0,0 so display reads from start
209 // 0x4E: RAM X counter - 1 byte (X / 8)
210 // 0x4F: RAM Y counter - 2 bytes (Y LSB, Y MSB)
211 this->cmd_data(0x4E, {0x00}); // RAM X counter = 0 (1 byte)
212 this->cmd_data(0x4F, {0x00, 0x00}); // RAM Y counter = 0 (2 bytes)
213
214 // Send UPDATE_FULL command (0x22) with display update control parameter
215 // Both WeAct and waveshare reference use 0xF7: {0x22, 0xF7}
216 // 0xF7 = Display update: Load temperature, Load LUT, Enable RAM content
217 this->cmd_data(0x22, {0xF7}); // Command 0x22 with parameter 0xF7
218 this->command(0x20); // Activate display update
219
220 // COMMAND TERMINATE FRAME READ WRITE (required by SSD1680)
221 // Removed 0xFF based on working reference implementation
222 // this->command(0xFF);
223}
224
226 // Power on sequence - send command to turn on power
227 // According to SSD1680 spec: 0x22, 0xF8 powers on the display
228 this->cmd_data(0x22, {0xF8}); // Power on
229 this->command(0x20); // Activate
230}
231
233 // Power off sequence - send command to turn off power
234 // According to SSD1680 spec: 0x22, 0x83 powers off the display
235 this->cmd_data(0x22, {0x83}); // Power off
236 this->command(0x20); // Activate
237}
238
240 // Deep sleep sequence
241 this->cmd_data(0x10, {0x01}); // Deep sleep mode
242}
243
244} // namespace esphome::epaper_spi
void command(uint8_t value)
bool rotate_coordinates_(int &x, int &y)
Check and rotate coordinates based on the transform flags.
split_buffer::SplitBuffer buffer_
Definition epaper_spi.h:166
void cmd_data(uint8_t command, const uint8_t *ptr, size_t length)
void fill(Color color) override
void draw_pixel_at(int x, int y, Color color) override
void refresh_screen(bool partial) override
void fill(uint8_t value) const
Fill the entire buffer with a single byte value.
constexpr Color COLOR_ON(255, 255, 255, 255)
Turn the pixel ON.
size_t size_t pos
Definition helpers.h:854
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
uint8_t g
Definition color.h:34
uint8_t b
Definition color.h:38
uint8_t r
Definition color.h:30
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6