27浏览
查看: 27|回复: 2

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

驴友花雕  中级技神
 楼主|

发表于 昨天 19:26

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

项目代码


  1. /*E-Paper Analog Clock with ESP32
  2. by mircemk, May 2025
  3. */
  4. #include "GxEPD2_BW.h"
  5. #include "Fonts/FreeSans9pt7b.h"
  6. #include "Fonts/FreeSansBold9pt7b.h"
  7. #include "WiFi.h"
  8. #include "esp_sntp.h"
  9. const char* TIMEZONE = "CET-1CEST,M3.5.0,M10.5.0/3";
  10. const char* SSID = "******";
  11. const char* PWD = "******";
  12. // Pin definitions
  13. #define PWR 7
  14. #define BUSY 48
  15. #define RES 47
  16. #define DC 46
  17. #define CS 45
  18. #define BUTTON_PIN 2
  19. #define INVERT_BUTTON_PIN 1  // IO1 for inversion
  20. RTC_DATA_ATTR bool useRomanNumerals = false;  // Store number style state in RTC memory
  21. RTC_DATA_ATTR bool invertedDisplay = false;   // Store display inversion state
  22. // Helper function to convert number to Roman numeral
  23. const char* toRoman(int number) {
  24.     static char roman[10];
  25.     const char* romanNumerals[] = {"I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"};
  26.     if (number >= 1 && number <= 12) {
  27.         strcpy(roman, romanNumerals[number - 1]);
  28.         return roman;
  29.     }
  30.     return "";
  31. }
  32. const char* DAYSTR[] = {
  33.   "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
  34. };
  35. // W, H flipped due to setRotation(1)
  36. const int H = GxEPD2_420_GDEY042T81::HEIGHT;  // Note: Using HEIGHT first
  37. const int W = GxEPD2_420_GDEY042T81::WIDTH;   // Using WIDTH second
  38. const int CW = W / 2;   // Center Width
  39. const int CH = H / 2;   // Center Height
  40. const int R = min(W, H) / 2 - 10;  // Radius with some margin
  41. const int BAR_WIDTH = 20;
  42. const int BAR_HEIGHT = GxEPD2_420_GDEY042T81::HEIGHT/1.3;  // Half of display height
  43. const int BAR_MARGIN = 25;   // Distance from clock edge
  44. const uint16_t WHITE = GxEPD_WHITE;
  45. const uint16_t BLACK = GxEPD_BLACK;
  46. RTC_DATA_ATTR uint16_t wakeups = 0;
  47. GxEPD2_BW<GxEPD2_420_GYE042A87, GxEPD2_420_GYE042A87::HEIGHT> epd(GxEPD2_420_GYE042A87(CS, DC, RES, BUSY));
  48. uint16_t getFgColor() {
  49.     return invertedDisplay ? WHITE : BLACK;
  50. }
  51. uint16_t getBgColor() {
  52.     return invertedDisplay ? BLACK : WHITE;
  53. }
  54. void drawDisplayFrame() {
  55.     // Outer frame
  56.     epd.drawRect(0, 0, W, H, getFgColor());
  57.    
  58.     // Inner frame (3 pixels gap)
  59.     epd.drawRect(4, 4, W-8, H-8, getFgColor());
  60. }
  61. void epdPower(int state) {
  62.   pinMode(PWR, OUTPUT);  
  63.   digitalWrite(PWR, state);  
  64. }
  65. void initDisplay() {
  66.   bool initial = wakeups == 0;
  67.   epd.init(115200, initial, 50, false);
  68.   epd.setRotation(0);  // Set rotation to 0 (90 degrees)
  69.   epd.setTextSize(1);
  70.   epd.setTextColor(getFgColor());
  71. }
  72. void setTimezone() {
  73.   setenv("TZ", TIMEZONE, 1);
  74.   tzset();
  75. }
  76. void syncTime() {
  77.   if (wakeups % 50 == 0) {
  78.     WiFi.begin(SSID, PWD);
  79.     while (WiFi.status() != WL_CONNECTED)
  80.       ;
  81.     configTzTime(TIMEZONE, "pool.ntp.org");
  82.   }
  83. }
  84. void printAt(int16_t x, int16_t y, const char* text) {
  85.   int16_t x1, y1;
  86.   uint16_t w, h;
  87.   epd.getTextBounds(text, x, y, &x1, &y1, &w, &h);
  88.   epd.setCursor(x - w / 2, y + h / 2);
  89.   epd.print(text);
  90. }
  91. void printfAt(int16_t x, int16_t y, const char* format, ...) {
  92.   static char buff[64];
  93.   va_list args;
  94.   va_start(args, format);
  95.   vsnprintf(buff, 64, format, args);
  96.   printAt(x, y, buff);
  97. }
  98. void polar2cart(float x, float y, float r, float alpha, int& cx, int& cy) {
  99.   alpha = alpha * TWO_PI / 360;
  100.   cx = int(x + r * sin(alpha));
  101.   cy = int(y - r * cos(alpha));
  102. }
  103. void checkButton() {
  104.     pinMode(BUTTON_PIN, INPUT_PULLUP);
  105.     if (digitalRead(BUTTON_PIN) == LOW) {
  106.         delay(50); // Debounce
  107.         if (digitalRead(BUTTON_PIN) == LOW) {
  108.             useRomanNumerals = !useRomanNumerals;
  109.             redrawDisplay();
  110.             while(digitalRead(BUTTON_PIN) == LOW); // Wait for button release
  111.         }
  112.     }
  113. }
  114. void checkInversionButton() {
  115.     pinMode(INVERT_BUTTON_PIN, INPUT_PULLUP);
  116.     if (digitalRead(INVERT_BUTTON_PIN) == LOW) {
  117.         delay(50); // Debounce
  118.         if (digitalRead(INVERT_BUTTON_PIN) == LOW) {
  119.             invertedDisplay = !invertedDisplay;
  120.             redrawDisplay();
  121.             while(digitalRead(INVERT_BUTTON_PIN) == LOW); // Wait for button release
  122.         }
  123.     }
  124. }
  125. void redrawDisplay() {
  126.     epd.setFullWindow();
  127.     epd.fillScreen(getBgColor());
  128.     drawDisplayFrame();
  129.     drawProgressBars();
  130.     drawClockFace();
  131.     drawClockHands();
  132.     drawDateDay();
  133.     epd.display(false);
  134. }
  135. void drawClockFace() {
  136.     int cx, cy;
  137.     epd.setFont(&FreeSansBold9pt7b);
  138.     epd.setTextColor(getFgColor());
  139.     const int FRAME_THICKNESS = 1;    // Outer frame thickness
  140.     const int FRAME_GAP = 3;          // Gap between outer and inner circles
  141.   
  142.     // Draw outer thick frame
  143.     for(int i = 0; i < FRAME_THICKNESS; i++) {
  144.         epd.drawCircle(CW, CH, R + i, getFgColor());
  145.     }
  146.   
  147.     // Draw inner circle after the gap
  148.     epd.drawCircle(CW, CH, R - FRAME_GAP, getFgColor());
  149.   
  150.     // Center dot
  151.     epd.fillCircle(CW, CH, 8, getFgColor());
  152.     // Draw hour markers and numbers
  153.     for (int h = 1; h <= 12; h++) {
  154.         float alpha = 360.0 * h / 12;
  155.         
  156.         // Move numbers slightly inward to accommodate new frame
  157.         polar2cart(CW, CH, R - 25, alpha, cx, cy);
  158.         
  159.         if (useRomanNumerals) {
  160.             const char* romanNumeral = toRoman(h);
  161.             printfAt(cx, cy, "%s", romanNumeral);
  162.         } else {
  163.             printfAt(cx, cy, "%d", h);
  164.         }
  165.         
  166.         polar2cart(CW, CH, R - 45, alpha, cx, cy);
  167.         epd.fillCircle(cx, cy, 3, getFgColor());
  168.         // Draw minute markers
  169.         for (int m = 1; m <= 12 * 5; m++) {
  170.             float alpha = 360.0 * m / (12 * 5);
  171.             polar2cart(CW, CH, R - 45, alpha, cx, cy);
  172.             epd.fillCircle(cx, cy, 2, getFgColor());
  173.         }
  174.     }
  175. }
  176. void drawTriangle(float alpha, int width, int len) {
  177.   int x0, y0, x1, y1, x2, y2;
  178.   polar2cart(CW, CH, len, alpha, x2, y2);
  179.   polar2cart(CW, CH, width, alpha - 90, x1, y1);
  180.   polar2cart(CW, CH, width, alpha + 90, x0, y0);
  181.   epd.drawTriangle(x0, y0, x1, y1, x2, y2, getFgColor());
  182. }
  183. void drawClockHands() {
  184.   struct tm t;
  185.   getLocalTime(&t);
  186.   // Calculate minute angle
  187.   float alphaM = 360.0 * (t.tm_min / 60.0);
  188.   
  189.   // Calculate hour angle with smooth movement
  190.   float hourAngle = (t.tm_hour % 12) * 30.0;
  191.   float minuteContribution = (t.tm_min / 60.0) * 30.0;
  192.   float alphaH = hourAngle + minuteContribution;
  193.   // Draw the hands
  194.   drawTriangle(alphaM, 8, R - 50);  // Minute hand
  195.   drawTriangle(alphaH, 8, R - 65);  // Hour hand
  196.   epd.fillCircle(CW, CH, 8, getFgColor()); // Center dot
  197. }
  198. void drawDateDay() {
  199.   struct tm t;
  200.   getLocalTime(&t);
  201.   
  202.   epd.setFont(&FreeSans9pt7b);
  203.   epd.setTextColor(getFgColor());
  204.   
  205.   printfAt(CW, CH+R/3, "%02d-%02d-%02d",         
  206.           t.tm_mday, t.tm_mon + 1, t.tm_year -100);
  207.   printfAt(CW, CH-R/3, "%s", DAYSTR[t.tm_wday]);   
  208. }
  209. void drawProgressBar(int x, int y, int width, int height, float percentage, const char* label) {
  210.     // Draw outer rectangle
  211.     epd.drawRect(x, y, width, height, getFgColor());
  212.     // Calculate inner area with margin
  213.     int innerX = x + 3;
  214.     int innerY = y + 3;
  215.     int innerWidth = width - 6;
  216.     int innerHeight = height - 6;
  217.     // Calculate fill height
  218.     int fillHeight = (int)(innerHeight * percentage);
  219.     int fillTop = innerY + innerHeight - fillHeight;
  220.     // First draw the filled portion
  221.     epd.fillRect(innerX, fillTop, innerWidth, fillHeight, getFgColor());
  222.     // Now draw the ticks - they'll appear correctly in both filled and empty areas
  223.     for(int i = 1; i < 4; i++) {
  224.         int tickY = innerY + (innerHeight * i / 4);
  225.         
  226.         // For each pixel in the tick line
  227.         for(int px = innerX; px < innerX + innerWidth; px++) {
  228.             // If this pixel is in the filled area, use bg color, else use fg color
  229.             uint16_t color = (tickY >= fillTop) ? getBgColor() : getFgColor();
  230.             epd.drawPixel(px, tickY, color);
  231.         }
  232.     }
  233.     // Draw label above the bar
  234.     epd.setFont(&FreeSans9pt7b);
  235.     epd.setTextColor(getFgColor());
  236.     int16_t x1, y1;
  237.     uint16_t w, h;
  238.     epd.getTextBounds(label, 0, 0, &x1, &y1, &w, &h);
  239.     epd.setCursor(x + (width - w)/2, y - 10);
  240.     epd.print(label);
  241. }
  242. void drawProgressBars() {
  243.     struct tm t;
  244.     getLocalTime(&t);
  245.     float hourProgress = (t.tm_min * 60.0f + t.tm_sec) / (60.0f * 60.0f);
  246.     float dayProgress = (t.tm_hour * 3600.0f + t.tm_min * 60.0f + t.tm_sec) / (24.0f * 3600.0f);
  247.     int leftX = BAR_MARGIN;
  248.     int leftY = (H - BAR_HEIGHT)/2;
  249.     int rightX = W - BAR_MARGIN - BAR_WIDTH;
  250.     int rightY = (H - BAR_HEIGHT)/2;
  251.     // Draw the progress bars
  252.     drawProgressBar(leftX, leftY, BAR_WIDTH, BAR_HEIGHT, hourProgress, "HOUR");
  253.     drawProgressBar(rightX, rightY, BAR_WIDTH, BAR_HEIGHT, dayProgress, "DAY");
  254.     // Add elapsed time information below the bars
  255.     epd.setFont(&FreeSans9pt7b);
  256.     epd.setTextColor(getFgColor());
  257.    
  258.     // Minutes elapsed
  259.     char minuteStr[10];
  260.     sprintf(minuteStr, "%d m", t.tm_min);
  261.     int16_t x1, y1;
  262.     uint16_t w, h;
  263.     epd.getTextBounds(minuteStr, 0, 0, &x1, &y1, &w, &h);
  264.     epd.setCursor(leftX + (BAR_WIDTH - w)/2, leftY + BAR_HEIGHT + 20);
  265.     epd.print(minuteStr);
  266.     // Hours elapsed
  267.     char hourStr[10];
  268.     sprintf(hourStr, "%d h", t.tm_hour);
  269.     epd.getTextBounds(hourStr, 0, 0, &x1, &y1, &w, &h);
  270.     epd.setCursor(rightX + (BAR_WIDTH - w)/2, rightY + BAR_HEIGHT + 20);
  271.     epd.print(hourStr);
  272. }
  273. void drawClock(const void* pv) {
  274.   static int lastMinute = -1;
  275.   
  276.   struct tm t;
  277.   getLocalTime(&t);
  278.   
  279.   // Full refresh every minute
  280.   if (lastMinute != t.tm_min || wakeups == 0) {
  281.     epd.setFullWindow();
  282.     epd.fillScreen(getBgColor());
  283.     // Draw the display frame first
  284.     drawDisplayFrame();
  285.    
  286.     // Draw progress bars first (behind clock)
  287.     drawProgressBars();
  288.    
  289.     // Draw clock elements
  290.     drawClockFace();
  291.     drawClockHands();
  292.     drawDateDay();
  293.    
  294.     lastMinute = t.tm_min;
  295.   }
  296. }
  297. void setup() {
  298.     epdPower(HIGH);
  299.     initDisplay();
  300.     setTimezone();
  301.     syncTime();
  302.     esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
  303.    
  304.     if (wakeup_reason == ESP_SLEEP_WAKEUP_EXT0) {
  305.         checkButton();
  306.     }
  307.    
  308.     if (wakeup_reason == ESP_SLEEP_WAKEUP_EXT1) {
  309.         uint64_t wakeup_pin_mask = esp_sleep_get_ext1_wakeup_status();
  310.         if (wakeup_pin_mask & (1ULL << INVERT_BUTTON_PIN)) {
  311.             checkInversionButton();
  312.         }
  313.     }
  314.    
  315.     drawClock(0);
  316.    
  317.     wakeups = (wakeups + 1) % 1000;
  318.    
  319.     epd.display(false);
  320.     epd.hibernate();
  321.     // Enable wakeup from both buttons
  322.     esp_sleep_enable_ext0_wakeup((gpio_num_t)BUTTON_PIN, LOW);
  323.     esp_sleep_enable_ext1_wakeup((1ULL << INVERT_BUTTON_PIN), ESP_EXT1_WAKEUP_ANY_LOW);
  324.    
  325.     struct tm t;
  326.     getLocalTime(&t);
  327.     uint64_t sleepTime = (60 - t.tm_sec) * 1000000ULL;
  328.    
  329.     esp_sleep_enable_timer_wakeup(sleepTime);
  330.     esp_deep_sleep_start();
  331. }
  332. void loop() {
  333. }
复制代码


回复

使用道具 举报

驴友花雕  中级技神
 楼主|

发表于 昨天 19:27

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

附录
【Arduino 动手做】使用 ESP32 构建电子纸模拟时钟
项目链接:https://www.hackster.io/mircemk/ ... ull-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/

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

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2025 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail