Mirko Pavleski
Published © GPL3+

DIY Smart Code Lock with CrowPanel ESP32 Rotary Display

In this video, I demonstrate the simplest way to build an advanced, keyless security device using the CrowPanel 1.28-inch-HMI ESP32 Display

BeginnerFull instructions provided3 hours140
DIY Smart Code Lock with CrowPanel ESP32 Rotary Display

Things used in this project

Hardware components

https://www.elecrow.com/crowpanel-1-28inch-hmi-esp32-rotary-display-240-240-ips-round-touch-knob-screen.html?idd=5
×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

Arduino Code

C/C++
....
//KRAEN KOD SE ZAEDNO I EKRAN I TELEFON
/*
  CrowPanel 1.28" (ESP32-S3 + GC9A01, TFT_eSPI)
  Електронска брава со ротационен енкодер и WEB интерфејс
  - Промена на лозинка преку веб
  - Зачувување во EEPROM
*/

#include <Arduino.h>
#include <TFT_eSPI.h>
#include <SPI.h>
#include <Wire.h>
#include <math.h>
#include <Adafruit_NeoPixel.h>
#include <WiFi.h>
#include <WebServer.h>
#include <EEPROM.h>

// === КОНФИГУРАЦИЈА НА EEPROM ===
#define EEPROM_SIZE 64
#define CODE_SAVE_ADDRESS 0  // Адреса каде ќе се чува кодот (5 бајти)

// === КОНФИГУРАЦИЈА НА ДИСПЛЕЈ ===
#define USE_PANEL_ENABLE_PINS  1
#define PIN_LCD_PWR_EN1   1
#define PIN_LCD_PWR_EN2   2
#define PIN_TFT_BL        46
#define PIN_TFT_RST       14

// === КОНФИГУРАЦИЈА НА WI-FI ACCESS POINT ===
const char* ap_ssid = "SmartLock_ESP32";
const char* ap_password = "12345678";  // Минимум 8 карактери
IPAddress local_IP(192, 168, 4, 1);
IPAddress gateway(192, 168, 4, 1);
IPAddress subnet(255, 255, 255, 0);

WebServer server(80);

// === NEO PIXEL LED ===
#define LED_PIN 48          // Пин за LED лента
#define LED_COUNT 5         // 5 LED диоди
#define LED_BRIGHTNESS 50   // Јачина (0-255)
#define LED_ROTATION_TIME 100  // Време на ротација (ms) - секои 100ms се менува LED

Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
int ledPosition = 0;        // Тековна LED позиција
bool ledEffectActive = false; // Дали LED ефектот е активен
unsigned long ledEffectStartTime = 0; // Време на почеток на ефект
unsigned long lastLEDUpdateTime = 0; // Време на последно ажурирање на LED
int ledEffectDuration = 5000; // Времетраење на ефект (5 секунди)
uint32_t ledEffectColor = 0; // Боја на ефектот

// === ПАРАМЕТРИ НА ПРСТЕНИ ===
#define OUTER_CIRCLE_THICKNESS 8   // Дебелина на надворешниот прстен (пиксели)
#define INNER_CIRCLE_THICKNESS 5    // Дебелина на внатрешниот прстен (пиксели)
#define CIRCLE_GAP 5                // Празно место меѓу прстените (пиксели)
#define OUTER_CIRCLE_COLOR TFT_BLUE // Светло сина боја за надворешниот круг
#define INNER_CIRCLE_COLOR TFT_PURPLE // Лилава боја за внатрешниот круг
#define SUCCESS_CIRCLE_COLOR TFT_GREEN   // Боја за успешна лозинка
#define ERROR_CIRCLE_COLOR TFT_RED       // Боја за грешна лозинка

// === БОЈИ НА ТЕКСТ ===
#define TOP_NUMBER_COLOR 0x3D7D  // Светло сина боја на горната цифра
#define TOP_NUMBER_SUCCESS_COLOR TFT_GREEN  // Зелена боја на горната цифра за успех
#define TOP_NUMBER_ERROR_COLOR TFT_RED      // Црвена боја на горната цифра за грешка
#define BOTTOM_CODE_COLOR TFT_GREEN // Зелена боја на долните цифри од кодот
#define BOTTOM_CODE_SUCCESS_COLOR TFT_GREEN // Зелена боја за успешен код
#define BOTTOM_CODE_ERROR_COLOR TFT_RED     // Црвена боја за грешен код
#define RECTANGLE_COLOR TFT_WHITE     // Боја на правоаголникот
#define ENTER_PIN_TEXT_COLOR TFT_WHITE  // Боја на текстот "enter your pin:"
#define STATUS_TEXT_COLOR TFT_WHITE    // Боја на текстовите "door open" и "access denied"
#define WIFI_TEXT_COLOR TFT_CYAN       // Боја за Wi-Fi статус текстот

// === ПОДЕЛБА НА ЕКРАНОТ ===
#define SCREEN_WIDTH 240
#define SCREEN_HEIGHT 240
#define SCREEN_CENTER_X (SCREEN_WIDTH / 2)
#define SCREEN_CENTER_Y (SCREEN_HEIGHT / 2)
#define TOP_PART_HEIGHT 160  // Горниот дел (2/3 од екранот)
#define BOTTOM_PART_HEIGHT 80 // Долниот дел (1/3 од екранот)
#define DIVIDER_LINE_COLOR TFT_WHITE  // Боја на разделната линија
#define HORIZONTAL_LINE_THICKNESS 2   // Дебелина на хоризонталната линија
#define INNER_WHITE_CIRCLE_THICKNESS 4 // Дебелина на внатрешниот бел круг
#define NUMBER_Y_OFFSET 15             // Поместување на бројката надолу (пиксели)
#define ENTER_PIN_TEXT_Y_OFFSET -10    // Поместување на текстот "enter your pin:" во однос на линијата
#define STATUS_TEXT_Y_OFFSET -10       // Поместување на статус текстовите
#define WIFI_STATUS_Y_OFFSET 20        // Поместување на Wi-Fi статус текстот

// === ПРАВОАГОЛНИК ЗА КОД ===
#define RECTANGLE_THICKNESS 3       // Дебелина на линиите на правоаголникот
#define RECTANGLE_PADDING 7          // Простор помеѓу цифрите и правоаголникот
#define RECTANGLE_CORNER_RADIUS 8     // Заоблени агли на правоаголникот
#define CODE_VERTICAL_OFFSET -10     // Поместување на кодот нагоре (негативно = нагоре)

// === ПИНОВИ ЗА ЕНКОДЕР ===
#define ENC_A 45
#define ENC_B 42
#define ENC_BTN 41

TFT_eSPI tft;

// === ПРОМЕНЛИВИ ЗА СИСТЕМОТ ===
volatile int8_t encQuart = 0;
int currentNumber = 0;              // Тековно избрана цифра (0-9)
int enteredCode[5] = { -1, -1, -1, -1, -1 };  // Внесен код
int codePosition = 0;               // Позиција во внесувањето
int correctCode[5] = {1, 2, 3, 4, 5};  // Точен код (сега променлив)
bool isChecking = false;            // Дали се проверува лозинка
unsigned long circleColorEndTime = 0;  // Време кога бојата на прстените треба да се врати
bool showStatusMessage = false;     // Дали да се прикаже статус порака
String statusMessage = "";          // Текст на статус пораката
uint32_t statusCircleColor = 0;     // Боја на круговите за статус
uint32_t currentTopNumberColor = TOP_NUMBER_COLOR; // Тековна боја на горната цифра
uint32_t currentBottomCodeColor = BOTTOM_CODE_COLOR; // Тековна боја на долните цифри
bool showWiFiStatus = false;        // Дали да се прикаже Wi-Fi статус
String wifiStatusMessage = "";      // Wi-Fi статус порака
unsigned long wifiStatusEndTime = 0; // Време до кога да се прикажува Wi-Fi статус

// === HTML СТРАНА ЗА ВЕБ ИНТЕРФЕЈС ===
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta charset="UTF-8">
  <style>
    body {
      font-family: Arial, sans-serif;
      text-align: center;
      background-color: #1a1a2e;
      color: #ffffff;
      margin: 0;
      padding: 20px;
    }
    .container {
      max-width: 400px;
      margin: 0 auto;
      background-color: #16213e;
      padding: 30px;
      border-radius: 20px;
      box-shadow: 0 0 20px rgba(0,0,0,0.5);
    }
    h1 {
      color: #4ecca3;
      margin-bottom: 30px;
    }
    h2 {
      color: #4ecca3;
      font-size: 20px;
      margin-top: 30px;
      border-top: 2px solid #4ecca3;
      padding-top: 20px;
    }
    .code-display {
      background-color: #0f3460;
      padding: 15px;
      border-radius: 15px;
      margin-bottom: 25px;
      font-size: 32px;
      letter-spacing: 8px;
      font-family: monospace;
      color: #4ecca3;
      border: 3px solid #4ecca3;
    }
    .keypad {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 8px;
      margin-bottom: 25px;
    }
    .key {
      background-color: #0f3460;
      border: none;
      color: white;
      padding: 12px;
      font-size: 22px;
      border-radius: 12px;
      cursor: pointer;
      transition: all 0.3s;
      box-shadow: 0 4px 0 #070d1f;
    }
    .key:hover {
      background-color: #1a4d8c;
      transform: translateY(-2px);
      box-shadow: 0 6px 0 #070d1f;
    }
    .key:active {
      transform: translateY(4px);
      box-shadow: 0 2px 0 #070d1f;
    }
    .menu-buttons {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 20px;
      margin: 40px 0;
    }
    .menu-btn {
      padding: 25px 15px;
      font-size: 24px;
      border: none;
      border-radius: 15px;
      cursor: pointer;
      font-weight: bold;
      transition: all 0.3s;
      box-shadow: 0 8px 0 #070d1f;
      width: 100%;
      min-height: 120px;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .btn-unlock {
      background-color: #4ecca3;
      color: #16213e;
    }
    .btn-change {
      background-color: #ffa500;
      color: #16213e;
    }
    .menu-btn:hover {
      opacity: 0.9;
      transform: translateY(-2px);
      box-shadow: 0 10px 0 #070d1f;
    }
    .menu-btn:active {
      transform: translateY(8px);
      box-shadow: 0 2px 0 #070d1f;
    }
    .action-buttons {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 12px;
      margin-bottom: 15px;
    }
    .btn {
      padding: 12px;
      font-size: 16px;
      border: none;
      border-radius: 10px;
      cursor: pointer;
      font-weight: bold;
      transition: all 0.3s;
    }
    .btn-clear {
      background-color: #e94560;
      color: white;
    }
    .btn-submit {
      background-color: #4ecca3;
      color: #16213e;
    }
    .btn:hover {
      opacity: 0.9;
      transform: scale(1.02);
    }
    .status {
      margin-top: 15px;
      padding: 10px;
      border-radius: 10px;
      font-weight: bold;
      font-size: 14px;
    }
    .success {
      background-color: #4ecca3;
      color: #16213e;
    }
    .error {
      background-color: #e94560;
      color: white;
    }
    .info {
      margin-top: 15px;
      font-size: 12px;
      color: #888;
    }
    .back-btn {
      background-color: #e94560;
      color: white;
      width: 100%;
      margin-top: 15px;
      padding: 12px;
      font-size: 16px;
      border: none;
      border-radius: 10px;
      cursor: pointer;
      font-weight: bold;
      transition: all 0.3s;
    }
    .back-btn:hover {
      opacity: 0.9;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>🔐 SMART LOCK</h1>
    
    <div id="mainMenu">
      <div class="menu-buttons">
        <button class="menu-btn btn-unlock" onclick="showUnlockMenu()">🔓 ОТКЛУЧИ</button>
        <button class="menu-btn btn-change" onclick="showChangePinMenu()">⚙ ПРОМЕНИ PIN</button>
      </div>
    </div>
    
    <div id="unlockMenu" style="display:none;">
      <h2>Внеси код за отклучување</h2>
      <div class="code-display" id="codeDisplay">_ _ _ _ _</div>
      
      <div class="keypad">
        <button class="key" onclick="addDigit(1)">1</button>
        <button class="key" onclick="addDigit(2)">2</button>
        <button class="key" onclick="addDigit(3)">3</button>
        <button class="key" onclick="addDigit(4)">4</button>
        <button class="key" onclick="addDigit(5)">5</button>
        <button class="key" onclick="addDigit(6)">6</button>
        <button class="key" onclick="addDigit(7)">7</button>
        <button class="key" onclick="addDigit(8)">8</button>
        <button class="key" onclick="addDigit(9)">9</button>
        <button class="key" onclick="addDigit(0)">0</button>
        <button class="key" onclick="addDigit(0)" style="opacity:0; cursor:default;"></button>
        <button class="key" onclick="addDigit(0)" style="opacity:0; cursor:default;"></button>
      </div>
      
      <div class="action-buttons">
        <button class="btn btn-clear" onclick="clearCode()">ИЗБРИШИ</button>
        <button class="btn btn-submit" onclick="submitCode()">ВНЕСИ</button>
      </div>
      <button class="back-btn" onclick="backToMain()">◄ НАЗАД</button>
    </div>
    
    <div id="changePinMenu" style="display:none;">
      <h2>Промени PIN код</h2>
      <p style="color:#888; font-size:14px; margin:5px;">Внеси стар PIN</p>
      <div class="code-display" id="oldCodeDisplay">_ _ _ _ _</div>
      <p style="color:#888; font-size:14px; margin:5px;">Внеси нов PIN (5 цифри)</p>
      <div class="code-display" id="newCodeDisplay">_ _ _ _ _</div>
      <p style="color:#888; font-size:14px; margin:5px;">Потврди нов PIN</p>
      <div class="code-display" id="confirmCodeDisplay">_ _ _ _ _</div>
      
      <div class="keypad">
        <button class="key" onclick="addDigitChange(1)">1</button>
        <button class="key" onclick="addDigitChange(2)">2</button>
        <button class="key" onclick="addDigitChange(3)">3</button>
        <button class="key" onclick="addDigitChange(4)">4</button>
        <button class="key" onclick="addDigitChange(5)">5</button>
        <button class="key" onclick="addDigitChange(6)">6</button>
        <button class="key" onclick="addDigitChange(7)">7</button>
        <button class="key" onclick="addDigitChange(8)">8</button>
        <button class="key" onclick="addDigitChange(9)">9</button>
        <button class="key" onclick="addDigitChange(0)">0</button>
        <button class="key" onclick="addDigitChange(0)" style="opacity:0; cursor:default;"></button>
        <button class="key" onclick="addDigitChange(0)" style="opacity:0; cursor:default;"></button>
      </div>
      
      <div class="action-buttons">
        <button class="btn btn-clear" onclick="clearChangeCode()">ИЗБРИШИ</button>
        <button class="btn btn-submit" onclick="submitChangePin()">ПРОМЕНИ</button>
      </div>
      <button class="back-btn" onclick="backToMain()">◄ НАЗАД</button>
    </div>
    
    <div class="status" id="status"></div>
    <div class="info">Поврзани сте на: SmartLock_ESP32</div>
  </div>

  <script>
    let code = [];
    let oldCode = [];
    let newCode = [];
    let confirmCode = [];
    let changePinStep = 1; // 1=old, 2=new, 3=confirm
    
    function showUnlockMenu() {
      document.getElementById('mainMenu').style.display = 'none';
      document.getElementById('unlockMenu').style.display = 'block';
      document.getElementById('changePinMenu').style.display = 'none';
      clearCode();
      document.getElementById('status').innerHTML = '';
    }
    
    function showChangePinMenu() {
      document.getElementById('mainMenu').style.display = 'none';
      document.getElementById('unlockMenu').style.display = 'none';
      document.getElementById('changePinMenu').style.display = 'block';
      clearChangeCode();
      document.getElementById('status').innerHTML = '';
      changePinStep = 1;
    }
    
    function backToMain() {
      document.getElementById('mainMenu').style.display = 'block';
      document.getElementById('unlockMenu').style.display = 'none';
      document.getElementById('changePinMenu').style.display = 'none';
    }
    
    function updateDisplay() {
      let display = '';
      for(let i = 0; i < 5; i++) {
        if(i < code.length) {
          display += code[i] + ' ';
        } else {
          display += '_ ';
        }
      }
      document.getElementById('codeDisplay').innerText = display;
    }
    
    function updateChangeDisplay() {
      // Прикажи го стариот PIN
      let oldDisplay = '';
      for(let i = 0; i < 5; i++) {
        if(i < oldCode.length) {
          oldDisplay += oldCode[i] + ' ';
        } else {
          oldDisplay += '_ ';
        }
      }
      document.getElementById('oldCodeDisplay').innerText = oldDisplay;
      
      // Прикажи го новиот PIN
      let newDisplay = '';
      for(let i = 0; i < 5; i++) {
        if(i < newCode.length) {
          newDisplay += newCode[i] + ' ';
        } else {
          newDisplay += '_ ';
        }
      }
      document.getElementById('newCodeDisplay').innerText = newDisplay;
      
      // Прикажи го потврдниот PIN
      let confirmDisplay = '';
      for(let i = 0; i < 5; i++) {
        if(i < confirmCode.length) {
          confirmDisplay += confirmCode[i] + ' ';
        } else {
          confirmDisplay += '_ ';
        }
      }
      document.getElementById('confirmCodeDisplay').innerText = confirmDisplay;
    }
    
    function addDigit(digit) {
      if(code.length < 5) {
        code.push(digit);
        updateDisplay();
      }
    }
    
    function addDigitChange(digit) {
      if(changePinStep == 1 && oldCode.length < 5) {
        oldCode.push(digit);
      } else if(changePinStep == 2 && newCode.length < 5) {
        newCode.push(digit);
      } else if(changePinStep == 3 && confirmCode.length < 5) {
        confirmCode.push(digit);
      }
      updateChangeDisplay();
      
      // Автоматски премини на следниот чекор
      if(changePinStep == 1 && oldCode.length == 5) {
        changePinStep = 2;
      } else if(changePinStep == 2 && newCode.length == 5) {
        changePinStep = 3;
      }
    }
    
    function clearCode() {
      code = [];
      updateDisplay();
    }
    
    function clearChangeCode() {
      oldCode = [];
      newCode = [];
      confirmCode = [];
      changePinStep = 1;
      updateChangeDisplay();
    }
    
    function submitCode() {
      if(code.length !== 5) {
        document.getElementById('status').innerHTML = '<div style="color: #e94560;">Внесете 5 цифри</div>';
        return;
      }
      
      fetch('/submit', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: 'code=' + code.join('')
      })
      .then(response => response.text())
      .then(data => {
        if(data.includes('correct')) {
          document.getElementById('status').innerHTML = '<div class="success">✓ УСПЕШНО ОТВОРЕНА ВРАТА</div>';
        } else {
          document.getElementById('status').innerHTML = '<div class="error">✗ ГРЕШЕН КОД</div>';
        }
        clearCode();
      });
    }
    
    function submitChangePin() {
      if(oldCode.length !== 5 || newCode.length !== 5 || confirmCode.length !== 5) {
        document.getElementById('status').innerHTML = '<div class="error">✗ Внесете ги сите полиња</div>';
        return;
      }
      
      if(newCode.join('') !== confirmCode.join('')) {
        document.getElementById('status').innerHTML = '<div class="error">✗ Новите кодови не се совпаѓаат</div>';
        return;
      }
      
      fetch('/changePin', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: 'old=' + oldCode.join('') + '&new=' + newCode.join('')
      })
      .then(response => response.text())
      .then(data => {
        if(data.includes('success')) {
          document.getElementById('status').innerHTML = '<div class="success">✓ PIN КОДОТ Е УСПЕШНО ПРОМЕНЕТ</div>';
          clearChangeCode();
          setTimeout(() => backToMain(), 2000);
        } else {
          document.getElementById('status').innerHTML = '<div class="error">✗ ГРЕШКА: Погрешен стар PIN</div>';
          clearChangeCode();
        }
      });
    }
  </script>
</body>
</html>
)rawliteral";

// === LED ФУНКЦИИ ===
void initLEDs() {
  strip.begin();
  strip.setBrightness(LED_BRIGHTNESS);
  strip.clear();
  strip.show();
}

void clearLEDs() {
  for (int i = 0; i < LED_COUNT; i++) {
    strip.setPixelColor(i, 0, 0, 0);
  }
  strip.show();
}

void startLEDEffect(uint32_t color, int durationMs) {
  ledEffectActive = true;
  ledEffectStartTime = millis();
  ledEffectDuration = durationMs;
  ledEffectColor = color;
  ledPosition = 0;
  lastLEDUpdateTime = millis();
  
  strip.clear();
  strip.setPixelColor(ledPosition, ledEffectColor);
  strip.show();
}

void stopLEDEffect() {
  ledEffectActive = false;
  clearLEDs();
}

void updateLEDEffect() {
  if (!ledEffectActive) return;
  
  if (millis() - ledEffectStartTime > ledEffectDuration) {
    stopLEDEffect();
    return;
  }
  
  if (millis() - lastLEDUpdateTime >= LED_ROTATION_TIME) {
    lastLEDUpdateTime = millis();
    
    strip.setPixelColor(ledPosition, 0, 0, 0);
    
    ledPosition++;
    if (ledPosition >= LED_COUNT) {
      ledPosition = 0;
    }
    
    strip.setPixelColor(ledPosition, ledEffectColor);
    strip.show();
  }
}

// === ФУНКЦИИ ЗА МОЌНА ДИСПЛЕЈ ===
static void panelPowerOn() {
  if(USE_PANEL_ENABLE_PINS) {
    pinMode(PIN_LCD_PWR_EN1, OUTPUT);
    pinMode(PIN_LCD_PWR_EN2, OUTPUT);
    digitalWrite(PIN_LCD_PWR_EN1, HIGH);
    digitalWrite(PIN_LCD_PWR_EN2, HIGH);
    delay(5);
  }
}

static void pulseResetPin() {
  pinMode(PIN_TFT_RST, OUTPUT);
  digitalWrite(PIN_TFT_RST, HIGH);
  delay(5);
  digitalWrite(PIN_TFT_RST, LOW);
  delay(10);
  digitalWrite(PIN_TFT_RST, HIGH);
  delay(20);
}

static void backlightInit(uint8_t duty) {
  pinMode(PIN_TFT_BL, OUTPUT);
  digitalWrite(PIN_TFT_BL, duty > 0 ? HIGH : LOW);
}

// === ФУНКЦИИ ЗА ЕНКОДЕР ===
int8_t readEncoderTransition() {
  static int last = 0;
  int a = digitalRead(ENC_A);
  int b = digitalRead(ENC_B);
  int val = (a << 1) | b;
  
  static const int8_t trans[16] = {
    0, -1, +1, 0,
    +1, 0, 0, -1,
    -1, 0, 0, +1,
    0, +1, -1, 0
  };
  
  int8_t d = trans[(last << 2) | val];
  last = val;
  return d;
}

int8_t readEncoderDetent() {
  int8_t t = readEncoderTransition();
  if(t) {
    encQuart += t;
    if(encQuart >= 4) {
      encQuart = 0;
      return +1;
    }
    if(encQuart <= -4) {
      encQuart = 0;
      return -1;
    }
  }
  return 0;
}

// === ФУНКЦИИ ЗА EEPROM ===
void loadCodeFromEEPROM() {
  EEPROM.begin(EEPROM_SIZE);
  
  // Провери дали има зачуван код
  bool hasValidCode = true;
  for (int i = 0; i < 5; i++) {
    int val = EEPROM.read(CODE_SAVE_ADDRESS + i);
    if (val < 0 || val > 9) {
      hasValidCode = false;
      break;
    }
    correctCode[i] = val;
  }
  
  // Ако нема валиден код, користи го дефаултниот
  if (!hasValidCode) {
    int defaultCode[5] = {1, 2, 3, 4, 5};
    for (int i = 0; i < 5; i++) {
      correctCode[i] = defaultCode[i];
    }
    saveCodeToEEPROM();
  }
  
  EEPROM.end();
  
  Serial.print("Вчитан код од EEPROM: ");
  for (int i = 0; i < 5; i++) {
    Serial.print(correctCode[i]);
  }
  Serial.println();
}

void saveCodeToEEPROM() {
  EEPROM.begin(EEPROM_SIZE);
  
  for (int i = 0; i < 5; i++) {
    EEPROM.write(CODE_SAVE_ADDRESS + i, correctCode[i]);
  }
  
  EEPROM.commit();
  EEPROM.end();
  
  Serial.print("Зачуван код во EEPROM: ");
  for (int i = 0; i < 5; i++) {
    Serial.print(correctCode[i]);
  }
  Serial.println();
}

// === ФУНКЦИИ ЗА ЦРТАЊЕ ===
void drawOuterCircle(uint32_t color) {
  int maxRadius = min(SCREEN_WIDTH, SCREEN_HEIGHT) / 2 - 1;
  int outerRadius = maxRadius;
  int innerOuterRadius = outerRadius - OUTER_CIRCLE_THICKNESS;
  
  tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, outerRadius, color);
  tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, innerOuterRadius, color);
  
  for (int r = innerOuterRadius + 1; r < outerRadius; r++) {
    tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, r, color);
  }
}

void drawInnerCircle(uint32_t color) {
  int maxRadius = min(SCREEN_WIDTH, SCREEN_HEIGHT) / 2 - 1;
  int innerRadius = maxRadius - (OUTER_CIRCLE_THICKNESS + CIRCLE_GAP);
  int innerInnerRadius = innerRadius - INNER_CIRCLE_THICKNESS;
  
  tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, innerRadius, color);
  tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, innerInnerRadius, color);
  
  for (int r = innerInnerRadius + 1; r < innerRadius; r++) {
    tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, r, color);
  }
}

void drawCircles(uint32_t outerColor, uint32_t innerColor) {
  drawOuterCircle(outerColor);
  drawInnerCircle(innerColor);
}

void drawWhiteInnerCircle() {
  int maxRadius = min(SCREEN_WIDTH, SCREEN_HEIGHT) / 2 - 1;
  int whiteCircleRadius = maxRadius - (OUTER_CIRCLE_THICKNESS + CIRCLE_GAP + INNER_CIRCLE_THICKNESS + 10);
  int innerWhiteRadius = whiteCircleRadius - INNER_WHITE_CIRCLE_THICKNESS;
  
  tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, whiteCircleRadius, TFT_WHITE);
  tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, innerWhiteRadius, TFT_WHITE);
  
  for (int r = innerWhiteRadius + 1; r < whiteCircleRadius; r++) {
    tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, r, TFT_WHITE);
  }
}

void clearCirclesArea() {
  int maxRadius = min(SCREEN_WIDTH, SCREEN_HEIGHT) / 2 - 1;
  for (int r = maxRadius - (OUTER_CIRCLE_THICKNESS + CIRCLE_GAP + INNER_CIRCLE_THICKNESS + 15); r <= maxRadius; r++) {
    tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, r, TFT_BLACK);
  }
}

void drawHorizontalDividerLine() {
  int lineY = TOP_PART_HEIGHT;
  for (int i = 0; i < HORIZONTAL_LINE_THICKNESS; i++) {
    tft.drawFastHLine(0, lineY + i, SCREEN_WIDTH, DIVIDER_LINE_COLOR);
  }
}

void drawEnterPinText() {
  tft.setTextDatum(MC_DATUM);
  tft.setTextFont(2);
  tft.setTextColor(ENTER_PIN_TEXT_COLOR, TFT_BLACK);
  int textY = TOP_PART_HEIGHT + ENTER_PIN_TEXT_Y_OFFSET;
  tft.drawString("enter your pin:", SCREEN_CENTER_X, textY);
}

void drawWiFiStatusText() {
  tft.setTextDatum(MC_DATUM);
  tft.setTextFont(1);
  tft.setTextColor(WIFI_TEXT_COLOR, TFT_BLACK);
  int textY = TOP_PART_HEIGHT + WIFI_STATUS_Y_OFFSET;
  tft.drawString(wifiStatusMessage, SCREEN_CENTER_X, textY);
}

void drawStatusText() {
  tft.setTextDatum(MC_DATUM);
  tft.setTextFont(2);
  tft.setTextColor(STATUS_TEXT_COLOR, TFT_BLACK);
  int textY = TOP_PART_HEIGHT + STATUS_TEXT_Y_OFFSET;
  tft.drawString(statusMessage, SCREEN_CENTER_X, textY);
}

void clearTextAreaAboveLine() {
  int textY = TOP_PART_HEIGHT + ENTER_PIN_TEXT_Y_OFFSET;
  int textHeight = 20;
  tft.fillRect(0, textY - textHeight/2, SCREEN_WIDTH, textHeight, TFT_BLACK);
}

void drawRoundedRectangle(int x, int y, int width, int height, int radius, uint32_t color, int thickness) {
  if (thickness == 1) {
    tft.drawRoundRect(x, y, width, height, radius, color);
  } else {
    for (int i = 0; i < thickness; i++) {
      tft.drawRoundRect(x - i, y - i, width + 2*i, height + 2*i, radius + i, color);
    }
  }
}

void drawCodeRectangle() {
  String codeStr = "";
  for (int i = 0; i < 5; i++) {
    if (enteredCode[i] >= 0) {
      codeStr += String(enteredCode[i]);
    } else {
      codeStr += "_";
    }
    if (i < 4) codeStr += "   ";
  }
  
  tft.setTextFont(4);
  int textWidth = tft.textWidth(codeStr);
  int textHeight = 40;
  
  int rectX = SCREEN_CENTER_X - textWidth/2 - RECTANGLE_PADDING;
  int rectY = TOP_PART_HEIGHT + (BOTTOM_PART_HEIGHT / 2) - textHeight/2 - RECTANGLE_PADDING + CODE_VERTICAL_OFFSET+7;
  int rectWidth = textWidth + 2 * RECTANGLE_PADDING;
  int rectHeight = textHeight + 1 * RECTANGLE_PADDING;
  
  if (rectY < TOP_PART_HEIGHT) {
    rectY = TOP_PART_HEIGHT + 10;
  }
  
  if (rectY + rectHeight > TOP_PART_HEIGHT + BOTTOM_PART_HEIGHT) {
    rectHeight = (TOP_PART_HEIGHT + BOTTOM_PART_HEIGHT) - rectY - 5;
  }
  
  drawRoundedRectangle(rectX, rectY, rectWidth, rectHeight, RECTANGLE_CORNER_RADIUS, RECTANGLE_COLOR, RECTANGLE_THICKNESS);
}

void drawTopNumber() {
  tft.fillRect(SCREEN_CENTER_X - 50, TOP_PART_HEIGHT/2 - 40 + NUMBER_Y_OFFSET, 100, 80, TFT_BLACK);
  
  tft.setTextDatum(MC_DATUM);
  tft.setTextFont(7);
  tft.setTextColor(currentTopNumberColor, TFT_BLACK);
  
  char numStr[2];
  sprintf(numStr, "%d", currentNumber);
  
  int numberY = (TOP_PART_HEIGHT / 2) + NUMBER_Y_OFFSET;
  tft.drawString(numStr, SCREEN_CENTER_X, numberY);
}

void drawTopPart() {
  tft.fillRect(0, 0, SCREEN_WIDTH, TOP_PART_HEIGHT, TFT_BLACK);
  
  drawCircles(OUTER_CIRCLE_COLOR, INNER_CIRCLE_COLOR);
  drawWhiteInnerCircle();
  drawHorizontalDividerLine();
  
  if (!showStatusMessage) {
    drawEnterPinText();
  } else {
    drawStatusText();
  }
  
  if (showWiFiStatus && millis() < wifiStatusEndTime) {
    drawWiFiStatusText();
  }
  
  drawTopNumber();
}

void drawBottomPart() {
  tft.fillRect(0, TOP_PART_HEIGHT, SCREEN_WIDTH, BOTTOM_PART_HEIGHT, TFT_BLACK);
  
  String codeStr = "";
  for (int i = 0; i < 5; i++) {
    if (enteredCode[i] >= 0) {
      codeStr += String(enteredCode[i]);
    } else {
      codeStr += "_";
    }
    if (i < 4) codeStr += "   ";
  }
  
  drawCodeRectangle();
  
  tft.setTextDatum(MC_DATUM);
  tft.setTextFont(4);
  tft.setTextColor(currentBottomCodeColor, TFT_BLACK);
  
  int bottomCenterY = TOP_PART_HEIGHT + (BOTTOM_PART_HEIGHT / 2) + CODE_VERTICAL_OFFSET;
  tft.drawString(codeStr, SCREEN_CENTER_X, bottomCenterY);
  
  drawHorizontalDividerLine();
  
  if (!showStatusMessage) {
    drawEnterPinText();
  } else {
    drawStatusText();
  }
  
  if (showWiFiStatus && millis() < wifiStatusEndTime) {
    drawWiFiStatusText();
  }
}

void clearEnteredCode() {
  for (int i = 0; i < 5; i++) {
    enteredCode[i] = -1;
  }
  codePosition = 0;
  drawBottomPart();
}

bool checkCode() {
  for (int i = 0; i < 5; i++) {
    if (enteredCode[i] != correctCode[i]) {
      return false;
    }
  }
  return true;
}

void showWiFiMessage(String message, int durationMs = 3000) {
  showWiFiStatus = true;
  wifiStatusMessage = message;
  wifiStatusEndTime = millis() + durationMs;
  
  drawTopPart();
  drawBottomPart();
}

void updateCircleColor(uint32_t circleColor, int durationMs, String message) {
  showStatusMessage = true;
  statusMessage = message;
  statusCircleColor = circleColor;
  
  if (message == "door open") {
    currentTopNumberColor = TOP_NUMBER_SUCCESS_COLOR;
    currentBottomCodeColor = BOTTOM_CODE_SUCCESS_COLOR;
    showWiFiMessage("✓ Успешно отворено", 3000);
  } else if (message == "access denied") {
    currentTopNumberColor = TOP_NUMBER_ERROR_COLOR;
    currentBottomCodeColor = BOTTOM_CODE_ERROR_COLOR;
    showWiFiMessage("✗ Грешен код", 3000);
  } else if (message == "pin changed") {
    currentTopNumberColor = TOP_NUMBER_SUCCESS_COLOR;
    currentBottomCodeColor = BOTTOM_CODE_SUCCESS_COLOR;
    showWiFiMessage("✓ PIN променет", 3000);
  }
  
  clearCirclesArea();
  drawOuterCircle(circleColor);
  drawInnerCircle(circleColor);
  drawWhiteInnerCircle();
  drawHorizontalDividerLine();
  
  clearTextAreaAboveLine();
  drawStatusText();
  
  drawTopNumber();
  drawBottomPart();
  
  if (message == "door open") {
    startLEDEffect(strip.Color(0, 255, 0), durationMs);
  } else if (message == "access denied") {
    startLEDEffect(strip.Color(255, 0, 0), durationMs);
  } else if (message == "pin changed") {
    startLEDEffect(strip.Color(0, 255, 255), durationMs);
  }
  
  circleColorEndTime = millis() + durationMs;
}

void returnToDefaultCircles() {
  stopLEDEffect();
  
  currentTopNumberColor = TOP_NUMBER_COLOR;
  currentBottomCodeColor = BOTTOM_CODE_COLOR;
  
  clearCirclesArea();
  drawCircles(OUTER_CIRCLE_COLOR, INNER_CIRCLE_COLOR);
  drawWhiteInnerCircle();
  drawHorizontalDividerLine();
  
  showStatusMessage = false;
  
  clearTextAreaAboveLine();
  drawEnterPinText();
  
  drawTopNumber();
  drawBottomPart();
}

void drawInitialScreen() {
  tft.fillScreen(TFT_BLACK);
  
  currentTopNumberColor = TOP_NUMBER_COLOR;
  currentBottomCodeColor = BOTTOM_CODE_COLOR;
  
  drawCircles(OUTER_CIRCLE_COLOR, INNER_CIRCLE_COLOR);
...

This file has been truncated, please download it to see its full contents.

Credits

Mirko Pavleski
217 projects • 1575 followers
Thanks to Jana Kuzmanoska.

Comments