Continuing with claude.ai to better work with the -paper displays, this project was to modify the library to allow partial refresh of a small portion of the screen and produce a world clock and grayline display.
All the trial runs I made were unsuccessful at producing the partial refresh yet when I reached out to BuyDisplay they said yes, the display supports partial refresh. They also provided some C-code examples, so I fed that to Claude and after several itrations and modifications, a working version was produced where the UTC and Local time parts of the display were updated every second and then the whole display was updated every 10th refresh. Full refreshes are required after a set number of partial refreshes to keep the image from becoming fuzzy. For my display it was after 10 partial updates, but other displays might be different.
This project was done entirely using claude.ai and “vibe” coding – entering text descriptions of the desired outcome, feeding any errors back in for Claude to made modifications and then describing any additions/changes.
A new library was created based on the previous one from “e-paper nametag” with modifications to support the partial screen updates. Here’s the new files:
EPD128x250_FastRefresh.cpp
/*
* EPD128x250_FastRefresh - E-Paper Display Library Implementation
* With UC8251 DU Partial Refresh Support
* FIXED VERSION - Proper rotation support
*/
#include "EPD128x250_FastRefresh.h"
// GC waveform LUT tables (full refresh - 800ms with flicker)
const uint8_t EPD128x250_FastRefresh::LUT_VCOM_GC[] = {
0x01, 0x00, 0x14, 0x14, 0x01, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
const uint8_t EPD128x250_FastRefresh::LUT_WW_GC[] = {
0x01, 0x60, 0x14, 0x14, 0x01, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
const uint8_t EPD128x250_FastRefresh::LUT_BW_GC[] = {
0x01, 0x60, 0x14, 0x14, 0x01, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
const uint8_t EPD128x250_FastRefresh::LUT_WB_GC[] = {
0x01, 0x90, 0x14, 0x14, 0x01, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
const uint8_t EPD128x250_FastRefresh::LUT_BB_GC[] = {
0x01, 0x90, 0x14, 0x14, 0x01, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// DU waveform LUT tables (partial refresh - 400ms, no flicker)
const uint8_t EPD128x250_FastRefresh::LUT_VCOM_DU[] = {
0x01, 0x00, 0x14, 0x01, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
const uint8_t EPD128x250_FastRefresh::LUT_WW_DU[] = {
0x01, 0x20, 0x14, 0x01, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
const uint8_t EPD128x250_FastRefresh::LUT_BW_DU[] = {
0x01, 0x80, 0x14, 0x01, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
const uint8_t EPD128x250_FastRefresh::LUT_WB_DU[] = {
0x01, 0x40, 0x14, 0x01, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
const uint8_t EPD128x250_FastRefresh::LUT_BB_DU[] = {
0x01, 0x00, 0x14, 0x01, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// 5x7 font
const uint8_t EPD128x250_FastRefresh::font5x7[] PROGMEM = {
0x00, 0x00, 0x00, 0x00, 0x00, // Space
0x00, 0x00, 0x5F, 0x00, 0x00, // !
0x00, 0x07, 0x00, 0x07, 0x00, // "
0x14, 0x7F, 0x14, 0x7F, 0x14, // #
0x24, 0x2A, 0x7F, 0x2A, 0x12, // $
0x23, 0x13, 0x08, 0x64, 0x62, // %
0x36, 0x49, 0x55, 0x22, 0x50, // &
0x00, 0x05, 0x03, 0x00, 0x00, // '
0x00, 0x1C, 0x22, 0x41, 0x00, // (
0x00, 0x41, 0x22, 0x1C, 0x00, // )
0x14, 0x08, 0x3E, 0x08, 0x14, // *
0x08, 0x08, 0x3E, 0x08, 0x08, // +
0x00, 0x50, 0x30, 0x00, 0x00, // ,
0x08, 0x08, 0x08, 0x08, 0x08, // -
0x00, 0x60, 0x60, 0x00, 0x00, // .
0x20, 0x10, 0x08, 0x04, 0x02, // /
0x3E, 0x51, 0x49, 0x45, 0x3E, // 0
0x00, 0x42, 0x7F, 0x40, 0x00, // 1
0x42, 0x61, 0x51, 0x49, 0x46, // 2
0x21, 0x41, 0x45, 0x4B, 0x31, // 3
0x18, 0x14, 0x12, 0x7F, 0x10, // 4
0x27, 0x45, 0x45, 0x45, 0x39, // 5
0x3C, 0x4A, 0x49, 0x49, 0x30, // 6
0x01, 0x71, 0x09, 0x05, 0x03, // 7
0x36, 0x49, 0x49, 0x49, 0x36, // 8
0x06, 0x49, 0x49, 0x29, 0x1E, // 9
0x00, 0x36, 0x36, 0x00, 0x00, // :
0x00, 0x56, 0x36, 0x00, 0x00, // ;
0x08, 0x14, 0x22, 0x41, 0x00, // <
0x14, 0x14, 0x14, 0x14, 0x14, // =
0x00, 0x41, 0x22, 0x14, 0x08, // >
0x02, 0x01, 0x51, 0x09, 0x06, // ?
0x32, 0x49, 0x79, 0x41, 0x3E, // @
0x7E, 0x11, 0x11, 0x11, 0x7E, // A
0x7F, 0x49, 0x49, 0x49, 0x36, // B
0x3E, 0x41, 0x41, 0x41, 0x22, // C
0x7F, 0x41, 0x41, 0x22, 0x1C, // D
0x7F, 0x49, 0x49, 0x49, 0x41, // E
0x7F, 0x09, 0x09, 0x09, 0x01, // F
0x3E, 0x41, 0x49, 0x49, 0x7A, // G
0x7F, 0x08, 0x08, 0x08, 0x7F, // H
0x00, 0x41, 0x7F, 0x41, 0x00, // I
0x20, 0x40, 0x41, 0x3F, 0x01, // J
0x7F, 0x08, 0x14, 0x22, 0x41, // K
0x7F, 0x40, 0x40, 0x40, 0x40, // L
0x7F, 0x02, 0x0C, 0x02, 0x7F, // M
0x7F, 0x04, 0x08, 0x10, 0x7F, // N
0x3E, 0x41, 0x41, 0x41, 0x3E, // O
0x7F, 0x09, 0x09, 0x09, 0x06, // P
0x3E, 0x41, 0x51, 0x21, 0x5E, // Q
0x7F, 0x09, 0x19, 0x29, 0x46, // R
0x46, 0x49, 0x49, 0x49, 0x31, // S
0x01, 0x01, 0x7F, 0x01, 0x01, // T
0x3F, 0x40, 0x40, 0x40, 0x3F, // U
0x1F, 0x20, 0x40, 0x20, 0x1F, // V
0x3F, 0x40, 0x38, 0x40, 0x3F, // W
0x63, 0x14, 0x08, 0x14, 0x63, // X
0x07, 0x08, 0x70, 0x08, 0x07, // Y
0x61, 0x51, 0x49, 0x45, 0x43, // Z
0x00, 0x7F, 0x41, 0x41, 0x00, // [
0x02, 0x04, 0x08, 0x10, 0x20, // Backslash
0x00, 0x41, 0x41, 0x7F, 0x00, // ]
0x04, 0x02, 0x01, 0x02, 0x04, // ^
0x40, 0x40, 0x40, 0x40, 0x40, // _
0x00, 0x01, 0x02, 0x04, 0x00, // `
0x20, 0x54, 0x54, 0x54, 0x78, // a
0x7F, 0x48, 0x44, 0x44, 0x38, // b
0x38, 0x44, 0x44, 0x44, 0x20, // c
0x38, 0x44, 0x44, 0x48, 0x7F, // d
0x38, 0x54, 0x54, 0x54, 0x18, // e
0x08, 0x7E, 0x09, 0x01, 0x02, // f
0x0C, 0x52, 0x52, 0x52, 0x3E, // g
0x7F, 0x08, 0x04, 0x04, 0x78, // h
0x00, 0x44, 0x7D, 0x40, 0x00, // i
0x20, 0x40, 0x44, 0x3D, 0x00, // j
0x7F, 0x10, 0x28, 0x44, 0x00, // k
0x00, 0x41, 0x7F, 0x40, 0x00, // l
0x7C, 0x04, 0x18, 0x04, 0x78, // m
0x7C, 0x08, 0x04, 0x04, 0x78, // n
0x38, 0x44, 0x44, 0x44, 0x38, // o
0x7C, 0x14, 0x14, 0x14, 0x08, // p
0x08, 0x14, 0x14, 0x18, 0x7C, // q
0x7C, 0x08, 0x04, 0x04, 0x08, // r
0x48, 0x54, 0x54, 0x54, 0x20, // s
0x04, 0x3F, 0x44, 0x40, 0x20, // t
0x3C, 0x40, 0x40, 0x20, 0x7C, // u
0x1C, 0x20, 0x40, 0x20, 0x1C, // v
0x3C, 0x40, 0x30, 0x40, 0x3C, // w
0x44, 0x28, 0x10, 0x28, 0x44, // x
0x0C, 0x50, 0x50, 0x50, 0x3C, // y
0x44, 0x64, 0x54, 0x4C, 0x44, // z
0x00, 0x08, 0x36, 0x41, 0x00, // {
0x00, 0x00, 0x7F, 0x00, 0x00, // |
0x00, 0x41, 0x36, 0x08, 0x00, // }
0x10, 0x08, 0x08, 0x10, 0x08, // ~
};
EPD128x250_FastRefresh::EPD128x250_FastRefresh(int8_t rst, int8_t dc, int8_t cs, int8_t busy,
int8_t mosi, int8_t clk)
: _rst(rst), _dc(dc), _cs(cs), _busy(busy),
_mosi(mosi), _clk(clk), _spi(nullptr), _spiOwned(false),
_orientation(PORTRAIT), _rotation(0),
_width(EPD_WIDTH), _height(EPD_HEIGHT),
_cursor_x(0), _cursor_y(0), _text_size(1), _line_spacing(2), _text_color(true),
_lutFlag(0), _borderFlag(false) {
memset(_buffer, 0xFF, EPD_BUFFER_SIZE);
}
bool EPD128x250_FastRefresh::begin(SPIClass *spiInstance) {
pinMode(_rst, OUTPUT);
pinMode(_dc, OUTPUT);
pinMode(_cs, OUTPUT);
pinMode(_busy, INPUT);
digitalWrite(_cs, HIGH);
digitalWrite(_dc, HIGH);
digitalWrite(_rst, HIGH);
if (spiInstance != nullptr) {
_spi = spiInstance;
_spiOwned = false;
} else {
#ifdef ESP32
_spi = new SPIClass(FSPI);
_spiOwned = true;
if (_mosi >= 0 && _clk >= 0) {
_spi->begin(_clk, -1, _mosi, _cs);
} else {
_spi->begin();
}
#else
_spi = &SPI;
_spiOwned = false;
_spi->begin();
#endif
}
delay(100);
initDisplay();
return true;
}
void EPD128x250_FastRefresh::end() {
if (_spiOwned && _spi != nullptr) {
_spi->end();
delete _spi;
_spi = nullptr;
}
}
void EPD128x250_FastRefresh::reset() {
digitalWrite(_rst, HIGH);
delay(10);
digitalWrite(_rst, LOW);
delay(10);
digitalWrite(_rst, HIGH);
delay(10);
}
void EPD128x250_FastRefresh::initDisplay() {
_lutFlag = 0;
_borderFlag = false;
reset();
_spi->beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
spiWrite(0x00, true); spiWrite(0xFF);
spiWrite(0x01, true); spiWrite(0x03); spiWrite(0x00); spiWrite(0x3F); spiWrite(0x3F); spiWrite(0x03);
spiWrite(0x03, true); spiWrite(0x00);
spiWrite(0x06, true); spiWrite(0x27); spiWrite(0x27); spiWrite(0x2F);
spiWrite(0x30, true); spiWrite(0x09);
spiWrite(0x60, true); spiWrite(0x22);
spiWrite(0x82, true); spiWrite(0x00);
spiWrite(0xE3, true); spiWrite(0x00);
spiWrite(0x41, true); spiWrite(0x00);
spiWrite(0x61, true); spiWrite(0x80); spiWrite(0x00); spiWrite(0xFA);
spiWrite(0x65, true); spiWrite(0x00); spiWrite(0x00); spiWrite(0x00);
spiWrite(0x50, true); spiWrite(0xB7);
}
void EPD128x250_FastRefresh::spiWrite(uint8_t data, bool isCommand) {
digitalWrite(_dc, isCommand ? LOW : HIGH);
digitalWrite(_cs, LOW);
_spi->transfer(data);
digitalWrite(_cs, HIGH);
if (!isCommand) {
delayMicroseconds(1);
}
}
void EPD128x250_FastRefresh::waitBusy(uint32_t timeout_ms) {
unsigned long start = millis();
while (digitalRead(_busy) == LOW) {
if (millis() - start > timeout_ms) {
return;
}
delay(10);
}
}
bool EPD128x250_FastRefresh::isBusy() {
return digitalRead(_busy) == LOW;
}
void EPD128x250_FastRefresh::loadGCLUT() {
spiWrite(0x20, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_VCOM_GC[i]);
spiWrite(0x21, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_WW_GC[i]);
spiWrite(0x24, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_BB_GC[i]);
if (_lutFlag == 0) {
spiWrite(0x22, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_BW_GC[i]);
spiWrite(0x23, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_WB_GC[i]);
_lutFlag = 1;
} else {
spiWrite(0x22, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_WB_GC[i]);
spiWrite(0x23, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_BW_GC[i]);
_lutFlag = 0;
}
}
void EPD128x250_FastRefresh::loadDULUT() {
spiWrite(0x20, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_VCOM_DU[i]);
spiWrite(0x21, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_WW_DU[i]);
spiWrite(0x24, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_BB_DU[i]);
if (_lutFlag == 0) {
spiWrite(0x22, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_BW_DU[i]);
spiWrite(0x23, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_WB_DU[i]);
_lutFlag = 1;
} else {
spiWrite(0x22, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_WB_DU[i]);
spiWrite(0x23, true);
for (int i = 0; i < 56; i++) spiWrite(LUT_BW_DU[i]);
_lutFlag = 0;
}
}
void EPD128x250_FastRefresh::sendBuffer() {
spiWrite(0x13, true);
digitalWrite(_dc, HIGH);
digitalWrite(_cs, LOW);
_spi->transferBytes(_buffer, NULL, EPD_BUFFER_SIZE);
digitalWrite(_cs, HIGH);
}
void EPD128x250_FastRefresh::refreshDisplay() {
_borderFlag = true;
spiWrite(0x17, true);
spiWrite(0xA5);
waitBusy();
}
void EPD128x250_FastRefresh::clearDisplay(bool white) {
spiWrite(0x50, true);
spiWrite(0xB7);
memset(_buffer, white ? 0xFF : 0x00, EPD_BUFFER_SIZE);
sendBuffer();
loadGCLUT();
refreshDisplay();
}
void EPD128x250_FastRefresh::display(RefreshMode mode) {
if (_borderFlag) {
spiWrite(0x50, true);
spiWrite(0xD7);
} else {
spiWrite(0x50, true);
spiWrite(0xB7);
}
sendBuffer();
if (mode == FULL_REFRESH_GC) {
loadGCLUT();
} else {
loadDULUT();
}
refreshDisplay();
}
void EPD128x250_FastRefresh::displayPartialArea(int16_t x, int16_t y, int16_t w, int16_t h) {
spiWrite(0x50, true);
spiWrite(0xD7);
sendBuffer();
loadDULUT();
refreshDisplay();
}
void EPD128x250_FastRefresh::sleep() {
spiWrite(0x07, true);
spiWrite(0xA5);
}
void EPD128x250_FastRefresh::setRotation(uint8_t rotation) {
_rotation = rotation % 4;
switch (_rotation) {
case 0:
case 2:
// 0° or 180° - portrait orientation
_width = EPD_WIDTH;
_height = EPD_HEIGHT;
_orientation = PORTRAIT;
break;
case 1:
case 3:
// 90° or 270° - landscape orientation (SWAPPED dimensions)
_width = EPD_HEIGHT; // Width becomes the physical height (250)
_height = EPD_WIDTH; // Height becomes the physical width (128)
_orientation = LANDSCAPE;
break;
}
}
int16_t EPD128x250_FastRefresh::width() {
return _width;
}
int16_t EPD128x250_FastRefresh::height() {
return _height;
}
void EPD128x250_FastRefresh::fillScreen(bool black) {
memset(_buffer, black ? 0x00 : 0xFF, EPD_BUFFER_SIZE);
}
void EPD128x250_FastRefresh::drawPixel(int16_t x, int16_t y, bool black) {
// Apply rotation transformation BEFORE bounds checking
int16_t temp;
switch (_rotation) {
case 0:
// No rotation - use coordinates as-is
break;
case 1:
// 90 degrees clockwise for landscape
// Landscape (x,y) where x:0-249, y:0-127
// Maps to Portrait physical (px,py) where px:0-127, py:0-249
// Formula: (x,y) -> (y, HEIGHT-1-x) = (y, 249-x)
temp = x;
x = y;
y = EPD_HEIGHT - 1 - temp;
break;
case 2:
// 180 degrees: (x,y) -> (WIDTH-1-x, HEIGHT-1-y)
x = EPD_WIDTH - 1 - x;
y = EPD_HEIGHT - 1 - y;
break;
case 3:
// 270 degrees clockwise: (x,y) -> (HEIGHT-1-y, x)
temp = x;
x = EPD_HEIGHT - 1 - y;
y = temp;
break;
}
// Now check bounds against PHYSICAL display dimensions
if (x < 0 || x >= EPD_WIDTH || y < 0 || y >= EPD_HEIGHT) return;
// Calculate buffer position (physical coordinates)
int16_t byte_col = x / 8;
uint8_t bit_mask = 0x80 >> (x % 8);
int32_t idx = y * 16 + byte_col;
if (black) {
_buffer[idx] &= ~bit_mask;
} else {
_buffer[idx] |= bit_mask;
}
}
void EPD128x250_FastRefresh::drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, bool black) {
int16_t dx = abs(x1 - x0);
int16_t dy = abs(y1 - y0);
int16_t sx = (x0 < x1) ? 1 : -1;
int16_t sy = (y0 < y1) ? 1 : -1;
int16_t err = dx - dy;
while (true) {
drawPixel(x0, y0, black);
if (x0 == x1 && y0 == y1) break;
int16_t e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
void EPD128x250_FastRefresh::drawHLine(int16_t x, int16_t y, int16_t w, bool black) {
for (int16_t i = 0; i < w; i++) {
drawPixel(x + i, y, black);
}
}
void EPD128x250_FastRefresh::drawVLine(int16_t x, int16_t y, int16_t h, bool black) {
for (int16_t i = 0; i < h; i++) {
drawPixel(x, y + i, black);
}
}
void EPD128x250_FastRefresh::drawRect(int16_t x, int16_t y, int16_t w, int16_t h, bool black) {
drawLine(x, y, x + w - 1, y, black);
drawLine(x + w - 1, y, x + w - 1, y + h - 1, black);
drawLine(x + w - 1, y + h - 1, x, y + h - 1, black);
drawLine(x, y + h - 1, x, y, black);
}
void EPD128x250_FastRefresh::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, bool black) {
for (int16_t i = y; i < y + h; i++) {
for (int16_t j = x; j < x + w; j++) {
drawPixel(j, i, black);
}
}
}
void EPD128x250_FastRefresh::drawCircle(int16_t x0, int16_t y0, int16_t r, bool black) {
int16_t x = r;
int16_t y = 0;
int16_t err = 0;
while (x >= y) {
drawPixel(x0 + x, y0 + y, black);
drawPixel(x0 + y, y0 + x, black);
drawPixel(x0 - y, y0 + x, black);
drawPixel(x0 - x, y0 + y, black);
drawPixel(x0 - x, y0 - y, black);
drawPixel(x0 - y, y0 - x, black);
drawPixel(x0 + y, y0 - x, black);
drawPixel(x0 + x, y0 - y, black);
if (err <= 0) {
y += 1;
err += 2*y + 1;
}
if (err > 0) {
x -= 1;
err -= 2*x + 1;
}
}
}
void EPD128x250_FastRefresh::fillCircle(int16_t x0, int16_t y0, int16_t r, bool black) {
for (int16_t y = -r; y <= r; y++) {
for (int16_t x = -r; x <= r; x++) {
if (x*x + y*y <= r*r) {
drawPixel(x0 + x, y0 + y, black);
}
}
}
}
void EPD128x250_FastRefresh::setTextSize(uint8_t size) {
_text_size = (size > 0) ? size : 1;
}
void EPD128x250_FastRefresh::setCursor(int16_t x, int16_t y) {
_cursor_x = x;
_cursor_y = y;
}
void EPD128x250_FastRefresh::setTextColor(bool black) {
_text_color = black;
}
void EPD128x250_FastRefresh::setLineSpacing(uint8_t spacing) {
_line_spacing = spacing;
}
void EPD128x250_FastRefresh::drawChar(int16_t x, int16_t y, unsigned char c, bool color, uint8_t size) {
if (c < 32 || c > 126) return;
c -= 32;
for (uint8_t col = 0; col < 5; col++) {
uint8_t line = pgm_read_byte(&font5x7[c * 5 + col]);
for (uint8_t row = 0; row < 8; row++) {
if (line & 0x01) {
if (size == 1) {
drawPixel(x + col, y + row, color);
} else {
fillRect(x + col * size, y + row * size, size, size, color);
}
}
line >>= 1;
}
}
}
void EPD128x250_FastRefresh::print(const char* text) {
while (*text) {
if (*text == '\n') {
_cursor_y += (_text_size * 8) + _line_spacing;
_cursor_x = 0;
} else if (*text == '\r') {
_cursor_x = 0;
} else {
drawChar(_cursor_x, _cursor_y, *text, _text_color, _text_size);
_cursor_x += _text_size * 6;
if (_cursor_x > (_width - _text_size * 6)) {
_cursor_x = 0;
_cursor_y += (_text_size * 8) + _line_spacing;
}
}
text++;
}
}
void EPD128x250_FastRefresh::print(const String& text) {
print(text.c_str());
}
void EPD128x250_FastRefresh::println(const char* text) {
print(text);
_cursor_x = 0;
_cursor_y += (_text_size * 8) + _line_spacing;
}
void EPD128x250_FastRefresh::println(const String& text) {
println(text.c_str());
}
void EPD128x250_FastRefresh::clearBuffer() {
memset(_buffer, 0xFF, EPD_BUFFER_SIZE);
}
EPD128x250_FastRefresh.h
/*
* EPD128x250_FastRefresh - E-Paper Display Library with DU Partial Refresh
* Based on UC8251 driver with proper LUT tables for fast updates
* Separate library to preserve existing EPD128x250 functionality
*/
#ifndef EPD128X250_FASTREFRESH_H
#define EPD128X250_FASTREFRESH_H
#include <Arduino.h>
#include <SPI.h>
#define EPD_WIDTH 128
#define EPD_HEIGHT 250
#define EPD_BUFFER_SIZE 4000
enum Orientation {
PORTRAIT = 0,
LANDSCAPE = 1
};
enum RefreshMode {
FULL_REFRESH_GC = 0, // GC waveform - full refresh with flicker (800ms)
PARTIAL_REFRESH_DU = 1 // DU waveform - fast partial refresh no flicker (400ms)
};
class EPD128x250_FastRefresh {
public:
EPD128x250_FastRefresh(int8_t rst, int8_t dc, int8_t cs, int8_t busy,
int8_t mosi = -1, int8_t clk = -1);
bool begin(SPIClass *spiInstance = nullptr);
void end();
// Display control
void clearDisplay(bool white = true);
void display(RefreshMode mode = FULL_REFRESH_GC);
void displayPartialArea(int16_t x, int16_t y, int16_t w, int16_t h);
void sleep();
void setRotation(uint8_t rotation);
Orientation getOrientation() { return _orientation; }
// Drawing functions
void fillScreen(bool black = false);
void drawPixel(int16_t x, int16_t y, bool black = true);
void drawLine(int16_t x0, int16_t y0, int16_t x1, int16_t y1, bool black = true);
void drawHLine(int16_t x, int16_t y, int16_t w, bool black = true);
void drawVLine(int16_t x, int16_t y, int16_t h, bool black = true);
void drawRect(int16_t x, int16_t y, int16_t w, int16_t h, bool black = true);
void fillRect(int16_t x, int16_t y, int16_t w, int16_t h, bool black = true);
void drawCircle(int16_t x0, int16_t y0, int16_t r, bool black = true);
void fillCircle(int16_t x0, int16_t y0, int16_t r, bool black = true);
// Text functions
void setTextSize(uint8_t size);
void setCursor(int16_t x, int16_t y);
void setTextColor(bool black);
void setLineSpacing(uint8_t spacing);
void print(const char* text);
void print(const String& text);
void println(const char* text);
void println(const String& text);
int16_t getCursorX() { return _cursor_x; }
int16_t getCursorY() { return _cursor_y; }
// Buffer access
uint8_t* getBuffer() { return _buffer; }
void clearBuffer();
// Utility functions
int16_t width();
int16_t height();
bool isBusy();
private:
int8_t _rst, _dc, _cs, _busy, _mosi, _clk;
SPIClass *_spi;
bool _spiOwned;
uint8_t _buffer[EPD_BUFFER_SIZE];
Orientation _orientation;
uint8_t _rotation;
int16_t _width, _height;
int16_t _cursor_x, _cursor_y;
uint8_t _text_size;
uint8_t _line_spacing;
bool _text_color;
// LUT state tracking
uint8_t _lutFlag;
bool _borderFlag;
void reset();
void initDisplay();
void waitBusy(uint32_t timeout_ms = 15000);
void spiWrite(uint8_t data, bool isCommand = false);
void sendBuffer();
void refreshDisplay();
void loadGCLUT(); // Full refresh LUT (GC waveform)
void loadDULUT(); // Partial refresh LUT (DU waveform)
void drawChar(int16_t x, int16_t y, unsigned char c, bool color, uint8_t size);
// GC waveform LUT tables (full refresh - 800ms)
static const uint8_t LUT_VCOM_GC[56];
static const uint8_t LUT_WW_GC[56];
static const uint8_t LUT_BW_GC[56];
static const uint8_t LUT_WB_GC[56];
static const uint8_t LUT_BB_GC[56];
// DU waveform LUT tables (partial refresh - 400ms, no flicker)
static const uint8_t LUT_VCOM_DU[56];
static const uint8_t LUT_WW_DU[56];
static const uint8_t LUT_BW_DU[56];
static const uint8_t LUT_WB_DU[56];
static const uint8_t LUT_BB_DU[56];
// Font data
static const uint8_t font5x7[];
};
#endif
Add these 2 files to a folder called EPD128x250_FastRefresh in the Arduino Library folder.
Here’s the main sketch:
// World Time & Grayline Clock for ESP32-C3 SuperMini
// VERSION 4.0 - With Fast Partial Refresh for Time Updates
// Uses new EPD128x250_FastRefresh library with DU waveform
#include <SPI.h>
#include <WiFi.h>
#include <time.h>
#include <wifi_credentials.h>
#include "EPD128x250_FastRefresh.h"
#define RST_PIN 2
#define DC_PIN 3
#define CS_PIN 7
#define BUSY_PIN 1
#define MOSI_PIN 10
#define CLK_PIN 8
// ===== CONFIGURATION =====
#define SHOW_LOCATION_MARKER false // Set to false to hide the Ottawa location marker
#define FULL_REFRESH_COUNT 10 // Full refresh every N partial refreshes
// =========================
SPIClass spi(FSPI);
EPD128x250_FastRefresh display(RST_PIN, DC_PIN, CS_PIN, BUSY_PIN, MOSI_PIN, CLK_PIN);
const char* localTimezone = "EST5EDT,M3.2.0,M11.1.0";
const unsigned long FULL_UPDATE_INTERVAL = 3600000; // 1 hour
const unsigned long TIME_UPDATE_INTERVAL = 60000; // 1 minute
unsigned long lastFullUpdate = 0;
unsigned long lastTimeUpdate = 0;
int updateCount = 0;
// Time display regions for partial refresh
#define UTC_TIME_X 5
#define UTC_TIME_Y 35
#define UTC_TIME_W 118
#define UTC_TIME_H 18
#define LOCAL_TIME_X 5
#define LOCAL_TIME_Y 87
#define LOCAL_TIME_W 118
#define LOCAL_TIME_H 18
void setup() {
Serial.begin(115200);
delay(2000);
Serial.println("\n\n### World Clock v4.0 - FAST PARTIAL REFRESH ###");
Serial.println("### Using EPD128x250_FastRefresh library with DU waveform ###");
Serial.println("Starting setup...");
Serial.println("Initializing SPI...");
spi.begin(CLK_PIN, -1, MOSI_PIN, CS_PIN);
Serial.println("Initializing display...");
if (!display.begin(&spi)) {
Serial.println("Failed to initialize display!");
while (1) {
Serial.println("Display init failed - halted");
delay(1000);
}
}
Serial.println("Display initialized!");
Serial.println("Showing startup screen...");
showStartupScreen();
Serial.println("Startup screen displayed");
Serial.println("Connecting to WiFi...");
Serial.print("SSID: ");
Serial.println(ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 30) {
delay(500);
Serial.print(".");
attempts++;
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi Connected!");
Serial.print("IP: ");
Serial.println(WiFi.localIP());
// Update display to show WiFi connected
display.clearBuffer();
display.setTextSize(2);
display.setCursor(30, 50);
display.print("World");
display.setCursor(30, 70);
display.print("Clock");
display.setCursor(25, 90);
display.print("v4.0");
display.setTextSize(1);
display.setCursor(15, 120);
display.print("WiFi Connected!");
display.setCursor(15, 135);
display.print("Syncing time...");
display.display(FULL_REFRESH_GC);
Serial.println("Configuring NTP time...");
configTime(0, 0, "pool.ntp.org");
Serial.print("Waiting for NTP time sync");
int timeoutCounter = 0;
time_t now = time(nullptr);
while (now < 1000000000 && timeoutCounter < 30) {
delay(1000);
Serial.print(".");
now = time(nullptr);
timeoutCounter++;
}
Serial.println();
if (now > 1000000000) {
Serial.println("Time synchronized!");
setenv("TZ", localTimezone, 1);
tzset();
Serial.println("Starting initial full display update...");
updateDisplayFull();
lastFullUpdate = millis();
lastTimeUpdate = millis();
updateCount = 0;
Serial.println("Setup complete!");
} else {
Serial.println("WARNING: Time sync failed!");
showErrorScreen("Time Sync Failed");
}
} else {
Serial.println("WiFi connection failed!");
showErrorScreen("WiFi Failed");
}
Serial.println("Entering main loop...");
}
void loop() {
static int loopCount = 0;
loopCount++;
if (loopCount % 10 == 0) {
Serial.print("Loop: ");
Serial.print(loopCount);
Serial.print(", uptime: ");
Serial.print(millis() / 1000);
Serial.println("s");
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected! Reconnecting...");
WiFi.begin(ssid, password);
delay(5000);
return;
}
unsigned long currentMillis = millis();
// Full refresh every hour OR every 10 partial updates
if (currentMillis - lastFullUpdate > FULL_UPDATE_INTERVAL || updateCount >= FULL_REFRESH_COUNT) {
updateCount = 0;
Serial.println("=== PERFORMING FULL REFRESH (GC waveform) ===");
updateDisplayFull();
lastFullUpdate = currentMillis;
lastTimeUpdate = currentMillis;
Serial.println("Full update complete!");
}
// Fast partial refresh for time updates
else if (currentMillis - lastTimeUpdate > TIME_UPDATE_INTERVAL) {
updateCount++;
Serial.printf("=== FAST TIME UPDATE #%d (DU waveform - 400ms) ===\n", updateCount);
unsigned long startTime = millis();
updateTimeOnlyFast();
unsigned long elapsed = millis() - startTime;
lastTimeUpdate = currentMillis;
Serial.printf("Fast update complete in %lu ms!\n", elapsed);
}
delay(1000);
}
void showStartupScreen() {
display.clearBuffer();
display.setTextSize(2);
display.setCursor(30, 50);
display.print("World");
display.setCursor(30, 70);
display.print("Clock");
display.setCursor(25, 90);
display.print("v4.0");
display.setTextSize(1);
display.setCursor(20, 120);
display.print("Fast Refresh");
display.display(FULL_REFRESH_GC);
}
void showErrorScreen(const char* error) {
display.clearBuffer();
display.setTextSize(2);
display.setCursor(20, 50);
display.print("ERROR:");
display.setTextSize(1);
display.setCursor(20, 80);
display.print(error);
display.display(FULL_REFRESH_GC);
}
void updateTimeOnlyFast() {
time_t now;
time(&now);
struct tm utcTime;
gmtime_r(&now, &utcTime);
struct tm localTime;
localtime_r(&now, &localTime);
char buf[32];
// Clear only the time regions (not the full screen)
display.fillRect(UTC_TIME_X, UTC_TIME_Y, UTC_TIME_W, UTC_TIME_H, false);
display.fillRect(LOCAL_TIME_X, LOCAL_TIME_Y, LOCAL_TIME_W, LOCAL_TIME_H, false);
// Draw UTC time
snprintf(buf, sizeof(buf), "%02d:%02d",
utcTime.tm_hour, utcTime.tm_min);
display.setTextSize(2);
display.setCursor(UTC_TIME_X, UTC_TIME_Y);
display.print(buf);
// Draw local time
snprintf(buf, sizeof(buf), "%02d:%02d",
localTime.tm_hour, localTime.tm_min);
display.setTextSize(2);
display.setCursor(LOCAL_TIME_X, LOCAL_TIME_Y);
display.print(buf);
// Use DU partial refresh - fast, no flicker, ~400ms
display.display(PARTIAL_REFRESH_DU);
}
void updateDisplayFull() {
display.clearBuffer();
time_t now;
time(&now);
struct tm utcTime;
gmtime_r(&now, &utcTime);
struct tm localTime;
localtime_r(&now, &localTime);
char buf[32];
// Header
display.setTextSize(1);
display.setCursor(5, 5);
display.print("WORLD TIME CLOCK");
display.drawHLine(0, 16, display.width());
// UTC section
display.setCursor(5, 22);
display.print("UTC TIME");
snprintf(buf, sizeof(buf), "%02d:%02d",
utcTime.tm_hour, utcTime.tm_min);
display.setTextSize(2);
display.setCursor(UTC_TIME_X, UTC_TIME_Y);
display.print(buf);
snprintf(buf, sizeof(buf), "%02d %s %04d",
utcTime.tm_mday,
(const char*[]){"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}[utcTime.tm_mon],
utcTime.tm_year + 1900);
display.setTextSize(1);
display.setCursor(5, 55);
display.print(buf);
display.drawHLine(0, 68, display.width());
// Local time section
bool isDST = localTime.tm_isdst > 0;
snprintf(buf, sizeof(buf), "LOCAL TIME %s", isDST ? "EDT" : "EST");
display.setCursor(5, 74);
display.print(buf);
snprintf(buf, sizeof(buf), "%02d:%02d",
localTime.tm_hour, localTime.tm_min);
display.setTextSize(2);
display.setCursor(LOCAL_TIME_X, LOCAL_TIME_Y);
display.print(buf);
display.drawHLine(0, 105, display.width());
// Grayline map section
display.setTextSize(1);
display.setCursor(5, 111);
display.print("GRAYLINE STATUS");
drawGraylineMap(utcTime);
display.drawHLine(0, 190, display.width());
// DX conditions
display.setCursor(5, 196);
display.print("DX CONDITIONS");
int utcHourInt = utcTime.tm_hour;
bool inGrayline = (utcHourInt >= 10 && utcHourInt <= 14) ||
(utcHourInt >= 22 || utcHourInt <= 2);
display.setCursor(5, 210);
if (inGrayline) {
display.print("Status: GOOD");
display.setCursor(5, 223);
display.print("*** Enhanced ***");
} else {
display.print("Status: NORMAL");
display.setCursor(5, 223);
display.print("Standard propag.");
}
int nextWindow;
if (utcHourInt < 10) {
nextWindow = 10 - utcHourInt;
} else if (utcHourInt < 22) {
nextWindow = 22 - utcHourInt;
} else {
nextWindow = 34 - utcHourInt;
}
snprintf(buf, sizeof(buf), "Next window: %dh", nextWindow);
display.setCursor(5, 236);
display.print(buf);
// Use GC full refresh for complete screen update
display.display(FULL_REFRESH_GC);
Serial.println("Full display updated with GC waveform!");
}
void drawGraylineMap(struct tm &utcTime) {
Serial.println(">>> Drawing grayline map <<<");
int mapX = 10;
int mapY = 135;
int mapW = 108;
int mapH = 50;
display.drawRect(mapX, mapY, mapW, mapH);
// Continents (simplified positions)
int naX = mapX + 15;
display.drawRect(naX, mapY + 8, 20, 15);
display.drawRect(naX + 1, mapY + 9, 18, 13);
int saX = mapX + 30;
display.drawRect(saX, mapY + 28, 12, 15);
display.drawRect(saX + 1, mapY + 29, 10, 13);
int euX = mapX + 51;
display.drawRect(euX, mapY + 10, 12, 10);
display.drawRect(euX + 1, mapY + 11, 10, 8);
int afX = mapX + 48;
display.drawRect(afX, mapY + 22, 20, 20);
display.drawRect(afX + 1, mapY + 23, 18, 18);
int asX = mapX + 66;
display.drawRect(asX, mapY + 8, 30, 18);
display.drawRect(asX + 1, mapY + 9, 28, 16);
int auX = mapX + 87;
display.drawRect(auX, mapY + 32, 16, 10);
display.drawRect(auX + 1, mapY + 33, 14, 8);
// Equator and latitude lines
int equatorY = mapY + mapH / 2;
display.drawHLine(mapX, equatorY, mapW);
int lat30N = mapY + mapH / 4;
int lat30S = mapY + 3 * mapH / 4;
for (int x = mapX; x < mapX + mapW; x += 3) {
display.drawPixel(x, lat30N);
display.drawPixel(x, lat30S);
}
// Calculate solar position
double utcHour = utcTime.tm_hour + utcTime.tm_min / 60.0;
int dayOfYear = utcTime.tm_yday + 1;
double declination = -23.44 * cos(2.0 * PI * (dayOfYear + 10) / 365.25);
double solarLon = (12.0 - utcHour) * 15.0;
while (solarLon > 180.0) solarLon -= 360.0;
while (solarLon < -180.0) solarLon += 360.0;
Serial.printf("Solar: UTC=%.2fh, Day=%d, Decl=%.1f°, SolarLon=%.1f°\n",
utcHour, dayOfYear, declination, solarLon);
// Draw terminator
double sinDecl = sin(declination * PI / 180.0);
double cosDecl = cos(declination * PI / 180.0);
for (int x = 0; x < mapW; x++) {
double lon = -180.0 + (x * 360.0 / mapW);
double hourAngle = lon - solarLon;
while (hourAngle > 180.0) hourAngle -= 360.0;
while (hourAngle < -180.0) hourAngle += 360.0;
for (int y = 0; y < mapH; y += 2) {
double lat = 90.0 - (y * 180.0 / mapH);
double sinLat = sin(lat * PI / 180.0);
double cosLat = cos(lat * PI / 180.0);
double cosHA = cos(hourAngle * PI / 180.0);
double sinElev = sinLat * sinDecl + cosLat * cosDecl * cosHA;
double elevation = asin(sinElev) * 180.0 / PI;
// Draw terminator line (day/night boundary)
if (elevation > -1.0 && elevation < 1.0) {
display.drawPixel(mapX + x, mapY + y);
if (x + 1 < mapW) display.drawPixel(mapX + x + 1, mapY + y);
if (y + 1 < mapH) display.drawPixel(mapX + x, mapY + y + 1);
if (x + 1 < mapW && y + 1 < mapH) display.drawPixel(mapX + x + 1, mapY + y + 1);
}
// Shade night side
else if (elevation < 0) {
if ((x + y) % 2 == 0) {
display.drawPixel(mapX + x, mapY + y);
if (y + 1 < mapH) display.drawPixel(mapX + x, mapY + y + 1);
}
}
}
}
// Prime meridian markers
int primeMeridian = mapX + mapW / 2;
display.drawVLine(primeMeridian, mapY, 4);
display.drawVLine(primeMeridian, mapY + mapH - 4, 4);
#if SHOW_LOCATION_MARKER
// Mark Ottawa's position for debug
double ottawaLat = 45.4;
double ottawaLon = -75.7;
double ottawaMapX = ((ottawaLon + 180.0) / 360.0) * mapW;
double ottawaMapY = ((90.0 - ottawaLat) / 180.0) * mapH;
int ottX = mapX + (int)ottawaMapX;
int ottY = mapY + (int)ottawaMapY;
// Draw large + marker at Ottawa (5x5 pixels)
for (int i = -2; i <= 2; i++) {
display.drawPixel(ottX + i, ottY, true);
display.drawPixel(ottX, ottY + i, true);
}
// Draw circle around it
display.drawPixel(ottX - 3, ottY, true);
display.drawPixel(ottX + 3, ottY, true);
display.drawPixel(ottX, ottY - 3, true);
display.drawPixel(ottX, ottY + 3, true);
display.drawPixel(ottX - 2, ottY - 2, true);
display.drawPixel(ottX + 2, ottY - 2, true);
display.drawPixel(ottX - 2, ottY + 2, true);
display.drawPixel(ottX + 2, ottY + 2, true);
Serial.printf("Ottawa marker at X=%d, Y=%d\n", ottX, ottY);
#else
Serial.println("Location marker disabled");
#endif
Serial.println("<<< Map complete >>>");
}
Remember to have the Arduino IDE set to use the ESP32 devices (ESP32C3 Dev Module in my case for the ESP32C3 super mini.
Here’s the final result:

The map portion of the display shows the major continents in order to represent day and night in the world. With the low resolution and size of the screen, this works for me.
Did Claude get it right the first time? Absolutely not! I had to keep prompting in order to get a final result that worked. This took several days to get to this result. Could the code be better? Probably, but it would have been very challenging for me to code this from scratch so I’m very happy with the result.
I have a few more projects lined up for the e-paper display and also for a small OLED that I have so stay tuned!