Hardware components | ||||||
| × | 1 | ||||
Software apps and online services | ||||||
![]() |
| |||||
Hand tools and fabrication machines | ||||||
![]() |
| |||||
![]() |
| |||||
Today I received a shipment with a large round LCD display from Elecrow. The device is packed in two boxes so that it is fully protected from damage during transportation. Inside there is a display, a USB cable for power and communication, as well as an additional cable for connecting an external module.
This is CrowPanel 2.1inch-HMI ESP32 Rotary Display 480*480 which has some really impressive features:
- high-performance ESP32-S3 chip
- WiFi and low-power Bluetooth
- capacitive touch screen with knob
- and the Rotary encoder in the form of a circular ring
It supports Arduino IDE, Lua RTOS, Home Assistant/PlatformIO/Micro Python, and LVGL library.
When the device is turned on for the first time, a demo application is installed that presents several basic features on the display.
Thanks to the high resolution, the image on the display is clear and beautifully visible from different angles.
This time I decided to make a relatively simple project, without the proposed support from Elecrow, in order to discover the way to connect all the components to the microcontroller. At first I tried to use the TFT_eSPI library but soon I discovered that the display communicates via RGB panel interface (i.e. parallel RGB, not SPI), so this idea was dropped. So I decided to use the Arduino_GFX_Library library which has support for this way of connecting the display. I imagined the project to be made in a classic way, without the support of the LVGL library and Squareline Studio, by manually drawing all the lines individually. This way the project will not look like most projects that use the previously mentioned libraries and tools and will therefore be authentic.
The choice is another Clock project in addition to my large collection of unusual clocks. This time it will be a visually interesting Radar Clock, actually a Radar simulation, on which the exact time and date will be written.
In this project I will present the functionality of the Wi-Fi part, as well as the rotary encoder. First, let's see how the device functions in real conditions: After turning on the device, a message appears on the screen and the Wi-Fi network is being connected.
After that, the main screen appears. The default color is green, as in most real radar systems. The upper half of the screen shows the time, and the lower half shows the date.
The exact time is downloaded via the Internet from an NTP server, so there is no need to manually set the clock. To demonstrate the functionality of the rotary encoder, I added a section in the code where the colors of the radar can be changed. Unfortunately, this video cannot capture the beautiful colors displayed on this high-quality display.
At the beginning of the code, 5 color schemes are defined that change by turning the encoder. The color change is instantaneous and does not affect the functionality at all.
Now a few words about the code. When compiling I used ESP32 cores 3.x.x, the latest version of Arduino _GFX library and arduino IDE ver. 1.8.16.
Overall, the code may not be very simple, which is a consequence of the fact that everything is drawn programmatically. However, it is made in a way that you can easily change several parameters to make a custom clock face. It is also seen that the code uses a minimum number of basic libraries. I plan to create code in the next projects with this display using the new libraries and tools developed specifically for projects with LCD displays, because the development is much easier and the final effect is fascinating.
Otherwise, I made a kind of housing for this project from 3 and 5 mm PVC material coated with colored self-adhesive wallpaper.
And finally, a short conclusion. This module from Elekrow has endless possibilities for making DIY projects in a relatively simple way using specialized libraries and tools, which way I will describe in more detail in one of the following videos. A huge advantage is the fact that there is no need for soldering or other hardware work. There is even a cable for connecting external modules, also without soldering, during the test period.
/* Arduino Radar Clock on CrowPanel 2.1inch-HMI ESP32 Rotary Display 480*480 by mircemk, October 2025*/
#include <Arduino.h>
#include <Arduino_GFX_Library.h>
#include <time.h>
#include <WiFi.h>
// WiFi credentials
const char* ssid = "*****"; // Replace with your WiFi SSID
const char* password = "*****"; // Replace with your WiFi password
// NTP Server settings
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 3600; // Change according to your timezone (3600 = GMT+1)
const int daylightOffset_sec = 3600;
/* ======== DISPLAY CONFIGURATION ======== */
#define TYPE_SEL 7 // ST7701 init table
#define PCLK_NEG 1 // 1 = falling edge
#define TIMING_SET 1 // 1 = wider safe porches
/* --- Backlight PIN --- */
#define BL_PIN 6
/* --- SPI for ST7701 init --- */
#define PANEL_CS 16
#define PANEL_SCK 2
#define PANEL_SDA 1
/* --- Rotary Encoder Pins --- */
#define ENCODER_A_PIN 42
#define ENCODER_B_PIN 44
/* --- Display Timing --- */
#if TIMING_SET == 0
static const int HFP=20, HPW=10, HBP=10;
static const int VFP=8, VPW=10, VBP=10;
#else
static const int HFP=40, HPW=8, HBP=40;
static const int VFP=20, VPW=8, VBP=20;
#endif
/* ======== RADAR CONFIGURATION ======== */
#define DISPLAY_WIDTH 480
#define DISPLAY_HEIGHT 480
#define CENTER_X 240
#define CENTER_Y 240
#define RADAR_RADIUS 200
#define FRAME_START 200
#define FRAME_WIDTH 5
#define BORDER_WIDTH 5
#define SWEEP_SPEED 3
/* ======== COLOR SCHEMES ======== */
// Color definitions for different schemes (RGB565)
typedef struct {
uint16_t green_color;
uint16_t dim_green_color;
uint16_t sweep_color;
uint16_t grid_color;
uint16_t frame_color;
uint16_t text_color;
uint16_t trail_color;
uint16_t black;
} ColorScheme;
// Scheme 1: Classic Green (original)
const ColorScheme SCHEME_GREEN = {
0x07E0, // green_color
0x03A0, // dim_green_color
0x07FF, // sweep_color (cyan)
0x03E0, // grid_color
0x07E0, // frame_color
0x07E0, // text_color
0x0320, // trail_color
0x0000 // black
};
// Scheme 2: Red
const ColorScheme SCHEME_RED = {
0xF800, // green_color -> red
0x7800, // dim_green_color -> dark red
0xF810, // sweep_color -> bright red
0xF800, // grid_color -> red
0xF800, // frame_color -> red
0xF800, // text_color -> red
0x7800, // trail_color -> dark red
0x0000 // black
};
// Scheme 3: Blue (like second vector link - digital radar)
const ColorScheme SCHEME_BLUE = {
0x001F, // green_color -> blue
0x0010, // dim_green_color -> dark blue
0x041F, // sweep_color -> bright blue
0x001F, // grid_color -> blue
0x001F, // frame_color -> blue
0x001F, // text_color -> blue
0x0010, // trail_color -> dark blue
0x0000 // black
};
// Scheme 4: Yellow-Orange
const ColorScheme SCHEME_YELLOW_ORANGE = {
0xFDA0, // green_color -> yellow-orange
0xFB00, // dim_green_color -> dark yellow-orange
0xFEA0, // sweep_color -> bright yellow-orange
0xFDA0, // grid_color -> yellow-orange
0xFDA0, // frame_color -> yellow-orange
0xFDA0, // text_color -> yellow-orange
0xFB00, // trail_color -> dark yellow-orange
0x0000 // black
};
// Scheme 5: White
const ColorScheme SCHEME_WHITE = {
0xFFFF, // green_color -> white
0xDEFB, // dim_green_color -> light gray
0xFFFF, // sweep_color -> white
0xFFFF, // grid_color -> white
0xFFFF, // frame_color -> white
0xFFFF, // text_color -> white
0xBDF7, // trail_color -> medium gray
0x0000 // black
};
const ColorScheme* colorSchemes[] = {
&SCHEME_GREEN,
&SCHEME_RED,
&SCHEME_BLUE,
&SCHEME_YELLOW_ORANGE,
&SCHEME_WHITE
};
#define NUM_COLOR_SCHEMES 5
/* ======== GLOBAL VARIABLES ======== */
Arduino_DataBus *panelBus = nullptr;
Arduino_ESP32RGBPanel *rgbpanel = nullptr;
Arduino_RGB_Display *gfx = nullptr;
static float current_angle = 0;
static uint16_t *frameBuffer = nullptr;
static uint16_t *staticFrameBuffer = nullptr;
// Color scheme management
int currentColorScheme = 0;
bool colorSchemeChanged = true;
// Rotary encoder variables
volatile int encoderPos = 0;
int lastEncoderPos = 0;
portMUX_TYPE encoderMux = portMUX_INITIALIZER_UNLOCKED;
// Startup sequence state
enum StartupState {
SHOW_TITLE,
SHOW_CONNECTING,
SHOW_CLOCK
};
StartupState currentStartupState = SHOW_TITLE;
unsigned long startupStartTime = 0;
// WiFi connection state
bool wifiConnecting = false;
bool wifiConnected = false;
unsigned long lastWiFiCheck = 0;
const unsigned long WIFI_CHECK_INTERVAL = 1000;
// Display stability
bool displayStable = false;
// Encoder interrupt service routine
void IRAM_ATTR encoderISR() {
portENTER_CRITICAL_ISR(&encoderMux);
static uint8_t old_AB = 0;
// Grey code
// 0b00: 0
// 0b01: 1
// 0b11: 2
// 0b10: 3
const int8_t enc_states[] = {0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0};
old_AB <<= 2; // Remember previous state
old_AB |= (digitalRead(ENCODER_A_PIN) ? (1 << 1) : 0) | (digitalRead(ENCODER_B_PIN) ? (1 << 0) : 0);
encoderPos += enc_states[(old_AB & 0x0f)];
portEXIT_CRITICAL_ISR(&encoderMux);
}
void handleEncoder() {
portENTER_CRITICAL(&encoderMux);
int currentPos = encoderPos;
portEXIT_CRITICAL(&encoderMux);
if (currentPos != lastEncoderPos && currentStartupState == SHOW_CLOCK) {
if (currentPos > lastEncoderPos) {
// Clockwise rotation - next color scheme
currentColorScheme = (currentColorScheme + 1) % NUM_COLOR_SCHEMES;
} else {
// Counter-clockwise rotation - previous color scheme
currentColorScheme = (currentColorScheme - 1 + NUM_COLOR_SCHEMES) % NUM_COLOR_SCHEMES;
}
colorSchemeChanged = true;
Serial.printf("Color scheme changed to: %d\n", currentColorScheme + 1);
lastEncoderPos = currentPos;
}
}
/* ======== STARTUP SCREEN FUNCTIONS ======== */
void showTitleScreen() {
// Use direct display drawing for maximum stability
gfx->fillScreen(SCHEME_GREEN.black);
// Draw "RADAR CLOCK" - larger text
gfx->setTextColor(SCHEME_GREEN.text_color);
gfx->setTextSize(4);
// Calculate position for "RADAR CLOCK"
String radarText = "RADAR CLOCK";
int16_t x1, y1;
uint16_t w, h;
gfx->getTextBounds(radarText, 0, 0, &x1, &y1, &w, &h);
int radarX = (DISPLAY_WIDTH - w) / 2;
int radarY = CENTER_Y - 60;
gfx->setCursor(radarX, radarY);
gfx->print(radarText);
// Draw "by" - smaller text
gfx->setTextSize(2);
String byText = "by";
gfx->getTextBounds(byText, 0, 0, &x1, &y1, &w, &h);
int byX = (DISPLAY_WIDTH - w) / 2;
int byY = CENTER_Y;
gfx->setCursor(byX, byY);
gfx->print(byText);
// Draw "Mircemk" - medium text
gfx->setTextSize(3);
String nameText = "Mircemk";
gfx->getTextBounds(nameText, 0, 0, &x1, &y1, &w, &h);
int nameX = (DISPLAY_WIDTH - w) / 2;
int nameY = CENTER_Y + 40;
gfx->setCursor(nameX, nameY);
gfx->print(nameText);
}
void showConnectingScreen() {
// Use direct display drawing for stability during WiFi connection
gfx->fillScreen(SCHEME_GREEN.black);
// Draw "connecting..." text
gfx->setTextColor(SCHEME_GREEN.text_color);
gfx->setTextSize(3);
String connectingText = "connecting...";
int16_t x1, y1;
uint16_t w, h;
gfx->getTextBounds(connectingText, 0, 0, &x1, &y1, &w, &h);
int textX = (DISPLAY_WIDTH - w) / 2;
int textY = CENTER_Y;
gfx->setCursor(textX, textY);
gfx->print(connectingText);
// Draw the display immediately using the frame buffer method for stability
if (frameBuffer) {
gfx->draw16bitRGBBitmap(0, 0, frameBuffer, DISPLAY_WIDTH, DISPLAY_HEIGHT);
}
}
void updateConnectingAnimation() {
static unsigned long lastDotUpdate = 0;
static bool dotVisible = false;
unsigned long currentTime = millis();
if (currentTime - lastDotUpdate >= 500) { // Blink every 500ms
lastDotUpdate = currentTime;
dotVisible = !dotVisible;
// Update dot without redrawing entire screen
if (dotVisible) {
gfx->fillRect(DISPLAY_WIDTH - 30, CENTER_Y + 40, 10, 10, SCHEME_GREEN.text_color);
} else {
gfx->fillRect(DISPLAY_WIDTH - 30, CENTER_Y + 40, 10, 10, SCHEME_GREEN.black);
}
// Update only the changed area
gfx->draw16bitRGBBitmap(DISPLAY_WIDTH - 30, CENTER_Y + 40,
&frameBuffer[CENTER_Y * DISPLAY_WIDTH + (DISPLAY_WIDTH - 30)],
10, 10);
}
}
bool connectToWiFi() {
Serial.print("Connecting to WiFi");
WiFi.begin(ssid, password);
unsigned long startTime = millis();
const unsigned long timeout = 30000; // 30 seconds timeout
while (WiFi.status() != WL_CONNECTED && millis() - startTime < timeout) {
delay(500);
Serial.print(".");
// Update connecting animation
updateConnectingAnimation();
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.println("WiFi connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
return true;
} else {
Serial.println("WiFi connection failed!");
return false;
}
}
void updateStartupSequence() {
unsigned long currentTime = millis();
unsigned long elapsedTime = currentTime - startupStartTime;
switch(currentStartupState) {
case SHOW_TITLE:
if (elapsedTime >= 2000) { // Show title for 2 seconds
currentStartupState = SHOW_CONNECTING;
startupStartTime = currentTime;
showConnectingScreen();
Serial.println("Showing connecting screen...");
// Start WiFi connection
wifiConnecting = true;
WiFi.begin(ssid, password);
}
break;
case SHOW_CONNECTING:
// Update connecting animation
updateConnectingAnimation();
// Check WiFi status periodically
if (currentTime - lastWiFiCheck >= WIFI_CHECK_INTERVAL) {
lastWiFiCheck = currentTime;
if (WiFi.status() == WL_CONNECTED) {
wifiConnecting = false;
wifiConnected = true;
currentStartupState = SHOW_CLOCK;
startupStartTime = currentTime;
Serial.println("WiFi connected! Showing clock...");
// Initialize the clock display
initClockDisplay();
displayStable = true;
}
}
break;
case SHOW_CLOCK:
// Clock is now running in main loop
break;
}
}
/* ======== DRAWING FUNCTIONS ======== */
void draw_line_to_buffer(uint16_t *buffer, int x0, int y0, int x1, int y1, uint16_t color) {
int dx = abs(x1 - x0);
int dy = abs(y1 - y0);
int sx = (x0 < x1) ? 1 : -1;
int sy = (y0 < y1) ? 1 : -1;
int err = dx - dy;
while (true) {
if (x0 >= 0 && x0 < DISPLAY_WIDTH && y0 >= 0 && y0 < DISPLAY_HEIGHT) {
buffer[y0 * DISPLAY_WIDTH + x0] = color;
}
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x0 += sx;
}
if (e2 < dx) {
err += dx;
y0 += sy;
}
}
}
void draw_rectangle_to_buffer(uint16_t *buffer, int x1, int y1, int x2, int y2, uint16_t color) {
for (int y = y1; y <= y2; y++) {
for (int x = x1; x <= x2; x++) {
if (x >= 0 && x < DISPLAY_WIDTH && y >= 0 && y < DISPLAY_HEIGHT) {
buffer[y * DISPLAY_WIDTH + x] = color;
}
}
}
}
void draw_digit_to_buffer(uint16_t *buffer, int x, int y, int digit, uint16_t color) {
// Increased sizes (3x original)
int width = 18; // Was 6
int height = 24; // Was 8
// Clear background for larger digit
draw_rectangle_to_buffer(buffer, x-9, y-12, x+9, y+12, colorSchemes[currentColorScheme]->black);
switch(digit) {
case 0:
draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top
draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom
draw_line_to_buffer(buffer, x-6, y-9, x-6, y+9, color); // left
draw_line_to_buffer(buffer, x+6, y-9, x+6, y+9, color); // right
break;
case 1:
draw_line_to_buffer(buffer, x, y-9, x, y+9, color); // vertical
draw_line_to_buffer(buffer, x-3, y-9, x, y-9, color); // top
break;
case 2:
draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top
draw_line_to_buffer(buffer, x+6, y-9, x+6, y, color); // right top
draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle
draw_line_to_buffer(buffer, x-6, y, x-6, y+9, color); // left bottom
draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom
break;
case 3:
draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top
draw_line_to_buffer(buffer, x+6, y-9, x+6, y+9, color); // right
draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle
draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom
break;
case 4:
draw_line_to_buffer(buffer, x-6, y-9, x-6, y, color); // left top
draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle
draw_line_to_buffer(buffer, x+6, y-9, x+6, y+9, color); // right
break;
case 5:
draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top
draw_line_to_buffer(buffer, x-6, y-9, x-6, y, color); // left top
draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle
draw_line_to_buffer(buffer, x+6, y, x+6, y+9, color); // right bottom
draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom
break;
case 6:
draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top
draw_line_to_buffer(buffer, x-6, y-9, x-6, y+9, color); // left
draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle
draw_line_to_buffer(buffer, x+6, y, x+6, y+9, color); // right bottom
draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom
break;
case 7:
draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top
draw_line_to_buffer(buffer, x+6, y-9, x+6, y+9, color); // right
break;
case 8:
draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top
draw_line_to_buffer(buffer, x-6, y-9, x-6, y+9, color); // left
draw_line_to_buffer(buffer, x+6, y-9, x+6, y+9, color); // right
draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle
draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom
break;
case 9:
draw_line_to_buffer(buffer, x-6, y-9, x+6, y-9, color); // top
draw_line_to_buffer(buffer, x-6, y-9, x-6, y, color); // left top
draw_line_to_buffer(buffer, x+6, y-9, x+6, y+9, color); // right
draw_line_to_buffer(buffer, x-6, y, x+6, y, color); // middle
draw_line_to_buffer(buffer, x-6, y+9, x+6, y+9, color); // bottom
break;
}
}
void draw_text_to_buffer(uint16_t *buffer, int x, int y, const char* text, uint16_t color) {
int char_width = 24; // Increased from 8 to 24 for larger spacing
int pos_x = x;
while (*text) {
char c = *text++;
if (c >= '0' && c <= '9') {
draw_digit_to_buffer(buffer, pos_x, y, c - '0', color);
}
else if (c == ':') {
// Draw larger colon
buffer[(y - 6) * DISPLAY_WIDTH + pos_x] = color;
buffer[(y - 5) * DISPLAY_WIDTH + pos_x] = color;
buffer[(y + 5) * DISPLAY_WIDTH + pos_x] = color;
buffer[(y + 6) * DISPLAY_WIDTH + pos_x] = color;
}
else if (c == '/') {
// Draw larger slash
for (int i = -9; i <= 9; i++) {
int px = pos_x + i;
int py = y - i;
if (px >= 0 && px < DISPLAY_WIDTH && py >= 0 && py < DISPLAY_HEIGHT) {
buffer[py * DISPLAY_WIDTH + px] = color;
}
}
}
pos_x += char_width;
}
}
void update_time_display() {
struct tm timeinfo;
if(!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time");
return;
}
// Calculate positions
// Font height is 24, so three heights = 72 pixels
int time_y = CENTER_Y - 72; // Move up by three font heights
int date_y = CENTER_Y + 72; // Move down by three font heights
int time_x = CENTER_X - 90; // Keep the same horizontal position
int date_x = CENTER_X - 105; // Keep the same horizontal position
// Clear previous time area - precise clearing
// Height of clearing = font height (24) + 2 pixels margin
// Width of clearing = 8 digits * 24 pixels width + 4 pixels margin
for (int y = time_y - 13; y < time_y + 13; y++) {
for (int x = time_x - 2; x < time_x + (8 * 24) + 2; x++) {
if (x >= 0 && x < DISPLAY_WIDTH && y >= 0 && y < DISPLAY_HEIGHT) {
frameBuffer[y * DISPLAY_WIDTH + x] = colorSchemes[currentColorScheme]->black;
}
}
}
// Format time string
char timeStr[9];
sprintf(timeStr, "%02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
// Draw time
draw_text_to_buffer(frameBuffer, time_x, time_y, timeStr, colorSchemes[currentColorScheme]->text_color);
// Clear previous date area - precise clearing
// Height of clearing = font height (24) + 2 pixels margin
// Width of clearing = 10 digits * 24 pixels width + 4 pixels margin
for (int y = date_y - 13; y < date_y + 13; y++) {
for (int x = date_x - 2; x < date_x + (10 * 24) + 2; x++) {
if (x >= 0 && x < DISPLAY_WIDTH && y >= 0 && y < DISPLAY_HEIGHT) {
frameBuffer[y * DISPLAY_WIDTH + x] = colorSchemes[currentColorScheme]->black;
}
}
}
// Format date string
char dateStr[11];
sprintf(dateStr, "%02d/%02d/%04d", timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year + 1900);
// Draw date
draw_text_to_buffer(frameBuffer, date_x, date_y, dateStr, colorSchemes[currentColorScheme]->text_color);
}
void redrawStaticElements() {
// Clear static frame buffer
for (int i = 0; i < DISPLAY_WIDTH * DISPLAY_HEIGHT; i++) {
staticFrameBuffer[i] = colorSchemes[currentColorScheme]->black;
}
// Redraw all static elements with new colors
draw_frame_border();
draw_radar_grid();
// Copy to main frame buffer
for (int i = 0; i < DISPLAY_WIDTH * DISPLAY_HEIGHT; i++) {
frameBuffer[i] = staticFrameBuffer[i];
}
colorSchemeChanged = false;
Serial.println("Static elements redrawn with new color scheme");
}
/* ======== DISPLAY INITIALIZATION ======== */
void init_display() {
Serial.println("Initializing display...");
pinMode(BL_PIN, OUTPUT);
digitalWrite(BL_PIN, HIGH);
delay(100);
// Initialize rotary encoder pins
pinMode(ENCODER_A_PIN, INPUT_PULLUP);
pinMode(ENCODER_B_PIN, INPUT_PULLUP);
// Attach interrupts for rotary encoder
attachInterrupt(digitalPinToInterrupt(ENCODER_A_PIN), encoderISR, CHANGE);
attachInterrupt(digitalPinToInterrupt(ENCODER_B_PIN), encoderISR, CHANGE);
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
);
#if TYPE_SEL == 7
gfx = new Arduino_RGB_Display(
DISPLAY_WIDTH, DISPLAY_HEIGHT, rgbpanel, 0, true,
panelBus, GFX_NOT_DEFINED,
st7701_type7_init_operations, sizeof(st7701_type7_init_operations)
);
#else
gfx = new Arduino_RGB_Display(
DISPLAY_WIDTH, DISPLAY_HEIGHT, rgbpanel, 0, true,
panelBus, GFX_NOT_DEFINED,
st7701_type5_init_operations, sizeof(st7701_type5_init_operations)
);
#endif
Serial.println("Starting display begin...");
bool ok = gfx->begin(16000000);
Serial.printf("Display begin: %s\n", ok ? "OK" : "FAILED");
if (!ok) {
Serial.println("Display initialization failed!");
while(1) delay(1000);
}
// Allocate main frame buffer with extra margin for safety
frameBuffer = (uint16_t*)ps_malloc(DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t) + 32);
if (!frameBuffer) {
Serial.println("Frame buffer allocation failed!");
while(1) delay(1000);
}
// Allocate static frame buffer for unchanging elements with extra margin
staticFrameBuffer = (uint16_t*)ps_malloc(DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint16_t) + 32);
if (!staticFrameBuffer) {
Serial.println("Static frame buffer allocation failed!");
while(1) delay(1000);
}
// Clear both buffers to black
for (int i = 0; i < DISPLAY_WIDTH * DISPLAY_HEIGHT; i++) {
frameBuffer[i] = colorSchemes[currentColorScheme]->black;
staticFrameBuffer[i] = colorSchemes[currentColorScheme]->black;
}
Serial.println("Display initialized successfully");
}
void initClockDisplay() {
// Draw static elements to static buffer
draw_frame_border();
draw_radar_grid();
// Copy static elements to main frame buffer
for (int i = 0; i < DISPLAY_WIDTH * DISPLAY_HEIGHT; i++) {
frameBuffer[i] = staticFrameBuffer[i];
}
// Init and get the time
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
Serial.println("Clock display initialized");
}
void draw_circle_to_buffer(uint16_t *buffer, int center_x, int center_y, int radius, uint16_t color) {
int x = radius;
int y = 0;
int err = 0;
while (x >= y) {
buffer[(center_y + y) * DISPLAY_WIDTH + (center_x + x)] = color;
buffer[(center_y + x) * DISPLAY_WIDTH + (center_x + y)] = color;
buffer[(center_y + x) * DISPLAY_WIDTH + (center_x - y)] = color;
buffer[(center_y + y) * DISPLAY_WIDTH + (center_x - x)] = color;
buffer[(center_y - y) * DISPLAY_WIDTH + (center_x + x)] = color;
buffer[(center_y - x) * DISPLAY_WIDTH + (center_x + y)] = color;
buffer[(center_y - x) * DISPLAY_WIDTH + (center_x - y)] = color;
buffer[(center_y - y) * DISPLAY_WIDTH + (center_x - x)] = color;
y++;
err += 1 + 2 * y;
if (2 * (err - x) + 1 > 0) {
x--;
err += 1 - 2 * x;
}
}
}
void draw_arc_to_buffer(uint16_t *buffer, int center_x, int center_y, int radius, int start_angle, int end_angle, uint16_t color) {
// Convert angles to radians
float start_rad = start_angle * PI / 180.0;
float end_rad = end_angle * PI / 180.0;
// Draw arc by stepping through angles
for (float angle = start_rad; angle <= end_rad; angle += 0.01) {
int x = center_x + radius * cos(angle);
int y = center_y + radius * sin(angle);
if (x >= 0 && x < DISPLAY_WIDTH && y >= 0 && y < DISPLAY_HEIGHT) {
buffer[y * DISPLAY_WIDTH + x] = color;
}
}
}
void draw_radial_scale_marks() {
for (int angle = 0; angle < 360; angle += 10) {
if ((angle >= 355 || angle <= 5) ||
(angle >= 85 && angle <= 95) ||
(angle >= 175 && angle <= 185) ||
(angle >= 265 && angle <= 275)) {
continue;
}
float rad = angle * PI / 180.0;
int inner_x = CENTER_X + (FRAME_START + FRAME_WIDTH) * sin(rad);
int inner_y = CENTER_Y - (FRAME_START + FRAME_WIDTH) * cos(rad);
int outer_x = CENTER_X + (FRAME_START + FRAME_WIDTH + 12) * sin(rad);
int outer_y = CENTER_Y - (FRAME_START + FRAME_WIDTH + 12) * cos(rad);
draw_line_to_buffer(staticFrameBuffer, inner_x, inner_y, outer_x, outer_y, colorSchemes[currentColorScheme]->frame_color);
}
for (int angle = 0; angle < 360; angle += 30) {
if (angle % 90 == 0) continue;
if ((angle >= 355 || angle <= 5) ||
(angle >= 85 && angle <= 95) ||
(angle >= 175 && angle <= 185) ||
(angle >= 265 && angle <= 275)) {
continue;
}
float rad = angle * PI / 180.0;
int inner_x = CENTER_X + (FRAME_START + FRAME_WIDTH) * sin(rad);
int inner_y = CENTER_Y - (FRAME_START + FRAME_WIDTH) * cos(rad);
int outer_x = CENTER_X + (FRAME_START + FRAME_WIDTH + 20) * sin(rad);
int outer_y = CENTER_Y - (FRAME_START + FRAME_WIDTH + 20) * cos(rad);
draw_line_to_buffer(staticFrameBuffer, inner_x, inner_y, outer_x, outer_y, colorSchemes[currentColorScheme]->frame_color);
}
}
void draw_cardinal_directions() {
const int gap_size = 10;
// North
int north_x = CENTER_X;
int north_y = CENTER_Y - FRAME_START - FRAME_WIDTH - 20;
for (int dx = -12; dx <= 12; dx++) {
for (int dy = -12; dy <= 12; dy++) {
int px = north_x + dx;
int py = north_y + dy;
if (px >= 0 && px < DISPLAY_WIDTH && py >= 0 && py < DISPLAY_HEIGHT) {
staticFrameBuffer[py * DISPLAY_WIDTH + px] = colorSchemes[currentColorScheme]->black;
}
}
}
draw_line_to_buffer(staticFrameBuffer, north_x - 8, north_y + 8, north_x - 8, north_y - 8, colorSchemes[currentColorScheme]->text_color);
draw_line_to_buffer(staticFrameBuffer, north_x + 8, north_y + 8, north_x + 8, north_y - 8, colorSchemes[currentColorScheme]->text_color);
draw_line_to_buffer(staticFrameBuffer, north_x - 8, north_y - 8, north_x + 8, north_y + 8, colorSchemes[currentColorScheme]->text_color);
// South
int south_x = CENTER_X;
int south_y = CENTER_Y + FRAME_START + FRAME_WIDTH + 20;
for (int dx = -12; dx <= 12; dx++) {
for (int dy = -12; dy <= 12; dy++) {
int px = south_x + dx;
int py = south_y + dy;
if (px >= 0 && px < DISPLAY_WIDTH && py >= 0 && py < DISPLAY_HEIGHT) {
staticFrameBuffer[py * DISPLAY_WIDTH + px] = colorSchemes[currentColorScheme]->black;
}
}
}
draw_line_to_buffer(staticFrameBuffer, south_x - 8, south_y - 8, south_x + 8, south_y - 8, colorSchemes[currentColorScheme]->text_color);
draw_line_to_buffer(staticFrameBuffer, south_x - 8, south_y - 8, south_x - 8, south_y, colorSchemes[currentColorScheme]->text_color);
draw_line_to_buffer(staticFrameBuffer, south_x - 8, south_y, south_x + 8, south_y, colorSchemes[currentColorScheme]->text_color);
draw_line_to_buffer(staticFrameBuffer, south_x + 8, south_y, south_x + 8, south_y + 8, colorSchemes[currentColorScheme]->text_color);
draw_line_to_buffer(staticFrameBuffer, south_x - 8, south_y + 8, south_x + 8, south_y + 8, colorSchemes[currentColorScheme]->text_color);
// East
int east_x = CENTER_X + FRAME_START + FRAME_WIDTH + 20;
int east_y = CENTER_Y;
for (int dx = -12; dx <= 12; dx++) {
for (int dy = -12; dy <= 12; dy++) {
int px = east_x + dx;
int py = east_y + dy;
if (px >= 0 && px < DISPLAY_WIDTH && py >= 0 && py < DISPLAY_HEIGHT) {
staticFrameBuffer[py * DISPLAY_WIDTH + px] = colorSchemes[currentColorScheme]->black;
}
}
}
draw_line_to_buffer(staticFrameBuffer, east_x - 8, east_y - 8, east_x + 8, east_y - 8, colorSchemes[currentColorScheme]->text_color);
draw_line_to_buffer(staticFrameBuffer, east_x - 8, east_y, east_x + 8, east_y, colorSchemes[currentColorScheme]->text_color);
draw_line_to_buffer(staticFrameBuffer, east_x - 8, east_y + 8, east_x + 8, east_y + 8, colorSchemes[currentColorScheme]->text_color);
draw_line_to_buffer(staticFrameBuffer, east_x - 8, east_y - 8, east_x - 8, east_y + 8, colorSchemes[currentColorScheme]->text_color);
// West
int west_x = CENTER_X - FRAME_START - FRAME_WIDTH - 20;
int west_y = CENTER_Y;
for (int dx = -12; dx <= 12; dx++) {
for (int dy = -12; dy <= 12; dy++) {
int px = west_x + dx;
int py = west_y + dy;
if (px >= 0 && px < DISPLAY_WIDTH && py >= 0 && py < DISPLAY_HEIGHT) {
staticFrameBuffer[py * DISPLAY_WIDTH + px] = colorSchemes[currentColorScheme]->black;
}
}
}
draw_line_to_buffer(staticFrameBuffer, west_x - 8, west_y - 8, west_x - 4, west_y + 8, colorSchemes[currentColorScheme]->text_color);
draw_line_to_buffer(staticFrameBuffer, west_x - 4, west_y + 8, west_x, west_y - 4, colorSchemes[currentColorScheme]->text_color);
draw_line_to_buffer(staticFrameBuffer, west_x, west_y - 4, west_x + 4, west_y + 8, colorSchemes[currentColorScheme]->text_color);
draw_line_to_buffer(staticFrameBuffer, west_x + 4, west_y + 8, west_x + 8, west_y - 8, colorSchemes[currentColorScheme]->text_color);
}
void draw_frame_border() {
const int gap_size = 10;
for (int r = FRAME_START; r < FRAME_START + FRAME_WIDTH; r++) {
draw_arc_to_buffer(staticFrameBuffer, CENTER_X, CENTER_Y, r,
gap_size, 90 - gap_size, colorSchemes[currentColorScheme]->frame_color);
draw_arc_to_buffer(staticFrameBuffer, CENTER_X, CENTER_Y, r,
90 + gap_size, 180 - gap_size, colorSchemes[currentColorScheme]->frame_color);
draw_arc_to_buffer(staticFrameBuffer, CENTER_X, CENTER_Y, r,
180 + gap_size, 270 - gap_size, colorSchemes[currentColorScheme]->frame_color);
draw_arc_to_buffer(staticFrameBuffer, CENTER_X, CENTER_Y, r,
270 + gap_size, 360 - gap_size, colorSchemes[currentColorScheme]->frame_color);
}
draw_radial_scale_marks();
draw_cardinal_directions();
}
void draw_radar_grid() {
for (int r = 50; r <= RADAR_RADIUS; r += 50) {
draw_circle_to_buffer(staticFrameBuffer, CENTER_X, CENTER_Y, r, colorSchemes[currentColorScheme]->grid_color);
}
draw_line_to_buffer(staticFrameBuffer, CENTER_X - RADAR_RADIUS, CENTER_Y, CENTER_X + RADAR_RADIUS, CENTER_Y, colorSchemes[currentColorScheme]->grid_color);
draw_line_to_buffer(staticFrameBuffer, CENTER_X, CENTER_Y - RADAR_RADIUS, CENTER_X, CENTER_Y + RADAR_RADIUS, colorSchemes[currentColorScheme]->grid_color);
}
void draw_radar_sweep() {
float rad = current_angle * PI / 180.0;
int end_x = CENTER_X + RADAR_RADIUS * sin(rad);
int end_y = CENTER_Y - RADAR_RADIUS * cos(rad);
for (int r = 0; r <= RADAR_RADIUS; r++) {
int trail_x = CENTER_X + r * sin(rad);
int trail_y = CENTER_Y - r * cos(rad);
int dx = trail_x - CENTER_X;
int dy = trail_y - CENTER_Y;
if (dx * dx + dy * dy <= RADAR_RADIUS * RADAR_RADIUS) {
uint16_t trail_color = colorSchemes[currentColorScheme]->trail_color;
if (r == RADAR_RADIUS) {
frameBuffer[trail_y * DISPLAY_WIDTH + trail_x] = colorSchemes[currentColorScheme]->sweep_color;
} else {
frameBuffer[trail_y * DISPLAY_WIDTH + trail_x] = trail_color;
}
}
}
current_angle += SWEEP_SPEED;
if (current_angle >= 360) {
current_angle = 0;
for (int y = 0; y < DISPLAY_HEIGHT; y++) {
for (int x = 0; x < DISPLAY_WIDTH; x++) {
int dx = x - CENTER_X;
int dy = y - CENTER_Y;
if (dx * dx + dy * dy <= RADAR_RADIUS * RADAR_RADIUS) {
frameBuffer[y * DISPLAY_WIDTH + x] = colorSchemes[currentColorScheme]->black;
}
}
}
for (int y = 0; y < DISPLAY_HEIGHT; y++) {
for (int x = 0; x < DISPLAY_WIDTH; x++) {
int dx = x - CENTER_X;
int dy = y - CENTER_Y;
if (dx * dx + dy * dy <= RADAR_RADIUS * RADAR_RADIUS) {
frameBuffer[y * DISPLAY_WIDTH + x] = staticFrameBuffer[y * DISPLAY_WIDTH + x];
}
}
}
}
}
void setup() {
Serial.begin(115200);
Serial.println("\n\nStarting ESP32S3 Radar Clock");
// Increase stability by setting WiFi to static mode
WiFi.mode(WIFI_STA);
WiFi.setAutoReconnect(true);
WiFi.persistent(true);
// Initialize display
init_display();
// Show title screen
startupStartTime = millis();
showTitleScreen();
Serial.println("Showing title screen...");
}
void loop() {
// Update startup sequence
updateStartupSequence();
// Only run clock functions when in clock mode
if (currentStartupState == SHOW_CLOCK) {
// Check for encoder rotation
handleEncoder();
// Redraw static elements if color scheme changed
if (colorSchemeChanged) {
redrawStaticElements();
}
// Update radar sweep
draw_radar_sweep();
// Update time display
update_time_display();
// Update display
gfx->draw16bitRGBBitmap(0, 0, frameBuffer, DISPLAY_WIDTH, DISPLAY_HEIGHT);
}
delay(50); // ~20 FPS
}










Comments