#include "epd.hpp" // Initialize static members #ifdef IS_BTCLOCK_REV_B Native_Pin EPDManager::EPD_DC(14); std::array EPDManager::EPD_CS = { Native_Pin(2), Native_Pin(4), Native_Pin(6), Native_Pin(10), Native_Pin(38), Native_Pin(21), Native_Pin(17) }; std::array EPDManager::EPD_BUSY = { Native_Pin(3), Native_Pin(5), Native_Pin(7), Native_Pin(9), Native_Pin(37), Native_Pin(18), Native_Pin(16) }; std::array EPDManager::EPD_RESET = { MCP23X17_Pin(mcp1, 8), MCP23X17_Pin(mcp1, 9), MCP23X17_Pin(mcp1, 10), MCP23X17_Pin(mcp1, 11), MCP23X17_Pin(mcp1, 12), MCP23X17_Pin(mcp1, 13), MCP23X17_Pin(mcp1, 14) }; #elif defined(IS_BTCLOCK_V8) Native_Pin EPDManager::EPD_DC(38); std::array EPDManager::EPD_BUSY = { MCP23X17_Pin(mcp1, 8), MCP23X17_Pin(mcp1, 9), MCP23X17_Pin(mcp1, 10), MCP23X17_Pin(mcp1, 11), MCP23X17_Pin(mcp1, 12), MCP23X17_Pin(mcp1, 13), MCP23X17_Pin(mcp1, 14), MCP23X17_Pin(mcp1, 4) }; std::array EPDManager::EPD_CS = { MCP23X17_Pin(mcp2, 8), MCP23X17_Pin(mcp2, 10), MCP23X17_Pin(mcp2, 12), MCP23X17_Pin(mcp2, 14), MCP23X17_Pin(mcp2, 0), MCP23X17_Pin(mcp2, 2), MCP23X17_Pin(mcp2, 4), MCP23X17_Pin(mcp2, 6) }; std::array EPDManager::EPD_RESET = { MCP23X17_Pin(mcp2, 9), MCP23X17_Pin(mcp2, 11), MCP23X17_Pin(mcp2, 13), MCP23X17_Pin(mcp2, 15), MCP23X17_Pin(mcp2, 1), MCP23X17_Pin(mcp2, 3), MCP23X17_Pin(mcp2, 5), MCP23X17_Pin(mcp2, 7) }; #else Native_Pin EPDManager::EPD_DC(14); std::array EPDManager::EPD_CS = { Native_Pin(2), Native_Pin(4), Native_Pin(6), Native_Pin(10), Native_Pin(33), Native_Pin(21), Native_Pin(17) }; std::array EPDManager::EPD_BUSY = { Native_Pin(3), Native_Pin(5), Native_Pin(7), Native_Pin(9), Native_Pin(37), Native_Pin(18), Native_Pin(16) }; std::array EPDManager::EPD_RESET = { MCP23X17_Pin(mcp1, 8), MCP23X17_Pin(mcp1, 9), MCP23X17_Pin(mcp1, 10), MCP23X17_Pin(mcp1, 11), MCP23X17_Pin(mcp1, 12), MCP23X17_Pin(mcp1, 13), MCP23X17_Pin(mcp1, 14) }; #endif EPDManager& EPDManager::getInstance() { static EPDManager instance; return instance; } EPDManager::EPDManager() : currentContent{} , content{} , lastFullRefresh{} , tasks{} , updateQueue{nullptr} , antonioFonts{nullptr, nullptr, nullptr} , oswaldFonts{nullptr, nullptr, nullptr} , fontSmall{nullptr} , fontBig{nullptr} , fontMedium{nullptr} , fontSatsymbol{nullptr} , bgColor{GxEPD_BLACK} , fgColor{GxEPD_WHITE} , displays{ #ifdef IS_BTCLOCK_V8 EPD_CLASS(&EPD_CS[0], &EPD_DC, &EPD_RESET[0], &EPD_BUSY[0]), EPD_CLASS(&EPD_CS[1], &EPD_DC, &EPD_RESET[1], &EPD_BUSY[1]), EPD_CLASS(&EPD_CS[2], &EPD_DC, &EPD_RESET[2], &EPD_BUSY[2]), EPD_CLASS(&EPD_CS[3], &EPD_DC, &EPD_RESET[3], &EPD_BUSY[3]), EPD_CLASS(&EPD_CS[4], &EPD_DC, &EPD_RESET[4], &EPD_BUSY[4]), EPD_CLASS(&EPD_CS[5], &EPD_DC, &EPD_RESET[5], &EPD_BUSY[5]), EPD_CLASS(&EPD_CS[6], &EPD_DC, &EPD_RESET[6], &EPD_BUSY[6]), EPD_CLASS(&EPD_CS[7], &EPD_DC, &EPD_RESET[7], &EPD_BUSY[7]) #else EPD_CLASS(&EPD_CS[0], &EPD_DC, &EPD_RESET[0], &EPD_BUSY[0]), EPD_CLASS(&EPD_CS[1], &EPD_DC, &EPD_RESET[1], &EPD_BUSY[1]), EPD_CLASS(&EPD_CS[2], &EPD_DC, &EPD_RESET[2], &EPD_BUSY[2]), EPD_CLASS(&EPD_CS[3], &EPD_DC, &EPD_RESET[3], &EPD_BUSY[3]), EPD_CLASS(&EPD_CS[4], &EPD_DC, &EPD_RESET[4], &EPD_BUSY[4]), EPD_CLASS(&EPD_CS[5], &EPD_DC, &EPD_RESET[5], &EPD_BUSY[5]), EPD_CLASS(&EPD_CS[6], &EPD_DC, &EPD_RESET[6], &EPD_BUSY[6]) #endif } { } EPDManager::~EPDManager() { // Clean up tasks for (auto& task : tasks) { if (task != nullptr) { vTaskDelete(task); } } // Clean up queue if (updateQueue != nullptr) { vQueueDelete(updateQueue); } // Clean up fonts delete antonioFonts.big; delete antonioFonts.medium; delete antonioFonts.small; delete oswaldFonts.big; delete oswaldFonts.medium; delete oswaldFonts.small; } void EPDManager::initialize() { // Load fonts based on preference String fontName = preferences.getString("fontName", DEFAULT_FONT_NAME); loadFonts(fontName); // Initialize displays std::lock_guard lockMcp(mcpMutex); for (auto& display : displays) { display.init(0, true, 30); } // Create update queue and task updateQueue = xQueueCreate(UPDATE_QUEUE_SIZE, sizeof(UpdateDisplayTaskItem)); xTaskCreate(prepareDisplayUpdateTask, "PrepareUpd", EPD_TASK_STACK_SIZE * 2, nullptr, 11, nullptr); // Create display update tasks for (size_t i = 0; i < NUM_SCREENS; i++) { auto* taskParam = new int(i); xTaskCreate(updateDisplayTask, ("EpdUpd" + String(i)).c_str(), EPD_TASK_STACK_SIZE, taskParam, 11, &tasks[i]); } // Check for storage mode (prevents burn-in) if (mcp1.read1(0) == LOW) { setForegroundColor(GxEPD_BLACK); setBackgroundColor(GxEPD_WHITE); content.fill(""); } else { // Initialize with custom text or default String customText = preferences.getString("displayText", DEFAULT_BOOT_TEXT); std::array newContent; newContent.fill(" "); for (size_t i = 0; i < std::min(customText.length(), (size_t)NUM_SCREENS); i++) { newContent[i] = String(customText[i]); } content = newContent; } setContent(content); } void EPDManager::loadFonts(const String& fontName) { if (fontName == FontNames::ANTONIO) { // Load Antonio fonts antonioFonts.big = FontLoader::loadCompressedFont(Antonio_SemiBold90pt7b_Properties); antonioFonts.medium = FontLoader::loadCompressedFont(Antonio_SemiBold40pt7b_Properties); antonioFonts.small = FontLoader::loadCompressedFont(Antonio_SemiBold20pt7b_Properties); fontBig = antonioFonts.big; fontMedium = antonioFonts.medium; fontSmall = antonioFonts.small; } else if (fontName == FontNames::OSWALD) { // Load Oswald fonts oswaldFonts.big = FontLoader::loadCompressedFont(Oswald_Medium80pt7b_Properties); oswaldFonts.medium = FontLoader::loadCompressedFont(Oswald_Medium30pt7b_Properties); oswaldFonts.small = FontLoader::loadCompressedFont(Oswald_Medium20pt7b_Properties); fontBig = oswaldFonts.big; fontMedium = oswaldFonts.medium; fontSmall = oswaldFonts.small; } fontSatsymbol = FontLoader::loadCompressedFont(Satoshi_Symbol90pt7b_Properties); } void EPDManager::forceFullRefresh() { std::fill(lastFullRefresh.begin(), lastFullRefresh.end(), 0); } void EPDManager::setContent(const std::array& newContent, bool forceUpdate) { std::lock_guard lock(updateMutex); waitUntilNoneBusy(); for (size_t i = 0; i < NUM_SCREENS; i++) { if (newContent[i].compareTo(currentContent[i]) != 0 || forceUpdate) { content[i] = newContent[i]; UpdateDisplayTaskItem dispUpdate{static_cast(i)}; xQueueSend(updateQueue, &dispUpdate, portMAX_DELAY); } } } void EPDManager::setContent(const std::array& newContent) { std::array conv; for (size_t i = 0; i < newContent.size(); ++i) { conv[i] = String(newContent[i].c_str()); } setContent(conv); } std::array EPDManager::getCurrentContent() const { return currentContent; } void EPDManager::waitUntilNoneBusy() { for (size_t i = 0; i < NUM_SCREENS; i++) { uint32_t count = 0; while (EPD_BUSY[i].digitalRead()) { count++; vTaskDelay(BUSY_RETRY_DELAY); if (count == BUSY_TIMEOUT_COUNT) { vTaskDelay(pdMS_TO_TICKS(100)); } else if (count > BUSY_TIMEOUT_COUNT + 5) { log_e("Display %d busy timeout", i); break; } } } } void EPDManager::setupDisplay(uint dispNum, const GFXfont* font) { displays[dispNum].setRotation(2); displays[dispNum].setFont(font); displays[dispNum].setTextColor(fgColor); displays[dispNum].fillScreen(bgColor); } void EPDManager::splitText(uint dispNum, const String& top, const String& bottom, bool partial) { if (preferences.getBool("verticalDesc", DEFAULT_VERTICAL_DESC) && dispNum == 0) { displays[dispNum].setRotation(1); } else { displays[dispNum].setRotation(2); } displays[dispNum].setFont(fontSmall); displays[dispNum].setTextColor(fgColor); // Top text int16_t ttbx, ttby; uint16_t ttbw, ttbh; displays[dispNum].getTextBounds(top, 0, 0, &ttbx, &ttby, &ttbw, &ttbh); uint16_t tx = ((displays[dispNum].width() - ttbw) / 2) - ttbx; uint16_t ty = ((displays[dispNum].height() - ttbh) / 2) - ttby - ttbh / 2 - 12; // Bottom text int16_t tbbx, tbby; uint16_t tbbw, tbbh; displays[dispNum].getTextBounds(bottom, 0, 0, &tbbx, &tbby, &tbbw, &tbbh); uint16_t bx = ((displays[dispNum].width() - tbbw) / 2) - tbbx; uint16_t by = ((displays[dispNum].height() - tbbh) / 2) - tbby + tbbh / 2 + 12; // Make separator as wide as the shortest text uint16_t lineWidth = (tbbw < ttbh) ? tbbw : ttbw; uint16_t lineX = round((displays[dispNum].width() - lineWidth) / 2); displays[dispNum].fillScreen(bgColor); displays[dispNum].setCursor(tx, ty); displays[dispNum].print(top); displays[dispNum].fillRoundRect(lineX, displays[dispNum].height() / 2 - 3, lineWidth, 6, 3, fgColor); displays[dispNum].setCursor(bx, by); displays[dispNum].print(bottom); } void EPDManager::showDigit(uint dispNum, char chr, bool partial, const GFXfont* font) { String str(chr); if (chr == '.') { str = "!"; } setupDisplay(dispNum, font); int16_t tbx, tby; uint16_t tbw, tbh; displays[dispNum].getTextBounds(str, 0, 0, &tbx, &tby, &tbw, &tbh); uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx; uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby; displays[dispNum].setCursor(x, y); displays[dispNum].print(str); if (chr == '.') { displays[dispNum].fillRect(0, 0, displays[dispNum].width(), round(displays[dispNum].height() * 0.67), bgColor); } } void EPDManager::showChars(uint dispNum, const String& chars, bool partial, const GFXfont* font) { setupDisplay(dispNum, font); int16_t tbx, tby; uint16_t tbw, tbh; displays[dispNum].getTextBounds(chars, 0, 0, &tbx, &tby, &tbw, &tbh); // Center the bounding box by transposition of the origin uint16_t x = ((displays[dispNum].width() - tbw) / 2) - tbx; uint16_t y = ((displays[dispNum].height() - tbh) / 2) - tby; for (size_t i = 0; i < chars.length(); i++) { char c = chars[i]; if (c == '.' || c == ',') { // For the dot, calculate its specific descent GFXglyph* dotGlyph = &font->glyph[c - font->first]; int16_t dotDescent = dotGlyph->yOffset; // Draw the dot with adjusted y-position displays[dispNum].setCursor(x, y + dotDescent + dotGlyph->height + 8); displays[dispNum].print(c); } else { // For other characters, use the original y-position displays[dispNum].setCursor(x, y); displays[dispNum].print(c); } // Move x-position for the next character x += font->glyph[c - font->first].xAdvance; } } bool EPDManager::renderIcon(uint dispNum, const String& text, bool partial) { displays[dispNum].setRotation(2); displays[dispNum].setPartialWindow(0, 0, displays[dispNum].width(), displays[dispNum].height()); displays[dispNum].fillScreen(bgColor); displays[dispNum].setTextColor(fgColor); uint iconIndex = 0; uint width = 122; uint height = 122; if (text.endsWith("rocket")) { iconIndex = 1; } else if (text.endsWith("lnbolt")) { iconIndex = 2; } else if (text.endsWith("bitaxe")) { width = 88; height = 220; iconIndex = 3; } else if (text.endsWith("miningpool")) { LogoData logo = MiningPoolStatsFetch::getInstance().getLogo(); if (logo.size == 0) { Serial.println(F("No logo found")); return false; } int x_offset = (displays[dispNum].width() - logo.width) / 2; int y_offset = (displays[dispNum].height() - logo.height) / 2; displays[dispNum].drawInvertedBitmap(x_offset, y_offset, logo.data, logo.width, logo.height, fgColor); return true; } int x_offset = (displays[dispNum].width() - width) / 2; int y_offset = (displays[dispNum].height() - height) / 2; displays[dispNum].drawInvertedBitmap(x_offset, y_offset, epd_icons_allArray[iconIndex], width, height, fgColor); return true; } void EPDManager::renderText(uint dispNum, const String& text, bool partial) { displays[dispNum].setRotation(2); displays[dispNum].setPartialWindow(0, 0, displays[dispNum].width(), displays[dispNum].height()); displays[dispNum].fillScreen(GxEPD_WHITE); displays[dispNum].setTextColor(GxEPD_BLACK); displays[dispNum].setCursor(0, 50); std::stringstream ss; ss.str(text.c_str()); std::string line; while (std::getline(ss, line, '\n')) { if (line.rfind("*", 0) == 0) { line.erase(std::remove(line.begin(), line.end(), '*'), line.end()); displays[dispNum].setFont(&FreeSansBold9pt7b); } else { displays[dispNum].setFont(&FreeSans9pt7b); } displays[dispNum].println(line.c_str()); } } void EPDManager::renderQr(uint dispNum, const String& text, bool partial) { #ifdef USE_QR // Dynamically allocate QR buffer uint8_t* qrcode = (uint8_t*)malloc(qrcodegen_BUFFER_LEN_MAX); if (!qrcode) { log_e("Failed to allocate QR buffer"); return; } uint8_t tempBuffer[800]; bool ok = qrcodegen_encodeText( text.substring(2).c_str(), tempBuffer, qrcode, qrcodegen_Ecc_LOW, qrcodegen_VERSION_MIN, qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true); if (ok) { const int size = qrcodegen_getSize(qrcode); const int padding = floor(float(displays[dispNum].width() - (size * 4)) / 2); const int paddingY = floor(float(displays[dispNum].height() - (size * 4)) / 2); displays[dispNum].setRotation(2); displays[dispNum].setPartialWindow(0, 0, displays[dispNum].width(), displays[dispNum].height()); displays[dispNum].fillScreen(GxEPD_WHITE); for (int y = 0; y < size * 4; y++) { for (int x = 0; x < size * 4; x++) { displays[dispNum].drawPixel( padding + x, paddingY + y, qrcodegen_getModule(qrcode, floor(float(x) / 4), floor(float(y) / 4)) ? GxEPD_BLACK : GxEPD_WHITE); } } } free(qrcode); #endif } int16_t EPDManager::calculateDescent(const GFXfont* font) { int16_t maxDescent = 0; for (uint16_t i = font->first; i <= font->last; i++) { GFXglyph* glyph = &font->glyph[i - font->first]; int16_t descent = glyph->yOffset; if (descent > maxDescent) { maxDescent = descent; } } return maxDescent; } void EPDManager::updateDisplayTask(void* pvParameters) noexcept { auto& instance = EPDManager::getInstance(); const int epdIndex = *(int*)pvParameters; delete (int*)pvParameters; for (;;) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); std::lock_guard lock(instance.displayMutexes[epdIndex]); { std::lock_guard lockMcp(mcpMutex); instance.displays[epdIndex].init(0, false, 40); } uint32_t count = 0; while (instance.EPD_BUSY[epdIndex].digitalRead() == HIGH || count < 10) { vTaskDelay(pdMS_TO_TICKS(100)); count++; } bool updatePartial = true; if (!instance.lastFullRefresh[epdIndex] || (millis() - instance.lastFullRefresh[epdIndex]) > (preferences.getUInt("fullRefreshMin", DEFAULT_MINUTES_FULL_REFRESH) * 60 * 1000)) { updatePartial = false; } char tries = 0; while (tries < 3) { if (instance.displays[epdIndex].displayWithReturn(updatePartial)) { instance.displays[epdIndex].powerOff(); instance.currentContent[epdIndex] = instance.content[epdIndex]; if (!updatePartial) { instance.lastFullRefresh[epdIndex] = millis(); } if (eventSourceTaskHandle != nullptr) { xTaskNotifyGive(eventSourceTaskHandle); } break; } vTaskDelay(pdMS_TO_TICKS(100)); tries++; } } } void EPDManager::prepareDisplayUpdateTask(void* pvParameters) { auto& instance = EPDManager::getInstance(); UpdateDisplayTaskItem receivedItem; for (;;) { if (xQueueReceive(instance.updateQueue, &receivedItem, portMAX_DELAY)) { uint epdIndex = receivedItem.dispNum; std::lock_guard lock(instance.displayMutexes[epdIndex]); bool updatePartial = true; if (instance.content[epdIndex].length() > 1 && strstr(instance.content[epdIndex].c_str(), "/") != nullptr) { String top = instance.content[epdIndex].substring( 0, instance.content[epdIndex].indexOf("/")); String bottom = instance.content[epdIndex].substring( instance.content[epdIndex].indexOf("/") + 1); instance.splitText(epdIndex, top, bottom, updatePartial); } else if (instance.content[epdIndex].startsWith(F("qr"))) { instance.renderQr(epdIndex, instance.content[epdIndex], updatePartial); } else if (instance.content[epdIndex].startsWith(F("mdi"))) { if (!instance.renderIcon(epdIndex, instance.content[epdIndex], updatePartial)) { continue; } } else if (instance.content[epdIndex].length() > 5) { instance.renderText(epdIndex, instance.content[epdIndex], updatePartial); } else { if (instance.content[epdIndex].length() == 2) { instance.showChars(epdIndex, instance.content[epdIndex], updatePartial, instance.fontBig); } else if (instance.content[epdIndex].length() > 1 && instance.content[epdIndex].indexOf(".") == -1) { if (instance.content[epdIndex].equals("STS")) { instance.showDigit(epdIndex, 'S', updatePartial, instance.fontSatsymbol); } else { instance.showChars(epdIndex, instance.content[epdIndex], updatePartial, instance.fontMedium); } } else { instance.showDigit(epdIndex, instance.content[epdIndex].c_str()[0], updatePartial, instance.fontBig); } } xTaskNotifyGive(instance.tasks[epdIndex]); } } }