DIY Smart Code Lock with CrowPanel 1.28 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 Rotary Display. This module is a game-changer for DIY electronics because it integrates a touch screen, a physical button, and a rotary encoder into one sleek unit.
Devices & Components
1
Elecrow CrowPanel 1.28inch-HMI ESP32 Rotary Display 240*240
Hardware & Tools
1
Soldering kit
Software & Tools
Arduino IDE
Project description
Code
SmartLock Code
cpp
...
1//KRAEN KOD SE ZAEDNO I EKRAN I TELEFON 2/* 3 CrowPanel 1.28" (ESP32-S3 + GC9A01, TFT_eSPI) 4 Електронска брава со ротационен енкодер и WEB интерфејс 5 - Промена на лозинка преку веб 6 - Зачувување во EEPROM 7*/ 8 9#include <Arduino.h> 10#include <TFT_eSPI.h> 11#include <SPI.h> 12#include <Wire.h> 13#include <math.h> 14#include <Adafruit_NeoPixel.h> 15#include <WiFi.h> 16#include <WebServer.h> 17#include <EEPROM.h> 18 19// === КОНФИГУРАЦИЈА НА EEPROM === 20#define EEPROM_SIZE 64 21#define CODE_SAVE_ADDRESS 0 // Адреса каде ќе се чува кодот (5 бајти) 22 23// === КОНФИГУРАЦИЈА НА ДИСПЛЕЈ === 24#define USE_PANEL_ENABLE_PINS 1 25#define PIN_LCD_PWR_EN1 1 26#define PIN_LCD_PWR_EN2 2 27#define PIN_TFT_BL 46 28#define PIN_TFT_RST 14 29 30// === КОНФИГУРАЦИЈА НА WI-FI ACCESS POINT === 31const char* ap_ssid = "SmartLock_ESP32"; 32const char* ap_password = "12345678"; // Минимум 8 карактери 33IPAddress local_IP(192, 168, 4, 1); 34IPAddress gateway(192, 168, 4, 1); 35IPAddress subnet(255, 255, 255, 0); 36 37WebServer server(80); 38 39// === NEO PIXEL LED === 40#define LED_PIN 48 // Пин за LED лента 41#define LED_COUNT 5 // 5 LED диоди 42#define LED_BRIGHTNESS 50 // Јачина (0-255) 43#define LED_ROTATION_TIME 100 // Време на ротација (ms) - секои 100ms се менува LED 44 45Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); 46int ledPosition = 0; // Тековна LED позиција 47bool ledEffectActive = false; // Дали LED ефектот е активен 48unsigned long ledEffectStartTime = 0; // Време на почеток на ефект 49unsigned long lastLEDUpdateTime = 0; // Време на последно ажурирање на LED 50int ledEffectDuration = 5000; // Времетраење на ефект (5 секунди) 51uint32_t ledEffectColor = 0; // Боја на ефектот 52 53// === ПАРАМЕТРИ НА ПРСТЕНИ === 54#define OUTER_CIRCLE_THICKNESS 8 // Дебелина на надворешниот прстен (пиксели) 55#define INNER_CIRCLE_THICKNESS 5 // Дебелина на внатрешниот прстен (пиксели) 56#define CIRCLE_GAP 5 // Празно место меѓу прстените (пиксели) 57#define OUTER_CIRCLE_COLOR TFT_BLUE // Светло сина боја за надворешниот круг 58#define INNER_CIRCLE_COLOR TFT_PURPLE // Лилава боја за внатрешниот круг 59#define SUCCESS_CIRCLE_COLOR TFT_GREEN // Боја за успешна лозинка 60#define ERROR_CIRCLE_COLOR TFT_RED // Боја за грешна лозинка 61 62// === БОЈИ НА ТЕКСТ === 63#define TOP_NUMBER_COLOR 0x3D7D // Светло сина боја на горната цифра 64#define TOP_NUMBER_SUCCESS_COLOR TFT_GREEN // Зелена боја на горната цифра за успех 65#define TOP_NUMBER_ERROR_COLOR TFT_RED // Црвена боја на горната цифра за грешка 66#define BOTTOM_CODE_COLOR TFT_GREEN // Зелена боја на долните цифри од кодот 67#define BOTTOM_CODE_SUCCESS_COLOR TFT_GREEN // Зелена боја за успешен код 68#define BOTTOM_CODE_ERROR_COLOR TFT_RED // Црвена боја за грешен код 69#define RECTANGLE_COLOR TFT_WHITE // Боја на правоаголникот 70#define ENTER_PIN_TEXT_COLOR TFT_WHITE // Боја на текстот "enter your pin:" 71#define STATUS_TEXT_COLOR TFT_WHITE // Боја на текстовите "door open" и "access denied" 72#define WIFI_TEXT_COLOR TFT_CYAN // Боја за Wi-Fi статус текстот 73 74// === ПОДЕЛБА НА ЕКРАНОТ === 75#define SCREEN_WIDTH 240 76#define SCREEN_HEIGHT 240 77#define SCREEN_CENTER_X (SCREEN_WIDTH / 2) 78#define SCREEN_CENTER_Y (SCREEN_HEIGHT / 2) 79#define TOP_PART_HEIGHT 160 // Горниот дел (2/3 од екранот) 80#define BOTTOM_PART_HEIGHT 80 // Долниот дел (1/3 од екранот) 81#define DIVIDER_LINE_COLOR TFT_WHITE // Боја на разделната линија 82#define HORIZONTAL_LINE_THICKNESS 2 // Дебелина на хоризонталната линија 83#define INNER_WHITE_CIRCLE_THICKNESS 4 // Дебелина на внатрешниот бел круг 84#define NUMBER_Y_OFFSET 15 // Поместување на бројката надолу (пиксели) 85#define ENTER_PIN_TEXT_Y_OFFSET -10 // Поместување на текстот "enter your pin:" во однос на линијата 86#define STATUS_TEXT_Y_OFFSET -10 // Поместување на статус текстовите 87#define WIFI_STATUS_Y_OFFSET 20 // Поместување на Wi-Fi статус текстот 88 89// === ПРАВОАГОЛНИК ЗА КОД === 90#define RECTANGLE_THICKNESS 3 // Дебелина на линиите на правоаголникот 91#define RECTANGLE_PADDING 7 // Простор помеѓу цифрите и правоаголникот 92#define RECTANGLE_CORNER_RADIUS 8 // Заоблени агли на правоаголникот 93#define CODE_VERTICAL_OFFSET -10 // Поместување на кодот нагоре (негативно = нагоре) 94 95// === ПИНОВИ ЗА ЕНКОДЕР === 96#define ENC_A 45 97#define ENC_B 42 98#define ENC_BTN 41 99 100TFT_eSPI tft; 101 102// === ПРОМЕНЛИВИ ЗА СИСТЕМОТ === 103volatile int8_t encQuart = 0; 104int currentNumber = 0; // Тековно избрана цифра (0-9) 105int enteredCode[5] = { -1, -1, -1, -1, -1 }; // Внесен код 106int codePosition = 0; // Позиција во внесувањето 107int correctCode[5] = {1, 2, 3, 4, 5}; // Точен код (сега променлив) 108bool isChecking = false; // Дали се проверува лозинка 109unsigned long circleColorEndTime = 0; // Време кога бојата на прстените треба да се врати 110bool showStatusMessage = false; // Дали да се прикаже статус порака 111String statusMessage = ""; // Текст на статус пораката 112uint32_t statusCircleColor = 0; // Боја на круговите за статус 113uint32_t currentTopNumberColor = TOP_NUMBER_COLOR; // Тековна боја на горната цифра 114uint32_t currentBottomCodeColor = BOTTOM_CODE_COLOR; // Тековна боја на долните цифри 115bool showWiFiStatus = false; // Дали да се прикаже Wi-Fi статус 116String wifiStatusMessage = ""; // Wi-Fi статус порака 117unsigned long wifiStatusEndTime = 0; // Време до кога да се прикажува Wi-Fi статус 118 119// === HTML СТРАНА ЗА ВЕБ ИНТЕРФЕЈС === 120const char index_html[] PROGMEM = R"rawliteral( 121<!DOCTYPE HTML> 122<html> 123<head> 124 <meta name="viewport" content="width=device-width, initial-scale=1"> 125 <meta charset="UTF-8"> 126 <style> 127 body { 128 font-family: Arial, sans-serif; 129 text-align: center; 130 background-color: #1a1a2e; 131 color: #ffffff; 132 margin: 0; 133 padding: 20px; 134 } 135 .container { 136 max-width: 400px; 137 margin: 0 auto; 138 background-color: #16213e; 139 padding: 30px; 140 border-radius: 20px; 141 box-shadow: 0 0 20px rgba(0,0,0,0.5); 142 } 143 h1 { 144 color: #4ecca3; 145 margin-bottom: 30px; 146 } 147 h2 { 148 color: #4ecca3; 149 font-size: 20px; 150 margin-top: 30px; 151 border-top: 2px solid #4ecca3; 152 padding-top: 20px; 153 } 154 .code-display { 155 background-color: #0f3460; 156 padding: 15px; 157 border-radius: 15px; 158 margin-bottom: 25px; 159 font-size: 32px; 160 letter-spacing: 8px; 161 font-family: monospace; 162 color: #4ecca3; 163 border: 3px solid #4ecca3; 164 } 165 .keypad { 166 display: grid; 167 grid-template-columns: repeat(3, 1fr); 168 gap: 8px; 169 margin-bottom: 25px; 170 } 171 .key { 172 background-color: #0f3460; 173 border: none; 174 color: white; 175 padding: 12px; 176 font-size: 22px; 177 border-radius: 12px; 178 cursor: pointer; 179 transition: all 0.3s; 180 box-shadow: 0 4px 0 #070d1f; 181 } 182 .key:hover { 183 background-color: #1a4d8c; 184 transform: translateY(-2px); 185 box-shadow: 0 6px 0 #070d1f; 186 } 187 .key:active { 188 transform: translateY(4px); 189 box-shadow: 0 2px 0 #070d1f; 190 } 191 .menu-buttons { 192 display: grid; 193 grid-template-columns: 1fr 1fr; 194 gap: 20px; 195 margin: 40px 0; 196 } 197 .menu-btn { 198 padding: 25px 15px; 199 font-size: 24px; 200 border: none; 201 border-radius: 15px; 202 cursor: pointer; 203 font-weight: bold; 204 transition: all 0.3s; 205 box-shadow: 0 8px 0 #070d1f; 206 width: 100%; 207 min-height: 120px; 208 display: flex; 209 align-items: center; 210 justify-content: center; 211 } 212 .btn-unlock { 213 background-color: #4ecca3; 214 color: #16213e; 215 } 216 .btn-change { 217 background-color: #ffa500; 218 color: #16213e; 219 } 220 .menu-btn:hover { 221 opacity: 0.9; 222 transform: translateY(-2px); 223 box-shadow: 0 10px 0 #070d1f; 224 } 225 .menu-btn:active { 226 transform: translateY(8px); 227 box-shadow: 0 2px 0 #070d1f; 228 } 229 .action-buttons { 230 display: grid; 231 grid-template-columns: 1fr 1fr; 232 gap: 12px; 233 margin-bottom: 15px; 234 } 235 .btn { 236 padding: 12px; 237 font-size: 16px; 238 border: none; 239 border-radius: 10px; 240 cursor: pointer; 241 font-weight: bold; 242 transition: all 0.3s; 243 } 244 .btn-clear { 245 background-color: #e94560; 246 color: white; 247 } 248 .btn-submit { 249 background-color: #4ecca3; 250 color: #16213e; 251 } 252 .btn:hover { 253 opacity: 0.9; 254 transform: scale(1.02); 255 } 256 .status { 257 margin-top: 15px; 258 padding: 10px; 259 border-radius: 10px; 260 font-weight: bold; 261 font-size: 14px; 262 } 263 .success { 264 background-color: #4ecca3; 265 color: #16213e; 266 } 267 .error { 268 background-color: #e94560; 269 color: white; 270 } 271 .info { 272 margin-top: 15px; 273 font-size: 12px; 274 color: #888; 275 } 276 .back-btn { 277 background-color: #e94560; 278 color: white; 279 width: 100%; 280 margin-top: 15px; 281 padding: 12px; 282 font-size: 16px; 283 border: none; 284 border-radius: 10px; 285 cursor: pointer; 286 font-weight: bold; 287 transition: all 0.3s; 288 } 289 .back-btn:hover { 290 opacity: 0.9; 291 } 292 </style> 293</head> 294<body> 295 <div class="container"> 296 <h1>🔐 SMART LOCK</h1> 297 298 <div id="mainMenu"> 299 <div class="menu-buttons"> 300 <button class="menu-btn btn-unlock" onclick="showUnlockMenu()">🔓 ОТКЛУЧИ</button> 301 <button class="menu-btn btn-change" onclick="showChangePinMenu()">⚙ ПРОМЕНИ PIN</button> 302 </div> 303 </div> 304 305 <div id="unlockMenu" style="display:none;"> 306 <h2>Внеси код за отклучување</h2> 307 <div class="code-display" id="codeDisplay">_ _ _ _ _</div> 308 309 <div class="keypad"> 310 <button class="key" onclick="addDigit(1)">1</button> 311 <button class="key" onclick="addDigit(2)">2</button> 312 <button class="key" onclick="addDigit(3)">3</button> 313 <button class="key" onclick="addDigit(4)">4</button> 314 <button class="key" onclick="addDigit(5)">5</button> 315 <button class="key" onclick="addDigit(6)">6</button> 316 <button class="key" onclick="addDigit(7)">7</button> 317 <button class="key" onclick="addDigit(8)">8</button> 318 <button class="key" onclick="addDigit(9)">9</button> 319 <button class="key" onclick="addDigit(0)">0</button> 320 <button class="key" onclick="addDigit(0)" style="opacity:0; cursor:default;"></button> 321 <button class="key" onclick="addDigit(0)" style="opacity:0; cursor:default;"></button> 322 </div> 323 324 <div class="action-buttons"> 325 <button class="btn btn-clear" onclick="clearCode()">ИЗБРИШИ</button> 326 <button class="btn btn-submit" onclick="submitCode()">ВНЕСИ</button> 327 </div> 328 <button class="back-btn" onclick="backToMain()">◄ НАЗАД</button> 329 </div> 330 331 <div id="changePinMenu" style="display:none;"> 332 <h2>Промени PIN код</h2> 333 <p style="color:#888; font-size:14px; margin:5px;">Внеси стар PIN</p> 334 <div class="code-display" id="oldCodeDisplay">_ _ _ _ _</div> 335 <p style="color:#888; font-size:14px; margin:5px;">Внеси нов PIN (5 цифри)</p> 336 <div class="code-display" id="newCodeDisplay">_ _ _ _ _</div> 337 <p style="color:#888; font-size:14px; margin:5px;">Потврди нов PIN</p> 338 <div class="code-display" id="confirmCodeDisplay">_ _ _ _ _</div> 339 340 <div class="keypad"> 341 <button class="key" onclick="addDigitChange(1)">1</button> 342 <button class="key" onclick="addDigitChange(2)">2</button> 343 <button class="key" onclick="addDigitChange(3)">3</button> 344 <button class="key" onclick="addDigitChange(4)">4</button> 345 <button class="key" onclick="addDigitChange(5)">5</button> 346 <button class="key" onclick="addDigitChange(6)">6</button> 347 <button class="key" onclick="addDigitChange(7)">7</button> 348 <button class="key" onclick="addDigitChange(8)">8</button> 349 <button class="key" onclick="addDigitChange(9)">9</button> 350 <button class="key" onclick="addDigitChange(0)">0</button> 351 <button class="key" onclick="addDigitChange(0)" style="opacity:0; cursor:default;"></button> 352 <button class="key" onclick="addDigitChange(0)" style="opacity:0; cursor:default;"></button> 353 </div> 354 355 <div class="action-buttons"> 356 <button class="btn btn-clear" onclick="clearChangeCode()">ИЗБРИШИ</button> 357 <button class="btn btn-submit" onclick="submitChangePin()">ПРОМЕНИ</button> 358 </div> 359 <button class="back-btn" onclick="backToMain()">◄ НАЗАД</button> 360 </div> 361 362 <div class="status" id="status"></div> 363 <div class="info">Поврзани сте на: SmartLock_ESP32</div> 364 </div> 365 366 <script> 367 let code = []; 368 let oldCode = []; 369 let newCode = []; 370 let confirmCode = []; 371 let changePinStep = 1; // 1=old, 2=new, 3=confirm 372 373 function showUnlockMenu() { 374 document.getElementById('mainMenu').style.display = 'none'; 375 document.getElementById('unlockMenu').style.display = 'block'; 376 document.getElementById('changePinMenu').style.display = 'none'; 377 clearCode(); 378 document.getElementById('status').innerHTML = ''; 379 } 380 381 function showChangePinMenu() { 382 document.getElementById('mainMenu').style.display = 'none'; 383 document.getElementById('unlockMenu').style.display = 'none'; 384 document.getElementById('changePinMenu').style.display = 'block'; 385 clearChangeCode(); 386 document.getElementById('status').innerHTML = ''; 387 changePinStep = 1; 388 } 389 390 function backToMain() { 391 document.getElementById('mainMenu').style.display = 'block'; 392 document.getElementById('unlockMenu').style.display = 'none'; 393 document.getElementById('changePinMenu').style.display = 'none'; 394 } 395 396 function updateDisplay() { 397 let display = ''; 398 for(let i = 0; i < 5; i++) { 399 if(i < code.length) { 400 display += code[i] + ' '; 401 } else { 402 display += '_ '; 403 } 404 } 405 document.getElementById('codeDisplay').innerText = display; 406 } 407 408 function updateChangeDisplay() { 409 // Прикажи го стариот PIN 410 let oldDisplay = ''; 411 for(let i = 0; i < 5; i++) { 412 if(i < oldCode.length) { 413 oldDisplay += oldCode[i] + ' '; 414 } else { 415 oldDisplay += '_ '; 416 } 417 } 418 document.getElementById('oldCodeDisplay').innerText = oldDisplay; 419 420 // Прикажи го новиот PIN 421 let newDisplay = ''; 422 for(let i = 0; i < 5; i++) { 423 if(i < newCode.length) { 424 newDisplay += newCode[i] + ' '; 425 } else { 426 newDisplay += '_ '; 427 } 428 } 429 document.getElementById('newCodeDisplay').innerText = newDisplay; 430 431 // Прикажи го потврдниот PIN 432 let confirmDisplay = ''; 433 for(let i = 0; i < 5; i++) { 434 if(i < confirmCode.length) { 435 confirmDisplay += confirmCode[i] + ' '; 436 } else { 437 confirmDisplay += '_ '; 438 } 439 } 440 document.getElementById('confirmCodeDisplay').innerText = confirmDisplay; 441 } 442 443 function addDigit(digit) { 444 if(code.length < 5) { 445 code.push(digit); 446 updateDisplay(); 447 } 448 } 449 450 function addDigitChange(digit) { 451 if(changePinStep == 1 && oldCode.length < 5) { 452 oldCode.push(digit); 453 } else if(changePinStep == 2 && newCode.length < 5) { 454 newCode.push(digit); 455 } else if(changePinStep == 3 && confirmCode.length < 5) { 456 confirmCode.push(digit); 457 } 458 updateChangeDisplay(); 459 460 // Автоматски премини на следниот чекор 461 if(changePinStep == 1 && oldCode.length == 5) { 462 changePinStep = 2; 463 } else if(changePinStep == 2 && newCode.length == 5) { 464 changePinStep = 3; 465 } 466 } 467 468 function clearCode() { 469 code = []; 470 updateDisplay(); 471 } 472 473 function clearChangeCode() { 474 oldCode = []; 475 newCode = []; 476 confirmCode = []; 477 changePinStep = 1; 478 updateChangeDisplay(); 479 } 480 481 function submitCode() { 482 if(code.length !== 5) { 483 document.getElementById('status').innerHTML = '<div style="color: #e94560;">Внесете 5 цифри</div>'; 484 return; 485 } 486 487 fetch('/submit', { 488 method: 'POST', 489 headers: { 490 'Content-Type': 'application/x-www-form-urlencoded', 491 }, 492 body: 'code=' + code.join('') 493 }) 494 .then(response => response.text()) 495 .then(data => { 496 if(data.includes('correct')) { 497 document.getElementById('status').innerHTML = '<div class="success">✓ УСПЕШНО ОТВОРЕНА ВРАТА</div>'; 498 } else { 499 document.getElementById('status').innerHTML = '<div class="error">✗ ГРЕШЕН КОД</div>'; 500 } 501 clearCode(); 502 }); 503 } 504 505 function submitChangePin() { 506 if(oldCode.length !== 5 || newCode.length !== 5 || confirmCode.length !== 5) { 507 document.getElementById('status').innerHTML = '<div class="error">✗ Внесете ги сите полиња</div>'; 508 return; 509 } 510 511 if(newCode.join('') !== confirmCode.join('')) { 512 document.getElementById('status').innerHTML = '<div class="error">✗ Новите кодови не се совпаѓаат</div>'; 513 return; 514 } 515 516 fetch('/changePin', { 517 method: 'POST', 518 headers: { 519 'Content-Type': 'application/x-www-form-urlencoded', 520 }, 521 body: 'old=' + oldCode.join('') + '&new=' + newCode.join('') 522 }) 523 .then(response => response.text()) 524 .then(data => { 525 if(data.includes('success')) { 526 document.getElementById('status').innerHTML = '<div class="success">✓ PIN КОДОТ Е УСПЕШНО ПРОМЕНЕТ</div>'; 527 clearChangeCode(); 528 setTimeout(() => backToMain(), 2000); 529 } else { 530 document.getElementById('status').innerHTML = '<div class="error">✗ ГРЕШКА: Погрешен стар PIN</div>'; 531 clearChangeCode(); 532 } 533 }); 534 } 535 </script> 536</body> 537</html> 538)rawliteral"; 539 540// === LED ФУНКЦИИ === 541void initLEDs() { 542 strip.begin(); 543 strip.setBrightness(LED_BRIGHTNESS); 544 strip.clear(); 545 strip.show(); 546} 547 548void clearLEDs() { 549 for (int i = 0; i < LED_COUNT; i++) { 550 strip.setPixelColor(i, 0, 0, 0); 551 } 552 strip.show(); 553} 554 555void startLEDEffect(uint32_t color, int durationMs) { 556 ledEffectActive = true; 557 ledEffectStartTime = millis(); 558 ledEffectDuration = durationMs; 559 ledEffectColor = color; 560 ledPosition = 0; 561 lastLEDUpdateTime = millis(); 562 563 strip.clear(); 564 strip.setPixelColor(ledPosition, ledEffectColor); 565 strip.show(); 566} 567 568void stopLEDEffect() { 569 ledEffectActive = false; 570 clearLEDs(); 571} 572 573void updateLEDEffect() { 574 if (!ledEffectActive) return; 575 576 if (millis() - ledEffectStartTime > ledEffectDuration) { 577 stopLEDEffect(); 578 return; 579 } 580 581 if (millis() - lastLEDUpdateTime >= LED_ROTATION_TIME) { 582 lastLEDUpdateTime = millis(); 583 584 strip.setPixelColor(ledPosition, 0, 0, 0); 585 586 ledPosition++; 587 if (ledPosition >= LED_COUNT) { 588 ledPosition = 0; 589 } 590 591 strip.setPixelColor(ledPosition, ledEffectColor); 592 strip.show(); 593 } 594} 595 596// === ФУНКЦИИ ЗА МОЌНА ДИСПЛЕЈ === 597static void panelPowerOn() { 598 if(USE_PANEL_ENABLE_PINS) { 599 pinMode(PIN_LCD_PWR_EN1, OUTPUT); 600 pinMode(PIN_LCD_PWR_EN2, OUTPUT); 601 digitalWrite(PIN_LCD_PWR_EN1, HIGH); 602 digitalWrite(PIN_LCD_PWR_EN2, HIGH); 603 delay(5); 604 } 605} 606 607static void pulseResetPin() { 608 pinMode(PIN_TFT_RST, OUTPUT); 609 digitalWrite(PIN_TFT_RST, HIGH); 610 delay(5); 611 digitalWrite(PIN_TFT_RST, LOW); 612 delay(10); 613 digitalWrite(PIN_TFT_RST, HIGH); 614 delay(20); 615} 616 617static void backlightInit(uint8_t duty) { 618 pinMode(PIN_TFT_BL, OUTPUT); 619 digitalWrite(PIN_TFT_BL, duty > 0 ? HIGH : LOW); 620} 621 622// === ФУНКЦИИ ЗА ЕНКОДЕР === 623int8_t readEncoderTransition() { 624 static int last = 0; 625 int a = digitalRead(ENC_A); 626 int b = digitalRead(ENC_B); 627 int val = (a << 1) | b; 628 629 static const int8_t trans[16] = { 630 0, -1, +1, 0, 631 +1, 0, 0, -1, 632 -1, 0, 0, +1, 633 0, +1, -1, 0 634 }; 635 636 int8_t d = trans[(last << 2) | val]; 637 last = val; 638 return d; 639} 640 641int8_t readEncoderDetent() { 642 int8_t t = readEncoderTransition(); 643 if(t) { 644 encQuart += t; 645 if(encQuart >= 4) { 646 encQuart = 0; 647 return +1; 648 } 649 if(encQuart <= -4) { 650 encQuart = 0; 651 return -1; 652 } 653 } 654 return 0; 655} 656 657// === ФУНКЦИИ ЗА EEPROM === 658void loadCodeFromEEPROM() { 659 EEPROM.begin(EEPROM_SIZE); 660 661 // Провери дали има зачуван код 662 bool hasValidCode = true; 663 for (int i = 0; i < 5; i++) { 664 int val = EEPROM.read(CODE_SAVE_ADDRESS + i); 665 if (val < 0 || val > 9) { 666 hasValidCode = false; 667 break; 668 } 669 correctCode[i] = val; 670 } 671 672 // Ако нема валиден код, користи го дефаултниот 673 if (!hasValidCode) { 674 int defaultCode[5] = {1, 2, 3, 4, 5}; 675 for (int i = 0; i < 5; i++) { 676 correctCode[i] = defaultCode[i]; 677 } 678 saveCodeToEEPROM(); 679 } 680 681 EEPROM.end(); 682 683 Serial.print("Вчитан код од EEPROM: "); 684 for (int i = 0; i < 5; i++) { 685 Serial.print(correctCode[i]); 686 } 687 Serial.println(); 688} 689 690void saveCodeToEEPROM() { 691 EEPROM.begin(EEPROM_SIZE); 692 693 for (int i = 0; i < 5; i++) { 694 EEPROM.write(CODE_SAVE_ADDRESS + i, correctCode[i]); 695 } 696 697 EEPROM.commit(); 698 EEPROM.end(); 699 700 Serial.print("Зачуван код во EEPROM: "); 701 for (int i = 0; i < 5; i++) { 702 Serial.print(correctCode[i]); 703 } 704 Serial.println(); 705} 706 707// === ФУНКЦИИ ЗА ЦРТАЊЕ === 708void drawOuterCircle(uint32_t color) { 709 int maxRadius = min(SCREEN_WIDTH, SCREEN_HEIGHT) / 2 - 1; 710 int outerRadius = maxRadius; 711 int innerOuterRadius = outerRadius - OUTER_CIRCLE_THICKNESS; 712 713 tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, outerRadius, color); 714 tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, innerOuterRadius, color); 715 716 for (int r = innerOuterRadius + 1; r < outerRadius; r++) { 717 tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, r, color); 718 } 719} 720 721void drawInnerCircle(uint32_t color) { 722 int maxRadius = min(SCREEN_WIDTH, SCREEN_HEIGHT) / 2 - 1; 723 int innerRadius = maxRadius - (OUTER_CIRCLE_THICKNESS + CIRCLE_GAP); 724 int innerInnerRadius = innerRadius - INNER_CIRCLE_THICKNESS; 725 726 tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, innerRadius, color); 727 tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, innerInnerRadius, color); 728 729 for (int r = innerInnerRadius + 1; r < innerRadius; r++) { 730 tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, r, color); 731 } 732} 733 734void drawCircles(uint32_t outerColor, uint32_t innerColor) { 735 drawOuterCircle(outerColor); 736 drawInnerCircle(innerColor); 737} 738 739void drawWhiteInnerCircle() { 740 int maxRadius = min(SCREEN_WIDTH, SCREEN_HEIGHT) / 2 - 1; 741 int whiteCircleRadius = maxRadius - (OUTER_CIRCLE_THICKNESS + CIRCLE_GAP + INNER_CIRCLE_THICKNESS + 10); 742 int innerWhiteRadius = whiteCircleRadius - INNER_WHITE_CIRCLE_THICKNESS; 743 744 tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, whiteCircleRadius, TFT_WHITE); 745 tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, innerWhiteRadius, TFT_WHITE); 746 747 for (int r = innerWhiteRadius + 1; r < whiteCircleRadius; r++) { 748 tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, r, TFT_WHITE); 749 } 750} 751 752void clearCirclesArea() { 753 int maxRadius = min(SCREEN_WIDTH, SCREEN_HEIGHT) / 2 - 1; 754 for (int r = maxRadius - (OUTER_CIRCLE_THICKNESS + CIRCLE_GAP + INNER_CIRCLE_THICKNESS + 15); r <= maxRadius; r++) { 755 tft.drawCircle(SCREEN_CENTER_X, SCREEN_CENTER_Y, r, TFT_BLACK); 756 } 757} 758 759void drawHorizontalDividerLine() { 760 int lineY = TOP_PART_HEIGHT; 761 for (int i = 0; i < HORIZONTAL_LINE_THICKNESS; i++) { 762 tft.drawFastHLine(0, lineY + i, SCREEN_WIDTH, DIVIDER_LINE_COLOR); 763 } 764} 765 766void drawEnterPinText() { 767 tft.setTextDatum(MC_DATUM); 768 tft.setTextFont(2); 769 tft.setTextColor(ENTER_PIN_TEXT_COLOR, TFT_BLACK); 770 int textY = TOP_PART_HEIGHT + ENTER_PIN_TEXT_Y_OFFSET; 771 tft.drawString("enter your pin:", SCREEN_CENTER_X, textY); 772} 773 774void drawWiFiStatusText() { 775 tft.setTextDatum(MC_DATUM); 776 tft.setTextFont(1); 777 tft.setTextColor(WIFI_TEXT_COLOR, TFT_BLACK); 778 int textY = TOP_PART_HEIGHT + WIFI_STATUS_Y_OFFSET; 779 tft.drawString(wifiStatusMessage, SCREEN_CENTER_X, textY); 780} 781 782void drawStatusText() { 783 tft.setTextDatum(MC_DATUM); 784 tft.setTextFont(2); 785 tft.setTextColor(STATUS_TEXT_COLOR, TFT_BLACK); 786 int textY = TOP_PART_HEIGHT + STATUS_TEXT_Y_OFFSET; 787 tft.drawString(statusMessage, SCREEN_CENTER_X, textY); 788} 789 790void clearTextAreaAboveLine() { 791 int textY = TOP_PART_HEIGHT + ENTER_PIN_TEXT_Y_OFFSET; 792 int textHeight = 20; 793 tft.fillRect(0, textY - textHeight/2, SCREEN_WIDTH, textHeight, TFT_BLACK); 794} 795 796void drawRoundedRectangle(int x, int y, int width, int height, int radius, uint32_t color, int thickness) { 797 if (thickness == 1) { 798 tft.drawRoundRect(x, y, width, height, radius, color); 799 } else { 800 for (int i = 0; i < thickness; i++) { 801 tft.drawRoundRect(x - i, y - i, width + 2*i, height + 2*i, radius + i, color); 802 } 803 } 804} 805 806void drawCodeRectangle() { 807 String codeStr = ""; 808 for (int i = 0; i < 5; i++) { 809 if (enteredCode[i] >= 0) { 810 codeStr += String(enteredCode[i]); 811 } else { 812 codeStr += "_"; 813 } 814 if (i < 4) codeStr += " "; 815 } 816 817 tft.setTextFont(4); 818 int textWidth = tft.textWidth(codeStr); 819 int textHeight = 40; 820 821 int rectX = SCREEN_CENTER_X - textWidth/2 - RECTANGLE_PADDING; 822 int rectY = TOP_PART_HEIGHT + (BOTTOM_PART_HEIGHT / 2) - textHeight/2 - RECTANGLE_PADDING + CODE_VERTICAL_OFFSET+7; 823 int rectWidth = textWidth + 2 * RECTANGLE_PADDING; 824 int rectHeight = textHeight + 1 * RECTANGLE_PADDING; 825 826 if (rectY < TOP_PART_HEIGHT) { 827 rectY = TOP_PART_HEIGHT + 10; 828 } 829 830 if (rectY + rectHeight > TOP_PART_HEIGHT + BOTTOM_PART_HEIGHT) { 831 rectHeight = (TOP_PART_HEIGHT + BOTTOM_PART_HEIGHT) - rectY - 5; 832 } 833 834 drawRoundedRectangle(rectX, rectY, rectWidth, rectHeight, RECTANGLE_CORNER_RADIUS, RECTANGLE_COLOR, RECTANGLE_THICKNESS); 835} 836 837void drawTopNumber() { 838 tft.fillRect(SCREEN_CENTER_X - 50, TOP_PART_HEIGHT/2 - 40 + NUMBER_Y_OFFSET, 100, 80, TFT_BLACK); 839 840 tft.setTextDatum(MC_DATUM); 841 tft.setTextFont(7); 842 tft.setTextColor(currentTopNumberColor, TFT_BLACK); 843 844 char numStr[2]; 845 sprintf(numStr, "%d", currentNumber); 846 847 int numberY = (TOP_PART_HEIGHT / 2) + NUMBER_Y_OFFSET; 848 tft.drawString(numStr, SCREEN_CENTER_X, numberY); 849} 850 851void drawTopPart() { 852 tft.fillRect(0, 0, SCREEN_WIDTH, TOP_PART_HEIGHT, TFT_BLACK); 853 854 drawCircles(OUTER_CIRCLE_COLOR, INNER_CIRCLE_COLOR); 855 drawWhiteInnerCircle(); 856 drawHorizontalDividerLine(); 857 858 if (!showStatusMessage) { 859 drawEnterPinText(); 860 } else { 861 drawStatusText(); 862 } 863 864 if (showWiFiStatus && millis() < wifiStatusEndTime) { 865 drawWiFiStatusText(); 866 } 867 868 drawTopNumber(); 869} 870 871void drawBottomPart() { 872 tft.fillRect(0, TOP_PART_HEIGHT, SCREEN_WIDTH, BOTTOM_PART_HEIGHT, TFT_BLACK); 873 874 String codeStr = ""; 875 for (int i = 0; i < 5; i++) { 876 if (enteredCode[i] >= 0) { 877 codeStr += String(enteredCode[i]); 878 } else { 879 codeStr += "_"; 880 } 881 if (i < 4) codeStr += " "; 882 } 883 884 drawCodeRectangle(); 885 886 tft.setTextDatum(MC_DATUM); 887 tft.setTextFont(4); 888 tft.setTextColor(currentBottomCodeColor, TFT_BLACK); 889 890 int bottomCenterY = TOP_PART_HEIGHT + (BOTTOM_PART_HEIGHT / 2) + CODE_VERTICAL_OFFSET; 891 tft.drawString(codeStr, SCREEN_CENTER_X, bottomCenterY); 892 893 drawHorizontalDividerLine(); 894 895 if (!showStatusMessage) { 896 drawEnterPinText(); 897 } else { 898 drawStatusText(); 899 } 900 901 if (showWiFiStatus && millis() < wifiStatusEndTime) { 902 drawWiFiStatusText(); 903 } 904} 905 906void clearEnteredCode() { 907 for (int i = 0; i < 5; i++) { 908 enteredCode[i] = -1; 909 } 910 codePosition = 0; 911 drawBottomPart(); 912} 913 914bool checkCode() { 915 for (int i = 0; i < 5; i++) { 916 if (enteredCode[i] != correctCode[i]) { 917 return false; 918 } 919 } 920 return true; 921} 922 923void showWiFiMessage(String message, int durationMs = 3000) { 924 showWiFiStatus = true; 925 wifiStatusMessage = message; 926 wifiStatusEndTime = millis() + durationMs; 927 928 drawTopPart(); 929 drawBottomPart(); 930} 931 932void updateCircleColor(uint32_t circleColor, int durationMs, String message) { 933 showStatusMessage = true; 934 statusMessage = message; 935 statusCircleColor = circleColor; 936 937 if (message == "door open") { 938 currentTopNumberColor = TOP_NUMBER_SUCCESS_COLOR; 939 currentBottomCodeColor = BOTTOM_CODE_SUCCESS_COLOR; 940 showWiFiMessage("✓ Успешно отворено", 3000); 941 } else if (message == "access denied") { 942 currentTopNumberColor = TOP_NUMBER_ERROR_COLOR; 943 currentBottomCodeColor = BOTTOM_CODE_ERROR_COLOR; 944 showWiFiMessage("✗ Грешен код", 3000); 945 } else if (message == "pin changed") { 946 currentTopNumberColor = TOP_NUMBER_SUCCESS_COLOR; 947 currentBottomCodeColor = BOTTOM_CODE_SUCCESS_COLOR; 948 showWiFiMessage("✓ PIN променет", 3000); 949 } 950 951 clearCirclesArea(); 952 drawOuterCircle(circleColor); 953 drawInnerCircle(circleColor); 954 drawWhiteInnerCircle(); 955 drawHorizontalDividerLine(); 956 957 clearTextAreaAboveLine(); 958 drawStatusText(); 959 960 drawTopNumber(); 961 drawBottomPart(); 962 963 if (message == "door open") { 964 startLEDEffect(strip.Color(0, 255, 0), durationMs); 965 } else if (message == "access denied") { 966 startLEDEffect(strip.Color(255, 0, 0), durationMs); 967 } else if (message == "pin changed") { 968 startLEDEffect(strip.Color(0, 255, 255), durationMs); 969 } 970 971 circleColorEndTime = millis() + durationMs; 972} 973 974void returnToDefaultCircles() { 975 stopLEDEffect(); 976 977 currentTopNumberColor = TOP_NUMBER_COLOR; 978 currentBottomCodeColor = BOTTOM_CODE_COLOR; 979 980 clearCirclesArea(); 981 drawCircles(OUTER_CIRCLE_COLOR, INNER_CIRCLE_COLOR); 982 drawWhiteInnerCircle(); 983 drawHorizontalDividerLine(); 984 985 showStatusMessage = false; 986 987 clearTextAreaAboveLine(); 988 drawEnterPinText(); 989 990 drawTopNumber(); 991 drawBottomPart(); 992} 993 994void drawInitialScreen() { 995 tft.fillScreen(TFT_BLACK); 996 997 currentTopNumberColor = TOP_NUMBER_COLOR; 998 currentBottomCodeColor = BOTTOM_CODE_COLOR; 999 1000 drawCircles(OUTER_CIRCLE_COLOR, INNER_CIRCLE_COLOR); 1001 drawWhiteInnerCircle(); 1002 drawHorizontalDividerLine(); 1003 drawEnterPinText(); 1004 drawTopNumber(); 1005 drawBottomPart(); 1006 1007 String wifiMsg = "WiFi: " + String(ap_ssid) + " " + WiFi.softAPIP().toString(); 1008 showWiFiMessage(wifiMsg, 5000); 1009} 1010 1011// === WEB СЕРВЕР ФУНКЦИИ === 1012void handleRoot() { 1013 server.send(200, "text/html", index_html); 1014} 1015 1016void handleSubmit() { 1017 if (server.hasArg("code")) { 1018 String codeStr = server.arg("code"); 1019 1020 if (codeStr.length() == 5) { 1021 for (int i = 0; i < 5; i++) { 1022 enteredCode[i] = codeStr.charAt(i) - '0'; 1023 } 1024 codePosition = 5; 1025 1026 drawBottomPart(); 1027 1028 if (checkCode()) { 1029 Serial.println("Кодот е ТОЧЕН (преку веб)!"); 1030 updateCircleColor(SUCCESS_CIRCLE_COLOR, 3000, "door open"); 1031 server.send(200, "text/plain", "correct"); 1032 } else { 1033 Serial.println("Кодот е ПОГРЕШЕН (преку веб)!"); 1034 updateCircleColor(ERROR_CIRCLE_COLOR, 3000, "access denied"); 1035 server.send(200, "text/plain", "incorrect"); 1036 } 1037 1038 clearEnteredCode(); 1039 } 1040 } 1041} 1042 1043void handleChangePin() { 1044 if (server.hasArg("old") && server.hasArg("new")) { 1045 String oldCodeStr = server.arg("old"); 1046 String newCodeStr = server.arg("new"); 1047 1048 if (oldCodeStr.length() != 5 || newCodeStr.length() != 5) { 1049 server.send(400, "text/plain", "error: invalid length"); 1050 return; 1051 } 1052 1053 bool oldCodeCorrect = true; 1054 for (int i = 0; i < 5; i++) { 1055 if ((oldCodeStr.charAt(i) - '0') != correctCode[i]) { 1056 oldCodeCorrect = false; 1057 break; 1058 } 1059 } 1060 1061 if (!oldCodeCorrect) { 1062 server.send(200, "text/plain", "error: wrong old code"); 1063 return; 1064 } 1065 1066 for (int i = 0; i < 5; i++) { 1067 correctCode[i] = newCodeStr.charAt(i) - '0'; 1068 } 1069 1070 saveCodeToEEPROM(); 1071 1072 updateCircleColor(SUCCESS_CIRCLE_COLOR, 3000, "pin changed"); 1073 1074 Serial.print("PIN кодот е променет: "); 1075 for (int i = 0; i < 5; i++) { 1076 Serial.print(correctCode[i]); 1077 } 1078 Serial.println(); 1079 1080 server.send(200, "text/plain", "success"); 1081 } 1082} 1083 1084void handleNotFound() { 1085 server.send(404, "text/plain", "404: Not Found"); 1086} 1087 1088void setupWiFiAP() { 1089 WiFi.mode(WIFI_AP); 1090 WiFi.softAPConfig(local_IP, gateway, subnet); 1091 WiFi.softAP(ap_ssid, ap_password); 1092 1093 Serial.println(""); 1094 Serial.println("WiFi Access Point стартуван"); 1095 Serial.print("SSID: "); 1096 Serial.println(ap_ssid); 1097 Serial.print("IP адреса: "); 1098 Serial.println(WiFi.softAPIP()); 1099 1100 server.on("/", handleRoot); 1101 server.on("/submit", HTTP_POST, handleSubmit); 1102 server.on("/changePin", HTTP_POST, handleChangePin); 1103 server.onNotFound(handleNotFound); 1104 1105 server.begin(); 1106 Serial.println("HTTP серверот стартуваше"); 1107} 1108 1109// === SETUP === 1110void setup() { 1111 Serial.begin(115200); 1112 delay(100); 1113 1114 loadCodeFromEEPROM(); 1115 1116 panelPowerOn(); 1117 backlightInit(255); 1118 pulseResetPin(); 1119 1120 tft.init(); 1121 tft.setRotation(0); 1122 tft.fillScreen(TFT_BLACK); 1123 1124 for (int d = 0; d <= 255; d += 5) { 1125 backlightInit(d); 1126 delay(10); 1127 } 1128 1129 initLEDs(); 1130 1131 pinMode(ENC_A, INPUT_PULLUP); 1132 pinMode(ENC_B, INPUT_PULLUP); 1133 pinMode(ENC_BTN, INPUT_PULLUP); 1134 1135 setupWiFiAP(); 1136 1137 drawInitialScreen(); 1138 1139 Serial.println("Електронска брава со WEB интерфејс - подготвена!"); 1140 Serial.println("Поврзете се на WiFi: SmartLock_ESP32, лозинка: 12345678"); 1141 Serial.println("Отворете browser и одете на 192.168.4.1"); 1142} 1143 1144// === LOOP === 1145void loop() { 1146 server.handleClient(); 1147 updateLEDEffect(); 1148 1149 if (circleColorEndTime > 0 && millis() > circleColorEndTime) { 1150 returnToDefaultCircles(); 1151 circleColorEndTime = 0; 1152 1153 if (isChecking) { 1154 clearEnteredCode(); 1155 isChecking = false; 1156 showStatusMessage = false; 1157 } 1158 } 1159 1160 if (showWiFiStatus && millis() > wifiStatusEndTime) { 1161 showWiFiStatus = false; 1162 drawTopPart(); 1163 drawBottomPart(); 1164 } 1165 1166 int8_t detent = readEncoderDetent(); 1167 1168 if (detent != 0 && circleColorEndTime == 0) { 1169 currentNumber += detent; 1170 1171 if (currentNumber > 9) currentNumber = 0; 1172 if (currentNumber < 0) currentNumber = 9; 1173 1174 drawTopNumber(); 1175 drawHorizontalDividerLine(); 1176 1177 if (!showStatusMessage) { 1178 drawEnterPinText(); 1179 } else { 1180 drawStatusText(); 1181 } 1182 1183 Serial.print("Тековна цифра: "); 1184 Serial.println(currentNumber); 1185 } 1186 1187 if (digitalRead(ENC_BTN) == LOW && circleColorEndTime == 0) { 1188 delay(50); 1189 1190 if (digitalRead(ENC_BTN) == LOW) { 1191 if (codePosition < 5) { 1192 enteredCode[codePosition] = currentNumber; 1193 codePosition++; 1194 1195 drawBottomPart(); 1196 1197 Serial.print("Внесена цифра: "); 1198 Serial.println(currentNumber); 1199 Serial.print("Позиција: "); 1200 Serial.println(codePosition); 1201 } 1202 1203 if (codePosition == 5) { 1204 isChecking = true; 1205 1206 if (checkCode()) { 1207 Serial.println("Кодот е ТОЧЕН!"); 1208 updateCircleColor(SUCCESS_CIRCLE_COLOR, 3000, "door open"); 1209 } else { 1210 Serial.println("Кодот е ПОГРЕШЕН!"); 1211 updateCircleColor(ERROR_CIRCLE_COLOR, 3000, "access denied"); 1212 } 1213 } 1214 1215 while (digitalRead(ENC_BTN) == LOW); 1216 } 1217 } 1218 1219 delay(5); 1220}
Comments
Only logged in users can leave comments