|
|
CrowPanel 2.1-inch HMI ESP32 Rotary Display |
x 1 | |
|
|
PCF8574 I2C I/O expander |
x 1 | |
|
|
Buzzer |
x 1 |
|
arduino IDEArduino
|
|
|
Soldering Iron Kit |
DIY ESP32 Universal Timer with Touchscreen & Rotary Encoder
In several of my previous videos, I presented different projects based on the CrowPanel 2.1-inch HMI ESP32 Rotary Display. This compact and very practical module combines a circular color touchscreen, an ESP32-S3 microcontroller, a rotary encoder with a push button, and an I2C connector for communicating with external devices. This combination of features makes it an ideal platform for my latest project — a Universal Timer with a clean, user-friendly interface and a very simple and intuitive way of setting the desired time.
This project is sponsored by PCBWay . Don't forget to visit the PCBWay Open Source Community where you will find countless useful projects divided into different categories. Choose your area of interest where you will find many useful and free projects. You can here also download Gerber files or order ready-made PCBs for some of your future projects. PCBWay has all the services you need to create your project at the best price.

The main idea was to create a timer that can be operated almost entirely with the rotary encoder. Turning the encoder sets the desired time, while a short press of the button starts, pauses, or resumes the countdown. A long press immediately resets the timer to zero. The circular display is used as a visual representation of the remaining time. The red area represents the selected or remaining time, while the white area gradually increases during the countdown. Thanks to framebuffer-based screen drawing, all graphic changes are smooth and free from unpleasant flickering. Four timer ranges are available: 1 minute, 10 minutes, 30 minutes, and 60 minutes.

The range can be changed simply by touching the lower part of the display. Each range also has its own scale color: yellow for 1 minute, turquoise for 10 minutes, orange for 30 minutes, and magenta for 60 minutes. The scale automatically changes according to the selected range, making it easy to estimate the remaining time even without reading the numerical display. For additional indication, I connected an external PCF8574 I2C I/O expander and an active buzzer.


When the countdown reaches zero, the display flashes red three times while the buzzer produces three synchronized beeps. The result is a compact and practical timer that is very easy to operate and can be useful in the workshop, kitchen, laboratory, photography, electronics experiments, or anywhere else where a simple visual countdown timer is needed.
A few recommendations regarding the installation of the code.

To avoid a black screen or unpleasant flashing, it is necessary to install ESP32 Core ver 2.0.17 on the Arduino, and the Arduino_GFX v1.6.1 library. On the ESP32 S3, the flash size should be set to 16MB, Partition Scheme - Huge APP, and PSRAM - OPI psram.
It can be noted that most of the parameters are defined at the beginning of the code, so various modifications can be made very easily, for example, changing the colors or geometry. As usual, the complete Arduino code and additional information about the project can be found in the description below the video.
Now let's see how the device performs in real conditions.

And finally a short conclusion. This project shows how the CrowPanel 2.1-inch ESP32-S3 display can be transformed into a compact, intuitive, and highly practical universal timer with both visual and audible indication.
/*==============================================================
TIMER V1.0 FINAL by mircemk Oct. 2026
CrowPanel 2.1" ESP32-S3 Rotary Display
480 x 480
ESP32 Core : 2.0.17
Arduino_GFX : 1.4.4
TIMER RANGES
------------------------------------------------------------
1 MIN - Yellow
10 MIN - Turquoise / Blue-Green
30 MIN - Orange
60 MIN - Magenta
TOUCH
------------------------------------------------------------
Lower-center touch zone:
1 MIN -> 10 MIN -> 30 MIN -> 60 MIN -> 1 MIN
ENCODER
------------------------------------------------------------
Stable SLOW-CLEAN polling algorithm
CLK = GPIO4
DT = GPIO42
SHORT CLICK:
START / STOP / RESUME
LONG CLICK (~1.2 s):
RESET -> 00:00
Current range remains selected
BUZZER
------------------------------------------------------------
External PCF8574 = 0x20
P0 -> buzzer (-)
buzzer (+) -> 3.3V
P0 LOW = buzzer ON
P0 HIGH = buzzer OFF
TIMER FINISHED
------------------------------------------------------------
RED + BEEP 500 ms
WHITE + SILENT 500 ms
repeated 3 times
==============================================================*/
#include <Arduino.h>
#include <Arduino_GFX_Library.h>
#include <Wire.h>
#include <Adafruit_CST8XX.h>
#include <math.h>
// ============================================================
// DISPLAY
// ============================================================
#define DISPLAY_WIDTH 480
#define DISPLAY_HEIGHT 480
#define CX 240
#define CY 240
// ============================================================
// DISPLAY PINS
// ============================================================
#define PCLK_NEG 1
#define BL_PIN 6
#define PANEL_CS 16
#define PANEL_SCK 2
#define PANEL_SDA 1
// ============================================================
// ROTARY ENCODER
// ============================================================
#define ENCODER_CLK 4
#define ENCODER_DT 42
// ============================================================
// I2C
// ============================================================
#define I2C_SDA 38
#define I2C_SCL 39
// Built-in PCF8574
#define INTERNAL_PCF_ADDR 0x21
// External PCF8574
#define EXTERNAL_PCF_ADDR 0x20
// Touch
#define TOUCH_ADDR 0x15
// ============================================================
// BUZZER
// External PCF8574 P0
// ============================================================
#define BUZZER_BIT 0
uint8_t externalPcfState = 0xFF;
// ============================================================
// DISPLAY OBJECTS
// ============================================================
Arduino_DataBus *panelBus = nullptr;
Arduino_ESP32RGBPanel *rgbpanel = nullptr;
Arduino_RGB_Display *display = nullptr;
Arduino_Canvas *gfx = nullptr;
extern const uint8_t st7701_type7_init_operations[];
// ============================================================
// TOUCH
// ============================================================
Adafruit_CST8XX touch = Adafruit_CST8XX();
bool touchWasDown = false;
bool rangeTouchCandidate = false;
unsigned long lastTouchPollMillis = 0;
const unsigned long TOUCH_POLL_MS = 20;
// ============================================================
// COLORS
// ============================================================
#define COL_BLACK 0x0000
#define COL_WHITE 0xFFFF
#define COL_RED 0xF800
// Range colors
#define COL_1MIN 0xFFE0 // Yellow
#define COL_10MIN 0x06F7 // Turquoise
#define COL_30MIN 0xFD20 // Orange
#define COL_60MIN 0xF81F // Magenta
// ============================================================
// TIMER GEOMETRY
// ============================================================
const int OUTER_RADIUS = 235;
const int SEPARATOR_RADIUS = 203;
const int INNER_RADIUS = 200;
const int TICK_OUTER_RADIUS = 228;
const int TICK_MINOR_INNER = 216;
const int TICK_MAJOR_INNER = 211;
const int NUMBER_RADIUS = 174;
// ============================================================
// TOUCH ZONE FOR RANGE SELECTION
// ============================================================
const int RANGE_TOUCH_X1 = 115;
const int RANGE_TOUCH_X2 = 365;
const int RANGE_TOUCH_Y1 = 290;
const int RANGE_TOUCH_Y2 = 435;
// ============================================================
// TIMER RANGES
// ============================================================
enum TimerRange
{
RANGE_1_MIN,
RANGE_10_MIN,
RANGE_30_MIN,
RANGE_60_MIN
};
TimerRange currentRange = RANGE_1_MIN;
// ============================================================
// TIMER VALUE / STATE
// ============================================================
int remainingSeconds = 0;
bool timerRunning = false;
bool countdownHasStarted = false;
unsigned long lastSecondMillis = 0;
// ============================================================
// STABLE ENCODER SETTINGS
// ============================================================
const int ENCODER_DIRECTION = -1;
const unsigned long STABLE_TIME_MS = 8;
const unsigned long CLICK_LOCKOUT_MS = 70;
// ============================================================
// ENCODER STATE
// ============================================================
int lastRawCLK = HIGH;
int stableCLK = HIGH;
unsigned long clkChangeTime = 0;
unsigned long lastClickTime = 0;
bool readyForNextClick = true;
// ============================================================
// BUTTON STATE
// ============================================================
bool rawButtonLast = false;
bool stableButtonState = false;
unsigned long buttonDebounceMillis = 0;
const unsigned long BUTTON_DEBOUNCE_MS = 40;
const unsigned long BUTTON_POLL_MS = 5;
unsigned long lastButtonPollMillis = 0;
// ============================================================
// LONG PRESS
// ============================================================
const unsigned long LONG_PRESS_MS = 1200;
unsigned long buttonPressStartMillis = 0;
bool longPressHandled = false;
// ============================================================
// RANGE HELPERS
// ============================================================
int getMaxSeconds()
{
switch (currentRange)
{
case RANGE_1_MIN:
return 60;
case RANGE_10_MIN:
return 600;
case RANGE_30_MIN:
return 1800;
case RANGE_60_MIN:
return 3600;
}
return 60;
}
// ============================================================
// ENCODER STEP
// ============================================================
int getEncoderStepSeconds()
{
switch (currentRange)
{
case RANGE_1_MIN:
// 60 fields
return 1;
case RANGE_10_MIN:
// 50 fields
// 600 / 50 = 12 sec
return 12;
case RANGE_30_MIN:
// 30 fields
// 1800 / 30 = 60 sec
return 60;
case RANGE_60_MIN:
// 60 fields
// 3600 / 60 = 60 sec
return 60;
}
return 1;
}
// ============================================================
// RANGE COLOR
// ============================================================
uint16_t getRingColor()
{
switch (currentRange)
{
case RANGE_1_MIN:
return COL_1MIN;
case RANGE_10_MIN:
return COL_10MIN;
case RANGE_30_MIN:
return COL_30MIN;
case RANGE_60_MIN:
return COL_60MIN;
}
return COL_1MIN;
}
// ============================================================
// RANGE TEXT
// ============================================================
const char *getRangeText()
{
switch (currentRange)
{
case RANGE_1_MIN:
return "1 MIN";
case RANGE_10_MIN:
return "10 MIN";
case RANGE_30_MIN:
return "30 MIN";
case RANGE_60_MIN:
return "60 MIN";
}
return "1 MIN";
}
// ============================================================
// POLAR COORDINATES
// ============================================================
void polarPoint(float angleDeg,
int radius,
int &x,
int &y)
{
float a =
angleDeg * PI / 180.0f;
x =
CX + round(radius * sin(a));
y =
CY - round(radius * cos(a));
}
// ============================================================
// RADIAL SCALE TICK
// ============================================================
void drawRadialTick(float angleDeg,
int innerRadius,
int outerRadius,
float thickness,
uint16_t color)
{
float a =
angleDeg * PI / 180.0f;
float rx = sin(a);
float ry = -cos(a);
float px = cos(a);
float py = sin(a);
float halfThickness =
thickness / 2.0f;
float ix =
CX + innerRadius * rx;
float iy =
CY + innerRadius * ry;
float ox =
CX + outerRadius * rx;
float oy =
CY + outerRadius * ry;
int x1 =
round(ix + px * halfThickness);
int y1 =
round(iy + py * halfThickness);
int x2 =
round(ix - px * halfThickness);
int y2 =
round(iy - py * halfThickness);
int x3 =
round(ox - px * halfThickness);
int y3 =
round(oy - py * halfThickness);
int x4 =
round(ox + px * halfThickness);
int y4 =
round(oy + py * halfThickness);
gfx->fillTriangle(
x1, y1,
x2, y2,
x3, y3,
color
);
gfx->fillTriangle(
x1, y1,
x3, y3,
x4, y4,
color
);
}
// ============================================================
// BOLD TEXT
// ============================================================
void drawBoldText(const char *text,
int x,
int y,
int textSize,
uint16_t color)
{
gfx->setTextSize(
textSize
);
gfx->setTextColor(
color
);
gfx->setCursor(x, y);
gfx->print(text);
gfx->setCursor(x + 1, y);
gfx->print(text);
gfx->setCursor(x, y + 1);
gfx->print(text);
gfx->setCursor(x + 1, y + 1);
gfx->print(text);
}
// ============================================================
// CENTERED BOLD TEXT
// ============================================================
void drawCenteredBoldText(const char *text,
int centerX,
int y,
int textSize,
uint16_t color)
{
int textWidth =
strlen(text) *
6 *
textSize;
int x =
centerX -
textWidth / 2;
drawBoldText(
text,
x,
y,
textSize,
color
);
}
// ============================================================
// GENERIC SCALE
// ============================================================
void drawGenericScale(int totalMarks,
int marksPerMajor)
{
float angleStep =
360.0f /
(float)totalMarks;
for (int i = 0;
i < totalMarks;
i++)
{
float angle =
i * angleStep;
bool major =
(i % marksPerMajor == 0);
if (major)
{
drawRadialTick(
angle,
TICK_MAJOR_INNER,
TICK_OUTER_RADIUS,
6.0f,
COL_BLACK
);
}
else
{
drawRadialTick(
angle,
TICK_MINOR_INNER,
TICK_OUTER_RADIUS,
3.0f,
COL_BLACK
);
}
}
}
// ============================================================
// DRAW SCALE
// ============================================================
void drawScale()
{
switch (currentRange)
{
case RANGE_1_MIN:
drawGenericScale(
60,
5
);
break;
case RANGE_10_MIN:
drawGenericScale(
50,
5
);
break;
case RANGE_30_MIN:
drawGenericScale(
30,
5
);
break;
case RANGE_60_MIN:
drawGenericScale(
60,
5
);
break;
}
}
// ============================================================
// DRAW ONE SCALE NUMBER
// ============================================================
void drawScaleNumber(float angle,
int value)
{
int x;
int y;
polarPoint(
angle,
NUMBER_RADIUS,
x,
y
);
char text[5];
sprintf(
text,
"%d",
value
);
const int textSize =
3;
int textWidth =
strlen(text) *
6 *
textSize;
int textHeight =
8 *
textSize;
drawBoldText(
text,
x - textWidth / 2,
y - textHeight / 2,
textSize,
COL_BLACK
);
}
// ============================================================
// DRAW SCALE NUMBERS
// ============================================================
void drawNumbers()
{
// ==========================================================
// 1 MIN
// 0,55,50 ... 5
// ==========================================================
if (currentRange == RANGE_1_MIN)
{
for (int pos = 0;
pos < 12;
pos++)
{
float angle =
pos * 30.0f;
int value;
if (pos == 0)
{
value = 0;
}
else
{
value =
60 -
pos * 5;
}
drawScaleNumber(
angle,
value
);
}
return;
}
// ==========================================================
// 10 MIN
// 0,9,8 ... 1
// ==========================================================
if (currentRange == RANGE_10_MIN)
{
for (int pos = 0;
pos < 10;
pos++)
{
float angle =
pos * 36.0f;
int value;
if (pos == 0)
{
value = 0;
}
else
{
value =
10 -
pos;
}
drawScaleNumber(
angle,
value
);
}
return;
}
// ==========================================================
// 30 MIN
// 0,25,20,15,10,5
// ==========================================================
if (currentRange == RANGE_30_MIN)
{
for (int pos = 0;
pos < 6;
pos++)
{
float angle =
pos * 60.0f;
int value;
if (pos == 0)
{
value = 0;
}
else
{
value =
30 -
pos * 5;
}
drawScaleNumber(
angle,
value
);
}
return;
}
// ==========================================================
// 60 MIN
// 0,55,50 ... 5
// ==========================================================
if (currentRange == RANGE_60_MIN)
{
for (int pos = 0;
pos < 12;
pos++)
{
float angle =
pos * 30.0f;
int value;
if (pos == 0)
{
value = 0;
}
else
{
value =
60 -
pos * 5;
}
drawScaleNumber(
angle,
value
);
}
}
}
// ============================================================
// RED REMAINING TIME SECTOR
// ============================================================
void drawRemainingRedSector()
{
int maxSeconds =
getMaxSeconds();
if (remainingSeconds <= 0)
{
return;
}
if (remainingSeconds >= maxSeconds)
{
gfx->fillCircle(
CX,
CY,
INNER_RADIUS,
COL_RED
);
return;
}
float redAngle =
((float)remainingSeconds /
(float)maxSeconds)
*
360.0f;
float startAngle =
360.0f -
redAngle;
for (float angle = startAngle;
angle < 360.0f;
angle += 0.7f)
{
int x1;
int y1;
int x2;
int y2;
polarPoint(
angle,
INNER_RADIUS,
x1,
y1
);
polarPoint(
angle + 1.0f,
INNER_RADIUS,
x2,
y2
);
gfx->fillTriangle(
CX,
CY,
x1,
y1,
x2,
y2,
COL_RED
);
}
}
// ============================================================
// FORMAT TIMER
// ============================================================
void formatTimer(char *buffer)
{
int minutes =
remainingSeconds /
60;
int seconds =
remainingSeconds %
60;
sprintf(
buffer,
"%02d:%02d",
minutes,
seconds
);
}
// ============================================================
// COMPLETE TIMER FRAME
// ============================================================
void renderTimerFrame()
{
gfx->fillScreen(
COL_BLACK
);
// Colored range ring
gfx->fillCircle(
CX,
CY,
OUTER_RADIUS,
getRingColor()
);
// Separator
gfx->fillCircle(
CX,
CY,
SEPARATOR_RADIUS,
COL_BLACK
);
// White timer area
gfx->fillCircle(
CX,
CY,
INNER_RADIUS,
COL_WHITE
);
// Red remaining area
drawRemainingRedSector();
drawScale();
drawNumbers();
char timerText[8];
formatTimer(
timerText
);
drawCenteredBoldText(
timerText,
CX,
184,
7,
COL_BLACK
);
drawCenteredBoldText(
getRangeText(),
CX,
315,
4,
COL_BLACK
);
// Flicker-free screen update
gfx->flush();
}
// ============================================================
// ALARM FLASH FRAME
// ============================================================
void renderFlashFrame(uint16_t innerColor)
{
gfx->fillScreen(
COL_BLACK
);
gfx->fillCircle(
CX,
CY,
OUTER_RADIUS,
getRingColor()
);
gfx->fillCircle(
CX,
CY,
SEPARATOR_RADIUS,
COL_BLACK
);
gfx->fillCircle(
CX,
CY,
INNER_RADIUS,
innerColor
);
drawScale();
drawNumbers();
drawCenteredBoldText(
"00:00",
CX,
184,
7,
COL_BLACK
);
drawCenteredBoldText(
getRangeText(),
CX,
315,
4,
COL_BLACK
);
gfx->flush();
}
// ============================================================
// INTERNAL PCF8574
// Address 0x21
// ============================================================
void internalPcfWrite(uint8_t value)
{
Wire.beginTransmission(
INTERNAL_PCF_ADDR
);
Wire.write(
value
);
Wire.endTransmission();
}
uint8_t internalPcfRead()
{
Wire.requestFrom(
INTERNAL_PCF_ADDR,
(uint8_t)1
);
if (Wire.available())
{
return Wire.read();
}
return 0xFF;
}
// ============================================================
// EXTERNAL PCF8574
// Address 0x20
// ============================================================
void externalPcfWrite()
{
Wire.beginTransmission(
EXTERNAL_PCF_ADDR
);
Wire.write(
externalPcfState
);
Wire.endTransmission();
}
// ============================================================
// BUZZER CONTROL
//
// Buzzer + = 3.3V
// Buzzer - = P0
//
// LOW = ON
// HIGH = OFF
// ============================================================
void buzzerON()
{
externalPcfState &=
~(1 << BUZZER_BIT);
externalPcfWrite();
}
void buzzerOFF()
{
externalPcfState |=
(1 << BUZZER_BIT);
externalPcfWrite();
}
// ============================================================
// TOUCH RESET
// Internal PCF8574 P0
// ============================================================
void resetTouch()
{
internalPcfWrite(
0xFF
);
delay(
20
);
// P0 LOW
internalPcfWrite(
0xFE
);
delay(
30
);
// P0 HIGH
internalPcfWrite(
0xFF
);
delay(
150
);
}
// ============================================================
// ENCODER BUTTON
// Internal PCF8574 P5
// ============================================================
bool isEncoderButtonPressed()
{
uint8_t state =
internalPcfRead();
return
!(state & (1 << 5));
}
// ============================================================
// RANGE TOUCH ZONE
// ============================================================
bool isRangeTouchZone(int x,
int y)
{
return
x >= RANGE_TOUCH_X1 &&
x <= RANGE_TOUCH_X2 &&
y >= RANGE_TOUCH_Y1 &&
y <= RANGE_TOUCH_Y2;
}
// ============================================================
// RESET TIMER
//
// LONG PRESS
// ============================================================
void resetTimer()
{
timerRunning =
false;
countdownHasStarted =
false;
remainingSeconds =
0;
lastSecondMillis =
millis();
// Ensure alarm is silent
buzzerOFF();
// Re-sync encoder
lastRawCLK =
digitalRead(
ENCODER_CLK
);
stableCLK =
lastRawCLK;
clkChangeTime =
millis();
lastClickTime =
millis();
readyForNextClick =
true;
// Touch state
touchWasDown =
false;
rangeTouchCandidate =
false;
renderTimerFrame();
Serial.println(
"TIMER RESET -> 00:00"
);
}
// ============================================================
// CHANGE TIMER RANGE
// ============================================================
void changeTimerRange()
{
if (countdownHasStarted)
{
return;
}
switch (currentRange)
{
case RANGE_1_MIN:
currentRange =
RANGE_10_MIN;
break;
case RANGE_10_MIN:
currentRange =
RANGE_30_MIN;
break;
case RANGE_30_MIN:
currentRange =
RANGE_60_MIN;
break;
case RANGE_60_MIN:
currentRange =
RANGE_1_MIN;
break;
}
remainingSeconds =
0;
buzzerOFF();
// Re-sync encoder
lastRawCLK =
digitalRead(
ENCODER_CLK
);
stableCLK =
lastRawCLK;
clkChangeTime =
millis();
lastClickTime =
millis();
readyForNextClick =
true;
renderTimerFrame();
Serial.print(
"RANGE: "
);
Serial.println(
getRangeText()
);
}
// ============================================================
// TOUCH HANDLER
// ============================================================
void handleTouch()
{
unsigned long now =
millis();
if (
now -
lastTouchPollMillis
<
TOUCH_POLL_MS
)
{
return;
}
lastTouchPollMillis =
now;
uint8_t touches =
touch.touched();
// ----------------------------------------------------------
// TOUCH DOWN
// ----------------------------------------------------------
if (touches > 0)
{
CST_TS_Point p =
touch.getPoint(0);
if (!touchWasDown)
{
touchWasDown =
true;
rangeTouchCandidate =
!countdownHasStarted &&
isRangeTouchZone(
p.x,
p.y
);
}
return;
}
// ----------------------------------------------------------
// RELEASE
// ----------------------------------------------------------
if (touchWasDown)
{
touchWasDown =
false;
if (rangeTouchCandidate)
{
rangeTouchCandidate =
false;
changeTimerRange();
}
else
{
rangeTouchCandidate =
false;
}
}
}
// ============================================================
// STABLE DT MAJORITY READ
//
// BEST WORKING ENCODER METHOD
// ============================================================
int readStableDT()
{
int highCount = 0;
int lowCount = 0;
for (int i = 0;
i < 7;
i++)
{
if (
digitalRead(
ENCODER_DT
)
)
{
highCount++;
}
else
{
lowCount++;
}
delayMicroseconds(
250
);
}
return
(highCount > lowCount)
?
HIGH
:
LOW;
}
// ============================================================
// ACCEPT ENCODER CLICK
// ============================================================
void acceptEncoderClick(int direction)
{
if (countdownHasStarted)
{
return;
}
int oldValue =
remainingSeconds;
int stepSeconds =
getEncoderStepSeconds();
remainingSeconds +=
direction *
stepSeconds;
int maxSeconds =
getMaxSeconds();
if (remainingSeconds < 0)
{
remainingSeconds =
0;
}
if (remainingSeconds > maxSeconds)
{
remainingSeconds =
maxSeconds;
}
if (
remainingSeconds !=
oldValue
)
{
renderTimerFrame();
Serial.print(
"SET: "
);
Serial.print(
remainingSeconds
);
Serial.print(
" sec "
);
Serial.println(
getRangeText()
);
}
}
// ============================================================
// SLOW-CLEAN ENCODER
//
// DO NOT MODIFY
// ============================================================
void handleEncoder()
{
unsigned long now =
millis();
int rawCLK =
digitalRead(
ENCODER_CLK
);
// Raw CLK changed
if (
rawCLK !=
lastRawCLK
)
{
lastRawCLK =
rawCLK;
clkChangeTime =
now;
}
// Stable for at least 8 ms
if (
rawCLK != stableCLK &&
(now - clkChangeTime)
>=
STABLE_TIME_MS
)
{
stableCLK =
rawCLK;
// --------------------------------------------------------
// Stable falling edge
// --------------------------------------------------------
if (
stableCLK == LOW &&
readyForNextClick &&
(now - lastClickTime)
>=
CLICK_LOCKOUT_MS
)
{
int dt =
readStableDT();
int direction;
if (dt == HIGH)
{
direction =
+1;
}
else
{
direction =
-1;
}
direction *=
ENCODER_DIRECTION;
acceptEncoderClick(
direction
);
lastClickTime =
now;
readyForNextClick =
false;
}
// --------------------------------------------------------
// Re-arm after CLK returns HIGH
// --------------------------------------------------------
if (
stableCLK ==
HIGH
)
{
readyForNextClick =
true;
}
}
}
// ============================================================
// SHORT CLICK
// START / STOP / RESUME
// ============================================================
void encoderButtonAction()
{
// ----------------------------------------------------------
// RUNNING -> PAUSE
// ----------------------------------------------------------
if (timerRunning)
{
timerRunning =
false;
Serial.println(
"TIMER PAUSED"
);
return;
}
// ----------------------------------------------------------
// PAUSED -> RESUME
// ----------------------------------------------------------
if (countdownHasStarted)
{
timerRunning =
true;
lastSecondMillis =
millis();
Serial.println(
"TIMER RESUMED"
);
return;
}
// ----------------------------------------------------------
// NEW START
// ----------------------------------------------------------
if (remainingSeconds > 0)
{
countdownHasStarted =
true;
timerRunning =
true;
lastSecondMillis =
millis();
Serial.println(
"TIMER STARTED"
);
}
}
// ============================================================
// BUTTON HANDLER
//
// SHORT click -> action on RELEASE
// LONG click -> RESET after 1.2 sec
// ============================================================
void handleButton()
{
unsigned long now =
millis();
if (
now -
lastButtonPollMillis
<
BUTTON_POLL_MS
)
{
return;
}
lastButtonPollMillis =
now;
bool rawState =
isEncoderButtonPressed();
// Raw state changed
if (
rawState !=
rawButtonLast
)
{
rawButtonLast =
rawState;
buttonDebounceMillis =
now;
}
// Debounced state
if (
now -
buttonDebounceMillis
>=
BUTTON_DEBOUNCE_MS
)
{
if (
rawState !=
stableButtonState
)
{
stableButtonState =
rawState;
// ======================================================
// PRESSED
// ======================================================
if (stableButtonState)
{
buttonPressStartMillis =
now;
longPressHandled =
false;
}
// ======================================================
// RELEASED
// ======================================================
else
{
// No long press -> normal short click
if (!longPressHandled)
{
encoderButtonAction();
}
}
}
}
// ==========================================================
// LONG PRESS
// ==========================================================
if (
stableButtonState &&
!longPressHandled &&
(now - buttonPressStartMillis)
>=
LONG_PRESS_MS
)
{
longPressHandled =
true;
resetTimer();
}
}
// ============================================================
// TIMER FINISHED ALARM
//
// RED + BUZZER = 500 ms
// WHITE + OFF = 500 ms
//
// x3
// ============================================================
void flashFinished()
{
for (int i = 0;
i < 3;
i++)
{
// --------------------------------------------------------
// RED + BEEP
// --------------------------------------------------------
renderFlashFrame(
COL_RED
);
buzzerON();
delay(
500
);
// --------------------------------------------------------
// WHITE + SILENT
// --------------------------------------------------------
buzzerOFF();
renderFlashFrame(
COL_WHITE
);
delay(
500
);
}
// Ensure final state
buzzerOFF();
renderFlashFrame(
COL_WHITE
);
// ----------------------------------------------------------
// Re-sync encoder after blocking alarm
// ----------------------------------------------------------
lastRawCLK =
digitalRead(
ENCODER_CLK
);
stableCLK =
lastRawCLK;
clkChangeTime =
millis();
lastClickTime =
millis();
readyForNextClick =
true;
// Touch state reset
touchWasDown =
false;
rangeTouchCandidate =
false;
}
// ============================================================
// COUNTDOWN
// ============================================================
void updateTimer()
{
if (!timerRunning)
{
return;
}
unsigned long now =
millis();
if (
now -
lastSecondMillis
>=
1000UL
)
{
lastSecondMillis +=
1000UL;
if (remainingSeconds > 0)
{
remainingSeconds--;
renderTimerFrame();
}
// ========================================================
// FINISHED
// ========================================================
if (remainingSeconds <= 0)
{
remainingSeconds =
0;
timerRunning =
false;
countdownHasStarted =
false;
Serial.println(
"TIMER FINISHED"
);
flashFinished();
}
}
}
// ============================================================
// DISPLAY INIT
// ============================================================
void initDisplay()
{
pinMode(
BL_PIN,
OUTPUT
);
digitalWrite(
BL_PIN,
HIGH
);
panelBus =
new Arduino_SWSPI(
GFX_NOT_DEFINED,
PANEL_CS,
PANEL_SCK,
PANEL_SDA,
GFX_NOT_DEFINED
);
rgbpanel =
new Arduino_ESP32RGBPanel(
40, 7, 15, 41,
46, 3, 8, 18, 17,
14, 13, 12, 11, 10, 9,
5, 45, 48, 47, 21,
1, 50, 10, 50,
1, 30, 10, 30,
PCLK_NEG,
8000000UL
);
display =
new Arduino_RGB_Display(
DISPLAY_WIDTH,
DISPLAY_HEIGHT,
rgbpanel,
0,
true,
panelBus,
GFX_NOT_DEFINED,
st7701_type7_init_operations,
sizeof(
st7701_type7_init_operations
)
);
// ----------------------------------------------------------
// Full screen Canvas
// ----------------------------------------------------------
gfx =
new Arduino_Canvas(
DISPLAY_WIDTH,
DISPLAY_HEIGHT,
display
);
bool ok =
gfx->begin(
16000000
);
if (!ok)
{
Serial.println(
"ERROR: CANVAS allocation failed!"
);
while (true)
{
delay(
1000
);
}
}
}
// ============================================================
// I2C + TOUCH + PCF INIT
// ============================================================
void initTouchAndIO()
{
Wire.begin(
I2C_SDA,
I2C_SCL
);
// PCF8574 is a standard-mode device.
// CST8XX also works correctly at 100 kHz.
Wire.setClock(
100000
);
// ----------------------------------------------------------
// External PCF:
// all pins HIGH
// buzzer therefore OFF
// ----------------------------------------------------------
externalPcfState =
0xFF;
externalPcfWrite();
// ----------------------------------------------------------
// Touch reset using INTERNAL PCF8574
// ----------------------------------------------------------
resetTouch();
// ----------------------------------------------------------
// Touch initialize
// ----------------------------------------------------------
if (
!touch.begin(
&Wire,
TOUCH_ADDR
)
)
{
Serial.println(
"ERROR: CST8XX TOUCH NOT FOUND"
);
while (true)
{
delay(
1000
);
}
}
Serial.println(
"TOUCH: OK"
);
Serial.println(
"EXTERNAL PCF8574: 0x20"
);
Serial.println(
"BUZZER: READY"
);
}
// ============================================================
// SETUP
// ============================================================
void setup()
{
Serial.begin(
115200
);
// ----------------------------------------------------------
// Encoder
// ----------------------------------------------------------
pinMode(
ENCODER_CLK,
INPUT_PULLUP
);
pinMode(
ENCODER_DT,
INPUT_PULLUP
);
lastRawCLK =
digitalRead(
ENCODER_CLK
);
stableCLK =
lastRawCLK;
// ----------------------------------------------------------
// Display
// ----------------------------------------------------------
initDisplay();
// ----------------------------------------------------------
// Touch + PCF8574 + buzzer
// ----------------------------------------------------------
initTouchAndIO();
delay(
300
);
// ----------------------------------------------------------
// Initial screen
// ----------------------------------------------------------
remainingSeconds =
0;
buzzerOFF();
renderTimerFrame();
Serial.println();
Serial.println(
"================================"
);
Serial.println(
"TIMER V1.0 FINAL READY"
);
Serial.println(
"================================"
);
Serial.println(
"1 MIN = YELLOW"
);
Serial.println(
"10 MIN = TURQUOISE"
);
Serial.println(
"30 MIN = ORANGE"
);
Serial.println(
"60 MIN = MAGENTA"
);
Serial.println();
Serial.println(
"SHORT CLICK = START / STOP"
);
Serial.println(
"LONG CLICK = RESET"
);
Serial.println(
"ALARM = 3 x RED + BEEP"
);
}
// ============================================================
// LOOP
// ============================================================
void loop()
{
// Encoder first
handleEncoder();
// Physical encoder button
handleButton();
// Touch range selector
handleTouch();
// Countdown
updateTimer();
delay(
1
);
}
DIY ESP32 Universal Timer with Touchscreen & Rotary Encoder
Raspberry Pi 5 7 Inch Touch Screen IPS 1024x600 HD LCD HDMI-compatible Display for RPI 4B 3B+ OPI 5 AIDA64 PC Secondary Screen(Without Speaker)
BUY NOW- Comments(0)
- Likes(0)
- 0 USER VOTES
- YOUR VOTE 0.00 0.00
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
More by Mirko Pavleski
-
Arduino 3D Printed self Balancing Cube
Self-balancing devices are electronic devices that use sensors and motors to keep themselves balanc...
-
DIY ESP32 Universal Timer with Touchscreen & Rotary Encoder
In several of my previous videos, I presented different projects based on the CrowPanel 2.1-inch HM...
-
Build a Real 5-Band Parametric EQ with ESP32 + WM8960 - Stereo DSP Audio Processor
Audio equalizer (EQ) is an electronic device that allows control over the strength of different fre...
-
Bass Reflex vs Sealed vs Horn – Same 2 inch Speaker Test
In one of the previous videos I presented the making of a miniature Horn type speaker box with a 2 ...
-
Building a Mini Fostex Horn Speaker with a 2-Inch Full-Range Driver
A back-loaded horn speaker is a loudspeaker design that channels the rear sound wave of a driver th...
-
ESP32 Moon Lamp That Shows the Real Moon Phase on Crowpanel Round Dispaly
Recently on the Hackaday portal I came across a very interesting project called Moon Display by aut...
-
40-Year-Old DIY Power Amplifiers – Do They Still Work - 500W MOSFET & Transistor Amps
This time I want to describe my two DIY super powerful audio amplifiers that I made about 40 years ...
-
Wi-Fi HaLow Explained! Long-Range Camera Test up to 300m (ESP32 + ThinkNode G4 from ELECROW)
I recently needed to install a security camera at a relatively large distance from my home, more pre...
-
Simple Object Recognition with the UNIHIKER K10 – No TensorFlow, No Internet, No Cloud AI
Object recognition has become one of the most popular applications of artificial intelligence and em...
-
DIY PCL82 Tube Amplifier, Single-Ended Amp with Amazing Sound
The idea for this project came from a local electronics magazine called EMITER, issue 12/13 from 20...
-
Simple MCP2036 SDR Radio - The Most Unusual Software Defined Radio
SDR (Software Defined Radio) is a radio communication system where many of the key electronic compo...
-
Elecrow All-in-One Arduino Starter Kit Review - 20 Projects & 16 Modules
This time I will describe a simple and practical way to enter the world of microcontrollers, specif...
-
ESP32-C3 Color Detector with TCS34725, Real-Time RGB Detection & Web Interface
Color detection is a fundamental task in many embedded systems – from industrial sorting machines t...
-
DIY ESP32 Telegram Flood Protection System - Smart Home Automation
Recently I had an unpleasant experience in my home, specifically my ground floor was flooded as a r...
-
Real-Time Air Traffic Radar using ESP32 + ADS-B Data
ADS-B, which stands for Automatic Dependent Surveillance-Broadcast, is the modern standard for trac...
-
DIY Green Laser Night Sky Object Finder - Find Stars & Galaxies Instantly with great accuracy
As an amateur astronomer, especially at the beginning, the most difficult part of observing the nig...
-
DIY Avionics Simulator with ESP32 - Artificial Horizon, Compass & Altimeter
The inspiration for this project comes from classical aircraft cockpit instruments used for navigat...
-
DIY Miniature X-Ray Machine using a TV Vacuum Tube DY86
An X-ray machine (or radiograph) is a quick, painless medical test that produces images of the struc...
-
Émetteur Récepteur Optique Expérimental (communication UART ATtiny + fibre optique)
138 0 1 -
SmallOLED - Live PC Stats, Animated Clocks & Audio Visualizer
979 0 3 -
"Talking Multimeter": Arduino-Based Speech Add-on for Multimeters with SCPI Protocol
561 0 1 -
Programmable Mist Maker - XIAO / QT PY Extension
3032 2 3 -
RadioHAT - Raspberry Pi radio development platform
2581 0 4 -
QWIIC-VL53L4CD Time-of-Flight Distance Sensor Module
2804 0 2 -
-







