Mirko Pavleski
Published © GPL3+

Making a Retro Analog NTP Clock with Unihiker K10 - Arduino

This project successfully demonstrates that the Unihiker K10 board, despite being AI-oriented, can effectively utilize standard Arduino lib.

BeginnerFull instructions provided1 hour53
Making a Retro Analog NTP Clock with Unihiker K10 - Arduino

Things used in this project

Hardware components

Unihiker K10
×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++
...
#include <WiFi.h>
#include "time.h"  // Built-in for NTP handling
#include <TFT_eSPI.h> // TFT_eSPI library
#include <math.h>  // For sin/cos calculations
#include "unihiker_k10.h"


UNIHIKER_K10 k10;

uint8_t screen_dir = 2;
Music music;  // For button functionality
bool showAnalog = true;  // To track which display is showing
bool first_digital = true;  // For digital display initialization

// Initialize display
TFT_eSPI tft = TFT_eSPI();

// Variables for digital display
int prev_hour = -1;
int prev_min = -1;
int prev_sec = -1;

uint16_t bgColors[] = {TFT_YELLOW, TFT_ORANGE, TFT_MAGENTA, TFT_OLIVE, TFT_WHITE};
int currentColorIndex = 0;
uint16_t currentBgColor = bgColors[0];


#define TFT_GREY 0x5AEB
#define TFT_LIGHTPINK 0xFDB8
#define TFT_GOLD 0xFEA0
#define TFT_LIGHTGREEN 0x9772
#define TFT_LIGHTSALMON 0xFD0F
#define TFT_ORANGE 0xFD20
#define TFT_MAGENTA 0xF81F
#define TFT_OLIVE 0x7BE0

// WiFi and NTP settings
const char* ssid = "*******";
const char* password = "*******";
const char* ntpServer = "pool.ntp.org";
long gmtOffset_sec = 3600;     // Timezone offset (UTC+1)
int daylightOffset_sec = 3600; // Daylight Saving Time

// Clock parameters
const int center_x = 160;      // Center of display
const int center_y = 120;      // Center of display
const int x_radius = 110;      // Horizontal radius
const int y_radius = 90;       // Vertical radius
const int ellipse_offset_x = 3;  // Ellipse offset right
const int ellipse_offset_y = -2; // Ellipse offset up
const int second_length = 85;
const int minute_length = 70;
const int hour_length = 50;
const int center_size = 5;
const int tick_size = 3;

// Previous hand positions
int prev_sx = center_x, prev_sy = center_y;
int prev_m1x, prev_m1y, prev_m2x, prev_m2y, prev_m3x, prev_m3y;
int prev_h1x, prev_h1y, prev_h2x, prev_h2y, prev_h3x, prev_h3y;

// Flag for first loop
bool first_loop = true;

// Function prototypes
void onButtonAPressed();
void onButtonBPressed();
void drawDigitalClock(struct tm timeinfo);

// Function to calculate elliptical points with offset
void getEllipsePoint(float angle, float* x, float* y) {
    *x = center_x + ellipse_offset_x + x_radius * sin(angle);
    *y = center_y + ellipse_offset_y - y_radius * cos(angle);
}

void drawDigitalClock(struct tm timeinfo) {
  if (first_digital) {
    tft.fillScreen(currentBgColor);
    
    // Draw outer frame (5 pixels thick)
    tft.fillRect(0, 0, 320, 5, TFT_DARKGREY);
    tft.fillRect(0, 235, 320, 5, TFT_DARKGREY);
    tft.fillRect(0, 0, 5, 240, TFT_DARKGREY);
    tft.fillRect(315, 0, 5, 240, TFT_DARKGREY);

    // Draw inner frame (2 pixels thick)
    tft.drawRoundRect(8, 8, 304, 2, 20, TFT_BLACK);
    tft.drawRoundRect(8, 230, 304, 2, 20, TFT_BLACK);
    tft.drawRoundRect(8, 8, 2, 224, 20, TFT_BLACK);
    tft.drawRoundRect(310, 8, 2, 224, 20, TFT_BLACK);

    first_digital = false;
  }

  // Set text properties for time
  tft.setTextSize(4);  // Size 4 for time
  tft.setTextColor(TFT_BLACK, currentBgColor);  // Red text on yellow background
  
  int current_hour = timeinfo.tm_hour;
  int current_min = timeinfo.tm_min;
  int current_sec = timeinfo.tm_sec;

  // Position for the time display - centered
  int x = 40;  // Adjusted for better centering
  int y = 80;  // Adjusted vertical position
  
  // Update hours if changed
  if (current_hour != prev_hour) {
    char hourStr[3];
    sprintf(hourStr, "%02d", current_hour);
    tft.fillRect(x, y, 70, 45, currentBgColor);  // Adjusted clear area
    tft.setCursor(x, y);
    tft.print(hourStr);
    prev_hour = current_hour;
  }
  
  // Always show colon
  tft.setCursor(x + 60, y);
  tft.print(":");

  // Update minutes if changed
  if (current_min != prev_min) {
    char minStr[3];
    sprintf(minStr, "%02d", current_min);
    tft.fillRect(x + 95, y, 70, 45, currentBgColor);  // Adjusted clear area
    tft.setCursor(x + 95, y);
    tft.print(minStr);
    prev_min = current_min;
  }

  // Always show colon
  tft.setCursor(x + 155, y);
  tft.print(":");

  // Update seconds if changed
  if (current_sec != prev_sec) {
    char secStr[3];
    sprintf(secStr, "%02d", current_sec);
    tft.fillRect(x + 190, y, 70, 45, currentBgColor);  // Adjusted clear area
    tft.setCursor(x + 190, y);
    tft.print(secStr);
    prev_sec = current_sec;
  }

  // Update date
  tft.setTextSize(3);  // Size 3 for date
  char dateStr[11];
  strftime(dateStr, sizeof(dateStr), "%Y-%m-%d", &timeinfo);
  
  // Center the date
  tft.fillRect(60, 150, 200, 30, currentBgColor);  // Clear previous date
  tft.setCursor(65, 150);  // Adjusted position for better centering
  tft.print(dateStr);
}
void onButtonAPressed() {
  showAnalog = !showAnalog;
  if (showAnalog) {
    // Instead of calling setup(), just redraw the analog clock
    tft.fillScreen(currentBgColor);  // Use current background color
    
    // Draw frames
    tft.fillRect(0, 0, 320, 5, TFT_DARKGREY);
    tft.fillRect(0, 235, 320, 5, TFT_DARKGREY);
    tft.fillRect(0, 0, 5, 240, TFT_DARKGREY);
    tft.fillRect(315, 0, 5, 240, TFT_DARKGREY);

    tft.drawRoundRect(8, 8, 304, 2, 20, TFT_BLACK);
    tft.drawRoundRect(8, 230, 304, 2, 20, TFT_BLACK);
    tft.drawRoundRect(8, 8, 2, 224, 20, TFT_BLACK);
    tft.drawRoundRect(310, 8, 2, 224, 20, TFT_BLACK);

    // Draw clock face
    tft.setTextSize(2);
    for (int i = 0; i < 60; i++) {
      float ang_rad = i * 6.0 * PI / 180.0;
      float tx, ty;
      getEllipsePoint(ang_rad, &tx, &ty);
      
      if (i % 5 != 0) {
        tft.fillCircle(tx, ty, 1, TFT_DARKGREY);
      }
    }

    for (int i = 1; i <= 12; i++) {
      float ang_rad = i * 30.0 * PI / 180.0;
      float tx, ty;
      getEllipsePoint(ang_rad, &tx, &ty);
      
      if (i % 3 == 0) {
        char num[3];
        sprintf(num, "%d", i);
        int text_offset_x, text_offset_y;
        
        if (i == 12) {
          text_offset_x = -12;
          text_offset_y = -6;
        } else if (i == 3) {
          text_offset_x = -4;
          text_offset_y = -6;
        } else if (i == 6) {
          text_offset_x = -6;
          text_offset_y = -6;
        } else if (i == 9) {
          text_offset_x = -6;
          text_offset_y = -6;
        }
        
        tft.setCursor(tx + text_offset_x, ty + text_offset_y);
        tft.print(num);
      } else {
        tft.fillCircle(tx, ty, tick_size, TFT_BLACK);
      }
    }
    tft.fillCircle(center_x, center_y, center_size, TFT_BLACK);
    first_loop = true;
  } else {
    // Reset digital clock variables
    prev_hour = -1;
    prev_min = -1;
    prev_sec = -1;
    first_digital = true;
  }
}

void onButtonBPressed() {
  // Change background color
  currentColorIndex = (currentColorIndex + 1) % 5;
  currentBgColor = bgColors[currentColorIndex];
  
  // Play three-tone ascending sequence
  music.playTone(262, 200);  // C4 (middle C)
  delay(250);  // Small delay between tones
  music.playTone(330, 200);  // E4
  delay(250);
  music.playTone(392, 200);  // G4
  
  // Redraw screen with new background color
  if (showAnalog) {
    onButtonAPressed();  // Redraw analog clock with new color
  } else {
    first_digital = true;  // Force complete redraw of digital display
  }
}

void setup() {
  Serial.begin(115200);

  k10.begin();
  k10.initScreen(screen_dir);
  
  // Add button callbacks
  k10.buttonA->setPressedCallback(onButtonAPressed);
  k10.buttonB->setPressedCallback(onButtonBPressed);

  // Initialize display
  tft.init();
  tft.setRotation(3);  // Landscape mode
  tft.fillScreen(currentBgColor);
  tft.setTextColor(TFT_BLACK, currentBgColor);

  // Connect to WiFi
  tft.setCursor(30, 100);
  tft.setTextSize(3);
  tft.print("Connecting...");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    tft.fillScreen(currentBgColor);
    Serial.print(".");
  }
  Serial.println("WiFi connected");
  tft.setCursor(55, 100);
  tft.setTextSize(3);
  tft.print("Connected!");
  delay(2000);
  tft.fillScreen(currentBgColor);

  // Draw outer frame (5 pixels thick)
  tft.fillRect(0, 0, 320, 5, TFT_DARKGREY);
  tft.fillRect(0, 235, 320, 5, TFT_DARKGREY);
  tft.fillRect(0, 0, 5, 240, TFT_DARKGREY);
  tft.fillRect(315, 0, 5, 240, TFT_DARKGREY);

  // Draw inner frame (2 pixels thick)
  tft.drawRoundRect(8, 8, 304, 2, 20, TFT_BLACK);
  tft.drawRoundRect(8, 230, 304, 2, 20, TFT_BLACK);
  tft.drawRoundRect(8, 8, 2, 224, 20, TFT_BLACK);
  tft.drawRoundRect(310, 8, 2, 224, 20, TFT_BLACK);

  // Initialize NTP
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);

  // Draw static clock face
  tft.setTextSize(2);
  
  // First draw all second markers (60 small dots)
  for (int i = 0; i < 60; i++) {
    float ang_rad = i * 6.0 * PI / 180.0;
    float tx, ty;
    getEllipsePoint(ang_rad, &tx, &ty);
    
    if (i % 5 != 0) {
      tft.fillCircle(tx, ty, 1, TFT_DARKGREY);
    }
  }

  // Then draw hour markers over the second markers
  for (int i = 1; i <= 12; i++) {
    float ang_rad = i * 30.0 * PI / 180.0;
    float tx, ty;
    getEllipsePoint(ang_rad, &tx, &ty);
    
    if (i % 3 == 0) {
      char num[3];
      sprintf(num, "%d", i);
      int text_offset_x, text_offset_y;
      
      if (i == 12) {
        text_offset_x = -12;
        text_offset_y = -6;
      } else if (i == 3) {
        text_offset_x = -4;
        text_offset_y = -6;
      } else if (i == 6) {
        text_offset_x = -6;
        text_offset_y = -6;
      } else if (i == 9) {
        text_offset_x = -6;
        text_offset_y = -6;
      }
      
      tft.setCursor(tx + text_offset_x, ty + text_offset_y);
      tft.print(num);
    } else {
      tft.fillCircle(tx, ty, tick_size, TFT_BLACK);
    }
  }
  tft.fillCircle(center_x, center_y, center_size, TFT_BLACK);
}

void loop() {
  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    if (showAnalog) {
      int sec = timeinfo.tm_sec;
      int min = timeinfo.tm_min;
      int hour = timeinfo.tm_hour;

      // Clear previous hands if not first loop
      if (!first_loop) {
        tft.drawTriangle(prev_h1x, prev_h1y, prev_h2x, prev_h2y, prev_h3x, prev_h3y, currentBgColor);
        tft.drawTriangle(prev_m1x, prev_m1y, prev_m2x, prev_m2y, prev_m3x, prev_m3y, currentBgColor);
        tft.drawLine(center_x, center_y, prev_sx, prev_sy, currentBgColor);
      }

      // Store current text settings
      uint8_t current_size = tft.textsize;
      uint8_t current_font = tft.textfont;

      // Display date with its own settings
      tft.setTextSize(1);
      tft.setTextFont(2);
      char dateStr[11];
      strftime(dateStr, sizeof(dateStr), "%Y-%m-%d", &timeinfo);
      tft.setCursor(130, 150);
      tft.print(dateStr);

      // Restore original text settings
      tft.setTextSize(current_size);
      tft.setTextFont(current_font);

      // Calculate new hour hand position
      float h_deg = (hour % 12) * 30.0 + min * 0.5 + sec * (0.5 / 60.0);
      float h_rad = h_deg * PI / 180.0;
      int hx = center_x + hour_length * sin(h_rad);
      int hy = center_y - hour_length * cos(h_rad);
      float hdx = sin(h_rad);
      float hdy = -cos(h_rad);
      float hpx = -hdy;
      float hpy = hdx;
      float base_half = 5.0;
      int p1x = center_x + base_half * hpx;
      int p1y = center_y + base_half * hpy;
      int p2x = center_x - base_half * hpx;
      int p2y = center_y - base_half * hpy;
      int p3x = hx;
      int p3y = hy;

      // Draw hour hand with cyan fill and black outline
      tft.fillTriangle(p1x, p1y, p2x, p2y, p3x, p3y, TFT_CYAN);
      tft.drawTriangle(p1x, p1y, p2x, p2y, p3x, p3y, TFT_BLACK);

      // Calculate new minute hand position
      float m_deg = min * 6.0 + sec * 0.1;
      float m_rad = m_deg * PI / 180.0;
      int mx = center_x + minute_length * sin(m_rad);
      int my = center_y - minute_length * cos(m_rad);
      float dx = sin(m_rad);
      float dy = -cos(m_rad);
      float px = -dy;
      float py = dx;
      float m_base_half = 5.0;
      int m1x = center_x + m_base_half * px;
      int m1y = center_y + m_base_half * py;
      int m2x = center_x - m_base_half * px;
      int m2y = center_y - m_base_half * py;
      int m3x = mx;
      int m3y = my;

      // Draw minute hand (outlined)
      tft.drawTriangle(m1x, m1y, m2x, m2y, m3x, m3y, TFT_BLACK);

      // Calculate new second hand position
      float s_deg = sec * 6.0;
      float s_rad = s_deg * PI / 180.0;
      int sx = center_x + second_length * sin(s_rad);
      int sy = center_y - second_length * cos(s_rad);

      // Draw second hand
      tft.drawLine(center_x, center_y, sx, sy, TFT_RED);

      // Draw center dot
      tft.fillCircle(center_x, center_y, center_size, TFT_BLACK);

      // Update previous positions
      prev_h1x = p1x; prev_h1y = p1y;
      prev_h2x = p2x; prev_h2y = p2y;
      prev_h3x = p3x; prev_h3y = p3y;
      prev_m1x = m1x; prev_m1y = m1y;
      prev_m2x = m2x; prev_m2y = m2y;
      prev_m3x = m3x; prev_m3y = m3y;
      prev_sx = sx; prev_sy = sy;

      first_loop = false;
    } else {
      drawDigitalClock(timeinfo);
    }
  } else {
    Serial.println("Failed to obtain time");
  }
  delay(1000);
}

Credits

Mirko Pavleski
195 projects • 1480 followers

Comments