SkillAgentSearch skills...

Epepd

An ePaper display library with Endless Possibilities (for Waveshare 3.7" ePaper HAT)

Install / Use

/learn @jrymk/Epepd
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Epepd

An ePaper display library with endless possibilities
currently only supports Waveshare 3.7" ePaper HAT

What can it do?

Functions

Like... draw stuff in 480*280 hIgH rEsOlUtIoN 4-bit 16 color wIdE cOlOr gAMut on your 4-shades-of-grey-capable ePaper display?

<img src="doc/Suletta.jpg" width="800"> <img src="doc/Suletta_macro.jpg" width="500">

source: MS Gundam - The Witch from Mercury


This library is not done, and so does this README. It's a mess, and information may be incorrect.

16 shades of grey

<img src="doc/16_shades_of_grey_tuned.jpg" width="600">

This is drawn with the EpGreyscaleDisplay function with the display mode GC16. It uses 3 update cycles to create 64 different "brightening" durations after wiping the screen black. Then from which 16 colors that most represents a greyscale are picked (manually, by eye). It can technically be done in 2 display cycles, but due to the nature of the ePaper chemistry (idk), "brightening" one unit of time from black will create a far bigger brightness difference than the difference between 62 units and 63 units.

const uint8_t EpGreyscaleDisplay::lut_64_to_16[] = {0, 1, 2, 3, 4, 5, 7, 9, 11, 13, 15, 23, 27, 31, 53, 63};

This is the lookup table I ended up using. dYou might see why doing in 2 cycles won't work as well as this does.
Also, since the black capsules are "barely below white" through this process, after a few display updates, the blacks tend to fade away (not that much though, don't worry, and it is after 40 partial updates). Even though I set VCOM voltage to 0 (so DCVCOM = VSS, so no voltage difference at all), it still does that. Starting from grey and do brightening and darkening at the same time is another option. The video below is done this way in 2 display cycles. But this method does not guarantee strictly increasing brightness, especially that the environment temperature can quite heavily affect the display, I think this is a better option.

More shades of grey? How about partial display? Same time?

Partial update with masks is also implemented. Here's a demo:

https://user-images.githubusercontent.com/39593345/213897907-6412e682-08c0-4cbb-b81f-d64d4be2cc42.mp4

(Converted oversize video with VLC, came out stretched, whatever)

{ /// draw greyscale background
    // clear screen
    EpPartialDisplay partialDisplay(epd);
    gfxBuffer.fillScreen(GFX_WHITE);
    partialDisplay.display(&gfxBuffer, EpPartialDisplay::DU2);

    // draw rectangles
    uint16_t colorCodes[4][16] = {
            {0b00000000, 0b11111111},
            {0b00000000, 0b01010101, 0b10101010, 0b11111111},
            {0b00000000, 0b00100100, 0b01001001, 0b01101101, 0b10010010, 0b10110110, 0b11011011, 0b11111111},
            {0b00000000, 0b00010001, 0b00100010, 0b00110011, 0b01000100, 0b01010101, 0b01100110, 0b01110111,
                    0b10001000, 0b10011001, 0b10101010, 0b10111011, 0b11001100, 0b11011101, 0b11101110, 0b11111111}
    };
    for (int bit = 1; bit <= 4; bit++)
        for (int seg = 0; seg < (1 << bit); seg++)
            gfxBuffer.fillRect((bit - 1) * (epd.EPD_WIDTH / 4), seg * (epd.EPD_HEIGHT / (1 << bit)), epd.EPD_WIDTH / depth, epd.EPD_HEIGHT / (1 << bit), colorCodes[bit - 1][seg] << 3);

    // draw image using greyscaleDisplay
    EpGreyscaleDisplay greyscaleDisplay(epd);
    greyscaleDisplay.display(&gfxBuffer, EpGreyscaleDisplay::GC16); // greyscale clear 16
}
delay(1000);
{ /// draw overlay menu with partial display
    // create a mask for partial display
    EpBitmapFast updateMask(280, 480, 1);
    updateMask.setBitmapShapeBlendMode(EpBitmapFast::SHAPES_ONLY); // use shapes mode to save memory space instead of bitmap
    updateMask.setRectangle(10, 40, 200, 400, 0xFF, EpShape::ADD);

    const char* str[20] = { // Clion File menu
            "New",
            "Open...",
            ...
            "Exit"
    };
    gfxBuffer.fillRect(10, 40, 200, 400, GFX_WHITE);
    int highlightedItem = 0;

    EpPartialDisplay partialDisplay(epd);
    gfxBuffer.fillRect(10, 40, 200, 400, GFX_WHITE);

    // draw menu
    gfxBuffer.setFont(&HarmonyOS_Sans_Medium8pt7b);
    for (int f = 0; f < 20; f++) {
        highlightedItem = f;
        // clear previous
        gfxBuffer.gfxUpdatedRegion.reset(); // reset the "bounds of updated pixels" after displaying
        int i = previousHighlightedItem;
        gfxBuffer.fillRect(10 + margin, 40 + margin + itemHeight * i, 200 - 2 * margin, itemHeight - 2 * margin, GFX_WHITE);
        gfxBuffer.setTextColor(GFX_BLACK);
        gfxBuffer.setCursor(10 + margin + 4, 40 + margin + itemHeight * i + 11);
        gfxBuffer.print(str[i]);
        // highlight current
        i = highlightedItem;
        gfxBuffer.fillRect(10 + margin, 40 + margin + itemHeight * i, 200 - 2 * margin, itemHeight - 2 * margin, GFX_BLACK);
        gfxBuffer.setTextColor(GFX_WHITE);
        gfxBuffer.setCursor(10 + margin + 4, 40 + margin + itemHeight * i + 11);
        gfxBuffer.print(str[i]);
        // display (with windowed update)
        partialDisplay.display(gfxBuffer, placement, EpPartialDisplay::A2, &updateMask, nullptr, &gfxBuffer.gfxUpdatedRegion);
        // we did nothing extra, just calling gfx draw functions. the region where pixels has been updated (gfxUpdatedRegion) has been determined automatically
        previousHighlightedItem = highlightedItem;
    }
}

You can now determine an update region from Adafruit_GFX functions and then pass it to the display function. For the example above, the update time is down to 18ms! Because finding out the bounds of "updated pixels" is not always simple, or require additional logic. Check the code below to see how easy it is to implement windowed update!

[epepd] Updating region X from 8 to 208, Y from 114 to 147, that is 4% of a full frame!
[epepd] EpPartialDisplay calculate lut took 17237us

Example to combine partial updates in one display update

Example pulled from jrymk/pdtvtPaper

bool greyscaleRefresh = false;
EpRegion updateRegion;
EpBitmapMono partialUpdateMask(gfxBuffer.width(), gfxBuffer.height());

void refresh() {
    if (greyscaleRefresh) {
        greyscaleDisplay.display(&gfxBuffer, epdPlacement, EpGreyscaleDisplay::GC16);
        greyscaleRefresh = false;
    }
    else {
        partialUpdateMask.setBitmapShapeBlendMode(EpBitmap::SHAPES_ONLY);
        partialDisplay.display(&gfxBuffer, epdPlacement, EpPartialDisplay::GC2_PARTIAL, &partialUpdateMask, &partialUpdateMask, &updateRegion);
    }
    updateRegion.reset();
    partialUpdateMask.clearShapes();
    gfxBuffer.gfxUpdatedRegion.reset();
}

// greyscale background
void drawBase() {
    partialDisplay.clear();

    gfxBuffer.fillScreen(COLOR_BACKGROUND);
    gfxBuffer.fillRect(DASHBOARD_STATUS_BAR_LINE_MARGIN, DASHBOARD_STATUS_BAR_HEIGHT,
                       gfxBuffer.width() - 2 * DASHBOARD_STATUS_BAR_LINE_MARGIN, DASHBOARD_STATUS_BAR_LINE_THICKNESS, COLOR_SEPARATOR);

    greyscaleRefresh = true;
}

// b/w datafields that can be partial updated
void updateIPAddressField() {
    static EpRegion prevRegion;
    // gfxUpdatedRegion should be empty now
    gfxBuffer.fillRect(prevRegion.x, prevRegion.y, prevRegion.w, prevRegion.h, COLOR_BACKGROUND);

    // discard the updated region from the background rect, because it is just prevRegion, and we want to know the region covering the new text only
    gfxBuffer.gfxUpdatedRegion.reset();

    drawText(gfxBuffer, DASHBOARD_STATUS_BAR_LINE_MARGIN + 2, DASHBOARD_STATUS_BAR_HEIGHT - 4, HALIGN_LEFT, VALIGN_BASELINE,
             COLOR_BODY, &FONT_SMALL,
             WiFi.localIP().toString().c_str());
    // gfxUpdatedRegion includes the new text only

    // add to partial update mask
    partialUpdateMask.pushShape(gfxBuffer.gfxUpdatedRegion.getEpShape()); // this includes the new text
    partialUpdateMask.pushShape(prevRegion.getEpShape()); // this includes the background rect for covering up

    // add to update region
    updateRegion.include(gfxBuffer.gfxUpdatedRegion, noPlacement); // this includes the new text
    updateRegion.include(prevRegion, noPlacement); // this includes the background rect for covering up

    // set prevRegion covering the new text only. doing the regions for the background and text separately is all for this
    prevRegion = gfxBuffer.gfxUpdatedRegion;

    // can reset after including to the global updateRegion, and allows other fields to know the smallest rect covering the updated region
    gfxBuffer.gfxUpdatedRegion.reset();
}

// other fields...

Anti-aliasing

Rough edges? With getPixelLUT custom transition function, you can send pixel data that are multisampled from neighbor pixels.

<img src="doc/antialiasing.jpg" width="600">

Yes, I know it looks terrible, especially that c... Although there's a white line that went through it.
But all you need to do is...

EpBitmap gfxBuffer(480, 280, 1); // set the Adafruit_GFX buffer
Epepd epd(gfxBuffer, EPAPER_CS, EPAPER_DC, EPAPER_RST, EPAPER_BUSY);

void setup() {
    epd.init();
    gfxBuffer.allocate(4096); // allocate the buffer in 4KB chunks so that it will fit in memory
    
    // start drawing
    epd.setTextColor(BLACK);
    epd.setCursor(10, 100);
    epd.print("Connecting to\nWiFi...");
    epd.setFont(&HarmonyOS_Sans_Medium8pt7b);
    epd.setCursor(10, 180);
    epd.print("Connecting to WiFi...");
    epd.drawLine(0, 150, 260, 9, BLACK);
    epd.drawLine(0, 10, 150, 425, WHITE);

    // display
    epd.display();
}

and for the display function override... (the actual "plugin" style classes has not been implemented yet)

void Epepd::display() {
    initDisplay();
    
    writeToDisplay([](Epepd &epepd, int16_t originX, int16_t y) {
        int blackPixels = 0;
        for (int dx = -1; dx <= 1; dx++)
            for (int dy = -1; dy <= 1; dy++)
             
View on GitHub
GitHub Stars5
CategoryDevelopment
Updated10mo ago
Forks0

Languages

C++

Security Score

62/100

Audited on May 26, 2025

No findings