驴友花雕 发表于 前天 19:24

【Arduino 动手做】使用 ESP32 构建电子纸模拟时钟

基于 ESP32 打造的低功耗电子纸时钟,具有罗马/阿拉伯数字切换、实时进度条和分分钟更新功能。

在我之前的几个项目中,你可以看到各种各样不同寻常的时钟,包括几款复古风格的模拟时钟。这次,我将向你展示这个系列中的另一款时钟,但它是电子纸显示屏上的。具体来说,在这个项目中,我使用了CrowPanel ESP32 4.2 英寸电子纸显示屏模块,内置 ESP32S3 MCU。

这个显示器是我之前项目中用到的,我可以告诉你,它非常实用,无需连接元件和焊接,而且它有多个IO端口、一个microSD卡槽、多个按钮,甚至还有一个电池充电电路。这个项目的灵感来源于makerguides网站,所以我对基本代码做了一些修改和补充。

这些变化包括:
针对上述显示模块调整代码,
将方向从垂直更改为水平
纠正部分刷新导致的残留“鬼影”打印
每 60 秒(经过的分钟)屏幕完全刷新一次,在此期间颜色会短暂反转,从而呈现出良好的视觉和信息效果
与原始代码不同,时针现在连续移动,并与经过的分钟数成比例
并且,时钟外框加厚,其参数可以在代码中更改
当然,我添加了几个新选项,除了视觉之外,它们还具有非常有用的信息特性,我将在时钟操作描述中解释它们的功能。

新功能:
两个进度条以图形方式显示已用时间,每个进度条分为四个间隔,
有关当天已用小时数以及当前小时数的数字信息,
使用按钮在阿拉伯数字和罗马数字之间更改钟面。
而且只需按一下按钮,就可以选择反转颜色。

至于代码,如您所见,它的设计方式允许您轻松更改基本的图形参数,因此您可以根据自己的想法轻松创建自定义外观的钟面。

值得一提的是,确切时间是根据您居住的时区通过互联网下载的。其他时区定义请查看Posix 时区数据库。您还需要输入本地 Wi-Fi 网络的凭证。

现在让我们看看该设备在实际情况下是如何工作的。开机后,需要等待一段时间,时钟才能连接到Wi-Fi并下载正确的时间。然后,时钟会以模拟方式显示在白色背景上。它会显示正确的时间、星期几以及以日/月/年格式显示的完整日期。

时钟两侧各有两个进度条。右侧进度条以图形形式显示当天已用时间,下方进度条则以数值形式显示该时间。同样,左侧进度条也以图形和数值形式显示当前小时已用时间。为了更直观地显示已用时间,两个进度条被分为四个部分,右侧进度条代表 6 小时,左侧进度条代表 15 分钟。

正如我之前提到的,显示模块包含多个按钮,因此我使用了其中两个按钮来提供更多选项。按下上方的按钮,指示小时的数字就会从阿拉伯数字转换为罗马数字。

再次按下按钮,它们将恢复到原始状态。现在,按下下方按钮,显示屏的颜色将反转,背景为黑色,小时为白色。

在讲解过程中,您可能会注意到屏幕会在新的一分钟开始的时刻准确刷新,这带来了额外的视觉和信息效果。考虑到显示屏刷新时间非常短(每分钟一次),电池续航时间非常长。

最后来个简短的总结。这是一款低功耗电子纸模拟时钟,具有 Wi-Fi 时间同步、可反转显示、罗马/阿拉伯数字切换、实时进度条和分分钟更新等智能功能,基于 ESP32 显示模块构建,即插即用。















驴友花雕 发表于 前天 19:26

【Arduino 动手做】使用 ESP32 构建电子纸模拟时钟

项目代码


/*E-Paper Analog Clock with ESP32
by mircemk, May 2025
*/

#include "GxEPD2_BW.h"
#include "Fonts/FreeSans9pt7b.h"
#include "Fonts/FreeSansBold9pt7b.h"
#include "WiFi.h"
#include "esp_sntp.h"

const char* TIMEZONE = "CET-1CEST,M3.5.0,M10.5.0/3";
const char* SSID = "******";
const char* PWD = "******";

// Pin definitions
#define PWR 7
#define BUSY 48
#define RES 47
#define DC 46
#define CS 45
#define BUTTON_PIN 2
#define INVERT_BUTTON_PIN 1// IO1 for inversion

RTC_DATA_ATTR bool useRomanNumerals = false;// Store number style state in RTC memory
RTC_DATA_ATTR bool invertedDisplay = false;   // Store display inversion state

// Helper function to convert number to Roman numeral
const char* toRoman(int number) {
    static char roman;
    const char* romanNumerals[] = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"};
    if (number >= 1 && number <= 12) {
      strcpy(roman, romanNumerals);
      return roman;
    }
    return "";
}

const char* DAYSTR[] = {
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
};

// W, H flipped due to setRotation(1)
const int H = GxEPD2_420_GDEY042T81::HEIGHT;// Note: Using HEIGHT first
const int W = GxEPD2_420_GDEY042T81::WIDTH;   // Using WIDTH second

const int CW = W / 2;   // Center Width
const int CH = H / 2;   // Center Height
const int R = min(W, H) / 2 - 10;// Radius with some margin

const int BAR_WIDTH = 20;
const int BAR_HEIGHT = GxEPD2_420_GDEY042T81::HEIGHT/1.3;// Half of display height
const int BAR_MARGIN = 25;   // Distance from clock edge

const uint16_t WHITE = GxEPD_WHITE;
const uint16_t BLACK = GxEPD_BLACK;

RTC_DATA_ATTR uint16_t wakeups = 0;
GxEPD2_BW<GxEPD2_420_GYE042A87, GxEPD2_420_GYE042A87::HEIGHT> epd(GxEPD2_420_GYE042A87(CS, DC, RES, BUSY));

uint16_t getFgColor() {
    return invertedDisplay ? WHITE : BLACK;
}

uint16_t getBgColor() {
    return invertedDisplay ? BLACK : WHITE;
}

void drawDisplayFrame() {
    // Outer frame
    epd.drawRect(0, 0, W, H, getFgColor());
   
    // Inner frame (3 pixels gap)
    epd.drawRect(4, 4, W-8, H-8, getFgColor());
}

void epdPower(int state) {
pinMode(PWR, OUTPUT);
digitalWrite(PWR, state);
}

void initDisplay() {
bool initial = wakeups == 0;
epd.init(115200, initial, 50, false);
epd.setRotation(0);// Set rotation to 0 (90 degrees)
epd.setTextSize(1);
epd.setTextColor(getFgColor());
}

void setTimezone() {
setenv("TZ", TIMEZONE, 1);
tzset();
}

void syncTime() {
if (wakeups % 50 == 0) {
    WiFi.begin(SSID, PWD);
    while (WiFi.status() != WL_CONNECTED)
      ;
    configTzTime(TIMEZONE, "pool.ntp.org");
}
}

void printAt(int16_t x, int16_t y, const char* text) {
int16_t x1, y1;
uint16_t w, h;
epd.getTextBounds(text, x, y, &x1, &y1, &w, &h);
epd.setCursor(x - w / 2, y + h / 2);
epd.print(text);
}

void printfAt(int16_t x, int16_t y, const char* format, ...) {
static char buff;
va_list args;
va_start(args, format);
vsnprintf(buff, 64, format, args);
printAt(x, y, buff);
}

void polar2cart(float x, float y, float r, float alpha, int& cx, int& cy) {
alpha = alpha * TWO_PI / 360;
cx = int(x + r * sin(alpha));
cy = int(y - r * cos(alpha));
}

void checkButton() {
    pinMode(BUTTON_PIN, INPUT_PULLUP);
    if (digitalRead(BUTTON_PIN) == LOW) {
      delay(50); // Debounce
      if (digitalRead(BUTTON_PIN) == LOW) {
            useRomanNumerals = !useRomanNumerals;
            redrawDisplay();
            while(digitalRead(BUTTON_PIN) == LOW); // Wait for button release
      }
    }
}

void checkInversionButton() {
    pinMode(INVERT_BUTTON_PIN, INPUT_PULLUP);
    if (digitalRead(INVERT_BUTTON_PIN) == LOW) {
      delay(50); // Debounce
      if (digitalRead(INVERT_BUTTON_PIN) == LOW) {
            invertedDisplay = !invertedDisplay;
            redrawDisplay();
            while(digitalRead(INVERT_BUTTON_PIN) == LOW); // Wait for button release
      }
    }
}

void redrawDisplay() {
    epd.setFullWindow();
    epd.fillScreen(getBgColor());
    drawDisplayFrame();
    drawProgressBars();
    drawClockFace();
    drawClockHands();
    drawDateDay();
    epd.display(false);
}

void drawClockFace() {
    int cx, cy;
    epd.setFont(&FreeSansBold9pt7b);
    epd.setTextColor(getFgColor());

    const int FRAME_THICKNESS = 1;    // Outer frame thickness
    const int FRAME_GAP = 3;          // Gap between outer and inner circles

    // Draw outer thick frame
    for(int i = 0; i < FRAME_THICKNESS; i++) {
      epd.drawCircle(CW, CH, R + i, getFgColor());
    }

    // Draw inner circle after the gap
    epd.drawCircle(CW, CH, R - FRAME_GAP, getFgColor());

    // Center dot
    epd.fillCircle(CW, CH, 8, getFgColor());

    // Draw hour markers and numbers
    for (int h = 1; h <= 12; h++) {
      float alpha = 360.0 * h / 12;
      
      // Move numbers slightly inward to accommodate new frame
      polar2cart(CW, CH, R - 25, alpha, cx, cy);
      
      if (useRomanNumerals) {
            const char* romanNumeral = toRoman(h);
            printfAt(cx, cy, "%s", romanNumeral);
      } else {
            printfAt(cx, cy, "%d", h);
      }
      
      polar2cart(CW, CH, R - 45, alpha, cx, cy);
      epd.fillCircle(cx, cy, 3, getFgColor());

      // Draw minute markers
      for (int m = 1; m <= 12 * 5; m++) {
            float alpha = 360.0 * m / (12 * 5);
            polar2cart(CW, CH, R - 45, alpha, cx, cy);
            epd.fillCircle(cx, cy, 2, getFgColor());
      }
    }
}

void drawTriangle(float alpha, int width, int len) {
int x0, y0, x1, y1, x2, y2;
polar2cart(CW, CH, len, alpha, x2, y2);
polar2cart(CW, CH, width, alpha - 90, x1, y1);
polar2cart(CW, CH, width, alpha + 90, x0, y0);
epd.drawTriangle(x0, y0, x1, y1, x2, y2, getFgColor());
}

void drawClockHands() {
struct tm t;
getLocalTime(&t);

// Calculate minute angle
float alphaM = 360.0 * (t.tm_min / 60.0);

// Calculate hour angle with smooth movement
float hourAngle = (t.tm_hour % 12) * 30.0;
float minuteContribution = (t.tm_min / 60.0) * 30.0;
float alphaH = hourAngle + minuteContribution;

// Draw the hands
drawTriangle(alphaM, 8, R - 50);// Minute hand
drawTriangle(alphaH, 8, R - 65);// Hour hand
epd.fillCircle(CW, CH, 8, getFgColor()); // Center dot
}

void drawDateDay() {
struct tm t;
getLocalTime(&t);

epd.setFont(&FreeSans9pt7b);
epd.setTextColor(getFgColor());

printfAt(CW, CH+R/3, "%02d-%02d-%02d",         
          t.tm_mday, t.tm_mon + 1, t.tm_year -100);
printfAt(CW, CH-R/3, "%s", DAYSTR);   
}

void drawProgressBar(int x, int y, int width, int height, float percentage, const char* label) {
    // Draw outer rectangle
    epd.drawRect(x, y, width, height, getFgColor());

    // Calculate inner area with margin
    int innerX = x + 3;
    int innerY = y + 3;
    int innerWidth = width - 6;
    int innerHeight = height - 6;

    // Calculate fill height
    int fillHeight = (int)(innerHeight * percentage);
    int fillTop = innerY + innerHeight - fillHeight;

    // First draw the filled portion
    epd.fillRect(innerX, fillTop, innerWidth, fillHeight, getFgColor());

    // Now draw the ticks - they'll appear correctly in both filled and empty areas
    for(int i = 1; i < 4; i++) {
      int tickY = innerY + (innerHeight * i / 4);
      
      // For each pixel in the tick line
      for(int px = innerX; px < innerX + innerWidth; px++) {
            // If this pixel is in the filled area, use bg color, else use fg color
            uint16_t color = (tickY >= fillTop) ? getBgColor() : getFgColor();
            epd.drawPixel(px, tickY, color);
      }
    }

    // Draw label above the bar
    epd.setFont(&FreeSans9pt7b);
    epd.setTextColor(getFgColor());
    int16_t x1, y1;
    uint16_t w, h;
    epd.getTextBounds(label, 0, 0, &x1, &y1, &w, &h);
    epd.setCursor(x + (width - w)/2, y - 10);
    epd.print(label);
}
void drawProgressBars() {
    struct tm t;
    getLocalTime(&t);

    float hourProgress = (t.tm_min * 60.0f + t.tm_sec) / (60.0f * 60.0f);
    float dayProgress = (t.tm_hour * 3600.0f + t.tm_min * 60.0f + t.tm_sec) / (24.0f * 3600.0f);

    int leftX = BAR_MARGIN;
    int leftY = (H - BAR_HEIGHT)/2;

    int rightX = W - BAR_MARGIN - BAR_WIDTH;
    int rightY = (H - BAR_HEIGHT)/2;

    // Draw the progress bars
    drawProgressBar(leftX, leftY, BAR_WIDTH, BAR_HEIGHT, hourProgress, "HOUR");
    drawProgressBar(rightX, rightY, BAR_WIDTH, BAR_HEIGHT, dayProgress, "DAY");

    // Add elapsed time information below the bars
    epd.setFont(&FreeSans9pt7b);
    epd.setTextColor(getFgColor());
   
    // Minutes elapsed
    char minuteStr;
    sprintf(minuteStr, "%d m", t.tm_min);
    int16_t x1, y1;
    uint16_t w, h;
    epd.getTextBounds(minuteStr, 0, 0, &x1, &y1, &w, &h);
    epd.setCursor(leftX + (BAR_WIDTH - w)/2, leftY + BAR_HEIGHT + 20);
    epd.print(minuteStr);

    // Hours elapsed
    char hourStr;
    sprintf(hourStr, "%d h", t.tm_hour);
    epd.getTextBounds(hourStr, 0, 0, &x1, &y1, &w, &h);
    epd.setCursor(rightX + (BAR_WIDTH - w)/2, rightY + BAR_HEIGHT + 20);
    epd.print(hourStr);
}

void drawClock(const void* pv) {
static int lastMinute = -1;

struct tm t;
getLocalTime(&t);

// Full refresh every minute
if (lastMinute != t.tm_min || wakeups == 0) {
    epd.setFullWindow();
    epd.fillScreen(getBgColor());

    // Draw the display frame first
    drawDisplayFrame();
   
    // Draw progress bars first (behind clock)
    drawProgressBars();
   
    // Draw clock elements
    drawClockFace();
    drawClockHands();
    drawDateDay();
   
    lastMinute = t.tm_min;
}
}

void setup() {
    epdPower(HIGH);
    initDisplay();
    setTimezone();
    syncTime();

    esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
   
    if (wakeup_reason == ESP_SLEEP_WAKEUP_EXT0) {
      checkButton();
    }
   
    if (wakeup_reason == ESP_SLEEP_WAKEUP_EXT1) {
      uint64_t wakeup_pin_mask = esp_sleep_get_ext1_wakeup_status();
      if (wakeup_pin_mask & (1ULL << INVERT_BUTTON_PIN)) {
            checkInversionButton();
      }
    }
   
    drawClock(0);
   
    wakeups = (wakeups + 1) % 1000;
   
    epd.display(false);
    epd.hibernate();

    // Enable wakeup from both buttons
    esp_sleep_enable_ext0_wakeup((gpio_num_t)BUTTON_PIN, LOW);
    esp_sleep_enable_ext1_wakeup((1ULL << INVERT_BUTTON_PIN), ESP_EXT1_WAKEUP_ANY_LOW);
   
    struct tm t;
    getLocalTime(&t);
    uint64_t sleepTime = (60 - t.tm_sec) * 1000000ULL;
   
    esp_sleep_enable_timer_wakeup(sleepTime);
    esp_deep_sleep_start();
}

void loop() {
}

驴友花雕 发表于 前天 19:27

【Arduino 动手做】使用 ESP32 构建电子纸模拟时钟

附录
【Arduino 动手做】使用 ESP32 构建电子纸模拟时钟
项目链接:https://www.hackster.io/mircemk/building-an-e-paper-analog-clock-with-esp32-full-tutorial-c3e2f3
项目作者:北马其顿 米尔塞姆克(Mirko Pavleski)

项目视频 :https://www.youtube.com/watch?v=BUBnaO2A57o&t=4s
项目代码:https://www.hackster.io/code_files/668599/download
参考资料:https://www.makerguides.com/analog-clock-e-paper-esp32/
https://www.makerguides.com/digital-clock-e-paper-esp32/



页: [1]
查看完整版本: 【Arduino 动手做】使用 ESP32 构建电子纸模拟时钟