Mirko Pavleski
Published © GPL3+

Radar Clock on Elecrow’s 2.1 HMI Round Display

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

BeginnerFull instructions provided2 hours57
Radar Clock on Elecrow’s 2.1 HMI Round Display

Things used in this project

Hardware components

Elecrow CrowPanel 2.1inch-HMI ESP32 Rotary Display 480*480
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free

Story

Read more

Code

Code

C/C++
....
/*  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
}

Credits

Mirko Pavleski
196 projects • 1487 followers

Comments