From Unboxing to Coding - Radar Clock on Elecrow’s 2.1 HMI Display

This module from Elecrow has endless possibilities for making DIY projects in a relatively simple way, without need for soldering or other hardware work.

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.

CODE
/*  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
}
License
All Rights
Reserved
licensBg
0