Курс Arduino с нуля · Большие проекты

Урок 29. Проект: сейф с цифровым кодом

Три кнопки вместо клавиатуры, звёздочки на экране и сервопривод-засов. Сейф сам сравнивает набранное с секретом, а после трёх ошибок подряд блокирует кнопки — как настоящий.

Урок 29 из 31⏱ примерно 55 минутОбновлено: 30 июля 2026

Что понадобится

  • Плата Uno из набора
  • Кабель USB
  • Макетная плата
  • Экран LCD1602
  • Потенциометр 10 кОм
  • 3 тактовые кнопки
  • Сервопривод SG90
  • Картон и скотч для засова
  • 22 провода «папа-папа»

Все детали есть в обучающем наборе, по которому идёт курс.

Начинается последний блок курса — большие проекты. Сегодня соберём сейф с цифровым кодом: три кнопки вместо клавиатуры, экран со звёздочками вместо окошка домофона и сервопривод вместо засова. Это первое наше устройство, которое решает само, пускать вас или нет.

Новых деталей снова не будет — всё из набора и всё уже знакомо: кнопки из урока 10, экран из урока 26, сервопривод из урока 22. Новой будет главная мысль: как программа хранит секрет и проверяет, что вы его знаете.

Что построим

OTKRYTO!Zakroyucherez4Vvedikod:**__STOP!3oshibkiZhdi15sкодверныйсамо, через5 секундтри ошибкиподрядсамо, через15 секундСейф открытзасов убран, идёт отсчётНабор кодавместо цифр — звёздочкиБлокировкакнопки не слушаются, идёт отсчёт
Три состояния сейфа. Обычно он ждёт код и показывает звёздочки. Верный код открывает засов на пять секунд, три ошибки подряд — блокировка на пятнадцать.

Как это работает в жизни:

  • нажимаете кнопки — на экране вместо цифр появляются звёздочки, как при вводе пароля, чтобы никто не подсмотрел;
  • после четырёх нажатий сейф сам сравнивает набранное с секретным кодом;
  • угадали — сервопривод убирает засов, идёт отсчёт, и сейф сам закрывается обратно;
  • ошиблись три раза подряд — сейф обижается и перестаёт слушать кнопки: у настоящих дверных замков и телефонов такая же защита от подбора.

Как сейф хранит код и проверяет его

Секретный код — это четыре номера кнопок подряд, например «1 3 2 2». Лучший способ хранить такой список мы знаем с урока 6 — массив. Массивов будет два: в одном секрет, записанный прямо в программе, в другой складываются нажатия. Остаётся сравнить их — и вот тут новичков ждёт сюрприз.

Все клетки совпали — открытьвы нажали1322====код в программе1322Третья не совпала — стопвы нажали1312==код в программе1322до четвёртой пары проверкауже не дошлаСпросить «равны ли два списка?» одним знаком нельзя — плата сравнивает по одной клетке за раз.
Сравнение идёт по парам клеток: первая с первой, вторая со второй. Одно несовпадение — и код неверный, дальше можно не смотреть.

Спросить у платы «равны ли эти два списка?» одним знаком равенства нельзя — так можно сравнивать только отдельные числа. Списки сравнивают циклом: берём пару клеток, сравниваем, переходим к следующей. Знакомый цикл for из урока 6 здесь займётся новой работой.

Кто на каком выводе

Деталей много, поэтому начинаем, как в уроке 28, — со списка мест. Экран забрал шесть выводов ещё в уроке 26, сервопривод сядет на вывод 9, как в шлагбауме из урока 24. Кнопкам достаются выводы 7, 8 и 10 — девятку между ними пропускаем, она занята.

Полный список: экран — 12, 11, 5, 4, 3, 2; сервопривод — 9; кнопки — 7, 8, 10. Свободными остались 6 и 13. Вывод 6 скоро пригодится — в задании со звёздочкой на него сядет зуммер.

Собираем схему

+abcdefghiкнопка 1кнопка 2кнопка 3Vvedikod:**__12345678910111213141516151520911242628301234561Экран, потенциометр контраста и все их провода — в точности как в уроке 26, тут ничего не менялось.2Три кнопки сидят верхом на канавке: кнопка 1 в столбиках 9–11, кнопка 2 в 24–26, кнопка 3 в 28–30.3Сигналы кнопок — с верхней стороны канавки: столбик 9 → вывод 7, столбик 24 → вывод 8, столбик 28 → вывод 10.4Земли кнопок — с нижней стороны, короткими проводами в столбики 7 и 18: там уже «минус» через контакты 5 и 16 экрана.5Сервопривод-засов: оранжевый провод на вывод 9, красный на шину «+», коричневый на шину «−».6К качалке сервопривода прикрепите картонный засов — он и будет держать дверцу.Питание макетки — с верхних шин: нижние закрыты экраном. Вывод 6 свободен — пригодится для зуммера в задании.Провод соединён только там, где на нём светлая шляпка; шины он пересекает между отверстиями.
Экран с потенциометром стоят как в уроке 26 и не трогаются. Добавились три кнопки на канавке и сервопривод у правого края.
  1. Проверьте узел из урока 26: экран в нижнем ряду j, потенциометр в столбиках 21–23, все шестнадцать проводов на местах. Если разбирали схему — соберите по схеме урока 26 заново.
  2. Посадите три кнопки верхом на канавку: первую в столбики 9–11, вторую в 24–26, третью в 28–30. Ножки должны выходить вверх и вниз, а не влево и вправо — повёрнутая кнопка будет «нажата» всегда, это ловушка из урока 10.
  3. Сигнальные провода кнопок — с верхней стороны канавки: из столбика 9 на вывод 7, из 24 — на вывод 8, из 28 — на вывод 10.
  4. Земли кнопок — с нижней стороны канавки, и тянуться к шинам не нужно: провод из столбика 9 в столбик 7 и провода из столбиков 24 и 28 в столбик 18. Столбики 7 и 18 уже соединены с «минусом» — там сидят контакты 5 и 16 экрана. Это приём «возьми питание у соседа» из урока 28.
  5. Сервопривод: оранжевый провод на вывод 9, красный на верхнюю шину «+», коричневый на верхнюю шину «−». Если провода коротки, вставьте в разъём сервопривода три провода «папа-папа».
  6. Вырежьте из картона полоску-засов и прикрутите к качалке сервопривода — годится скотч. Пока засов пусть смотрит вдоль корпуса.

Проверьте перед включением: кнопки сидят на канавке ножками вверх-вниз; сигналы уходят с верхней стороны канавки, земли — с нижней; на выводе 9 именно оранжевый провод сервопривода; и все провода экрана на месте — их удобно пересчитать по схеме урока 26.

Шаг 1. Проверяем кнопки

Как всегда в больших проектах — сначала убеждаемся, что железо живо. Экран пусть называет номер нажатой кнопки:

// Урок 29. Шаг 1: проверяем кнопки — экран называет нажатую.

#include <LiquidCrystal.h>

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

// Кнопки слева направо: 1, 2 и 3. Вывод 9 пропущен — он для сервопривода.
const int BUTTONS[3] = {7, 8, 10};

void setup() {
  lcd.begin(16, 2);
  for (int i = 0; i < 3; i++) {
    pinMode(BUTTONS[i], INPUT_PULLUP);
  }
  lcd.print("Proverka knopok");
}

void loop() {
  for (int i = 0; i < 3; i++) {
    if (digitalRead(BUTTONS[i]) == LOW) {
      lcd.setCursor(0, 1);
      lcd.print("Nazhata: ");
      lcd.print(i + 1);   // человеку привычно считать с единицы
    }
  }
}

Кнопки лежат в массиве, как в пульте из урока 13, и работают через INPUT_PULLUP — нажатая кнопка даёт LOW. Понажимайте все три: на второй строке должны появляться 1, 2 и 3. Если какая-то кнопка молчит или «нажата» сама по себе — загляните в раздел «Не работает?».

Шаг 2. Копим нажатия и рисуем звёздочки

Теперь нажатия нужно запоминать. Заводим массив на четыре клетки, счётчик набранного и — важная деталь из урока 11 — ловим момент нажатия, а не само нажатие, иначе одна удержанная кнопка мгновенно заполнит весь код:

// Урок 29. Шаг 2: копим четыре нажатия и показываем звёздочки.

#include <LiquidCrystal.h>

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

const int BUTTONS[3] = {7, 8, 10};
const int CODE_LEN = 4;   // длина кода

int attempt[CODE_LEN];    // сюда складываем нажатия
int entered = 0;          // сколько уже набрано
bool wasPressed[3] = {false, false, false};

void setup() {
  lcd.begin(16, 2);
  for (int i = 0; i < 3; i++) {
    pinMode(BUTTONS[i], INPUT_PULLUP);
  }
  showInput();
}

void loop() {
  for (int i = 0; i < 3; i++) {
    bool pressed = (digitalRead(BUTTONS[i]) == LOW);
    if (pressed && !wasPressed[i]) {   // ловим именно момент нажатия
      addPress(i + 1);
      delay(30);                       // защита от дребезга
    }
    wasPressed[i] = pressed;
  }
}

void addPress(int number) {
  attempt[entered] = number;   // записали номер кнопки в свободную клетку
  entered = entered + 1;
  showInput();
  if (entered == CODE_LEN) {   // набрали четыре — пока просто начинаем заново
    delay(800);
    entered = 0;
    showInput();
  }
}

void showInput() {
  lcd.clear();
  lcd.print("Vvedi kod:");
  lcd.setCursor(0, 1);
  for (int i = 0; i < CODE_LEN; i++) {
    if (i < entered) {
      lcd.print("* ");   // это место уже набрано
    } else {
      lcd.print("_ ");   // это место ещё пустое
    }
  }
}

Разберём главное:

  • attempt[entered] = number; — номер кнопки ложится в первую свободную клетку: счётчик набранного и есть её адрес;
  • звёздочки рисует отдельная функция: сколько клеток занято — столько звёзд, остальное — прочерки;
  • после четвёртого нажатия пока просто пауза и набор заново — проверку подключим следующим шагом.

Прошейте и понажимайте: прочерки должны по одному превращаться в звёздочки. Подглядеть, что именно набрано, невозможно — всё как у взрослых.

Шаг 3. Сравниваем с секретом

Добавляем массив с секретом и функцию-судью. Она отвечает одним словом — да или нет, — а внутри работает цикл со схемы выше:

// Урок 29. Шаг 3: сравниваем набранное с секретным кодом.

#include <LiquidCrystal.h>

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

const int BUTTONS[3] = {7, 8, 10};
const int CODE_LEN = 4;
const int SECRET[CODE_LEN] = {1, 3, 2, 2};   // секретный код сейфа

int attempt[CODE_LEN];
int entered = 0;
bool wasPressed[3] = {false, false, false};

void setup() {
  lcd.begin(16, 2);
  for (int i = 0; i < 3; i++) {
    pinMode(BUTTONS[i], INPUT_PULLUP);
  }
  showInput();
}

void loop() {
  for (int i = 0; i < 3; i++) {
    bool pressed = (digitalRead(BUTTONS[i]) == LOW);
    if (pressed && !wasPressed[i]) {
      addPress(i + 1);
      delay(30);
    }
    wasPressed[i] = pressed;
  }
}

void addPress(int number) {
  attempt[entered] = number;
  entered = entered + 1;
  showInput();
  if (entered == CODE_LEN) {   // код набран целиком — проверяем
    checkCode();
  }
}

void checkCode() {
  lcd.clear();
  if (codeCorrect()) {
    lcd.print("OTKRYTO!");
  } else {
    lcd.print("Nevernyy kod");
  }
  delay(2000);
  entered = 0;   // в любом случае начинаем набор заново
  showInput();
}

// Сравнить два списка можно только по одной клетке за раз.
bool codeCorrect() {
  for (int i = 0; i < CODE_LEN; i++) {
    if (attempt[i] != SECRET[i]) {
      return false;   // нашли расхождение — дальше смотреть незачем
    }
  }
  return true;        // все четыре клетки совпали
}

void showInput() {
  lcd.clear();
  lcd.print("Vvedi kod:");
  lcd.setCursor(0, 1);
  for (int i = 0; i < CODE_LEN; i++) {
    if (i < entered) {
      lcd.print("* ");
    } else {
      lcd.print("_ ");
    }
  }
}
  • if (attempt[i] != SECRET[i]) — сравнивается пара клеток с одинаковым номером;
  • return false; прекращает проверку на первом же несовпадении — ответ уже ясен, остальные клетки можно не смотреть;
  • до return true; дело доходит, только если цикл прошёл все четыре пары и ни разу не споткнулся.

Наберите 1-3-2-2 — экран скажет «OTKRYTO!». Наберите что-нибудь другое — «Nevernyy kod». Сейф уже умеет главное, осталось приделать ему мышцы.

Шаг 4. Подключаем засов

Сервопривод из урока 22 будет запирать дверцу: 90 градусов — засов задвинут, 0 — убран. Открывшись, сейф ведёт отсчёт прямо на экране и через пять секунд запирается сам:

// Урок 29. Шаг 4: верный код убирает засов, потом сейф закрывается сам.

#include <LiquidCrystal.h>
#include <Servo.h>

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
Servo zasov;   // сервопривод, который запирает дверцу

const int BUTTONS[3] = {7, 8, 10};
const int SERVO_PIN = 9;
const int CODE_LEN = 4;
const int SECRET[CODE_LEN] = {1, 3, 2, 2};

const int ANGLE_LOCKED = 90;   // засов задвинут
const int ANGLE_OPEN = 0;      // засов убран
const int OPEN_SECONDS = 5;    // сколько секунд сейф стоит открытым

int attempt[CODE_LEN];
int entered = 0;
bool wasPressed[3] = {false, false, false};

void setup() {
  lcd.begin(16, 2);
  for (int i = 0; i < 3; i++) {
    pinMode(BUTTONS[i], INPUT_PULLUP);
  }
  zasov.attach(SERVO_PIN);
  zasov.write(ANGLE_LOCKED);   // при включении сейф заперт
  showInput();
}

void loop() {
  for (int i = 0; i < 3; i++) {
    bool pressed = (digitalRead(BUTTONS[i]) == LOW);
    if (pressed && !wasPressed[i]) {
      addPress(i + 1);
      delay(30);
    }
    wasPressed[i] = pressed;
  }
}

void addPress(int number) {
  attempt[entered] = number;
  entered = entered + 1;
  showInput();
  if (entered == CODE_LEN) {
    checkCode();
  }
}

void checkCode() {
  if (codeCorrect()) {
    openSafe();
  } else {
    lcd.clear();
    lcd.print("Nevernyy kod");
    delay(2000);
  }
  entered = 0;
  showInput();
}

void openSafe() {
  zasov.write(ANGLE_OPEN);   // засов уехал — дверцу можно открыть
  // Пока сейф открыт, кнопки слушать не нужно — честно ждём с отсчётом.
  for (int s = OPEN_SECONDS; s >= 1; s--) {
    lcd.clear();
    lcd.print("OTKRYTO!");
    lcd.setCursor(0, 1);
    lcd.print("Zakroyu cherez ");
    lcd.print(s);
    delay(1000);
  }
  zasov.write(ANGLE_LOCKED);   // время вышло — заперли обратно
}

bool codeCorrect() {
  for (int i = 0; i < CODE_LEN; i++) {
    if (attempt[i] != SECRET[i]) {
      return false;
    }
  }
  return true;
}

void showInput() {
  lcd.clear();
  lcd.print("Vvedi kod:");
  lcd.setCursor(0, 1);
  for (int i = 0; i < CODE_LEN; i++) {
    if (i < entered) {
      lcd.print("* ");
    } else {
      lcd.print("_ ");
    }
  }
}

Пока сейф открыт, программа честно стоит в цикле с delay — и это правильно. В уроке 24 мы воевали с delay, потому что шлагбаум должен был слушать кнопку во время движения. А открытому сейфу кнопки слушать незачем — ему положено просто ждать. Блокирующее ожидание уместно, когда ждать — и есть вся работа: об этом был урок 21.

Проверьте: верный код — засов отъезжает, на экране «Zakroyu cherez 5… 4… 3…», потом засов возвращается. При включении сейф сразу запирается — это строка с zasov.write(ANGLE_LOCKED) в самом начале.

Шаг 5. Полный сейф: счётчик ошибок и блокировка

Осталась защита от подбора. Заводим счётчик неверных попыток: ошибка — плюс один, верный код — сброс в ноль. Три ошибки подряд — и сейф на пятнадцать секунд перестаёт замечать кнопки:

// Урок 29. Шаг 5: полный сейф — три ошибки подряд включают блокировку.

#include <LiquidCrystal.h>
#include <Servo.h>

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
Servo zasov;

const int BUTTONS[3] = {7, 8, 10};
const int SERVO_PIN = 9;
const int CODE_LEN = 4;
const int SECRET[CODE_LEN] = {1, 3, 2, 2};

const int ANGLE_LOCKED = 90;
const int ANGLE_OPEN = 0;
const int OPEN_SECONDS = 5;
const int MAX_FAILS = 3;          // сколько ошибок сейф терпит
const int LOCKOUT_SECONDS = 15;   // и сколько потом не слушает кнопки

int attempt[CODE_LEN];
int entered = 0;
bool wasPressed[3] = {false, false, false};
int fails = 0;   // счётчик неверных попыток подряд

void setup() {
  lcd.begin(16, 2);
  for (int i = 0; i < 3; i++) {
    pinMode(BUTTONS[i], INPUT_PULLUP);
  }
  zasov.attach(SERVO_PIN);
  zasov.write(ANGLE_LOCKED);
  showInput();
}

void loop() {
  for (int i = 0; i < 3; i++) {
    bool pressed = (digitalRead(BUTTONS[i]) == LOW);
    if (pressed && !wasPressed[i]) {
      addPress(i + 1);
      delay(30);
    }
    wasPressed[i] = pressed;
  }
}

void addPress(int number) {
  attempt[entered] = number;
  entered = entered + 1;
  showInput();
  if (entered == CODE_LEN) {
    checkCode();
  }
}

void checkCode() {
  if (codeCorrect()) {
    fails = 0;   // верный код прощает старые ошибки
    openSafe();
  } else {
    fails = fails + 1;
    if (fails >= MAX_FAILS) {
      lockout();
    } else {
      lcd.clear();
      lcd.print("Nevernyy kod");
      lcd.setCursor(0, 1);
      lcd.print("Popytka ");
      lcd.print(fails);
      lcd.print(" iz ");
      lcd.print(MAX_FAILS);
      delay(2000);
    }
  }
  entered = 0;
  showInput();
}

void lockout() {
  // Сейф нарочно перестаёт слушать кнопки: жать бесполезно, надо ждать.
  for (int s = LOCKOUT_SECONDS; s >= 1; s--) {
    lcd.clear();
    lcd.print("STOP! 3 oshibki");
    lcd.setCursor(0, 1);
    lcd.print("Zhdi ");
    lcd.print(s);
    lcd.print(" s");
    delay(1000);
  }
  fails = 0;   // наказание отбыто — счётчик обнуляется
}

void openSafe() {
  zasov.write(ANGLE_OPEN);
  for (int s = OPEN_SECONDS; s >= 1; s--) {
    lcd.clear();
    lcd.print("OTKRYTO!");
    lcd.setCursor(0, 1);
    lcd.print("Zakroyu cherez ");
    lcd.print(s);
    delay(1000);
  }
  zasov.write(ANGLE_LOCKED);
}

bool codeCorrect() {
  for (int i = 0; i < CODE_LEN; i++) {
    if (attempt[i] != SECRET[i]) {
      return false;
    }
  }
  return true;
}

void showInput() {
  lcd.clear();
  lcd.print("Vvedi kod:");
  lcd.setCursor(0, 1);
  for (int i = 0; i < CODE_LEN; i++) {
    if (i < entered) {
      lcd.print("* ");
    } else {
      lcd.print("_ ");
    }
  }
}

Что здесь нового:

  • после ошибки экран показывает счёт: «Popytka 1 iz 3» — честное предупреждение;
  • блокировка — тот же приём отсчёта, что и у открытого сейфа: пока идёт цикл, нажатия просто никто не читает;
  • верный код прощает старые ошибки — счётчик обнуляется.

Это и есть готовый проект. Прошейте, позовите домашних и предложите угадать код: пусть сейф сам объяснит им, что торопиться не стоит.

Не работает? Разбираемся

  • Экран пустой или два ряда квадратов. Это контраст: медленно покрутите потенциометр в обе стороны, как в уроке 26.
  • Звёздочки сыплются сами, без нажатий. Какая-то кнопка стоит повёрнутой на 90 градусов и всегда замкнута. Выньте её и посадите так, чтобы ножки выходили вверх и вниз — про эту ловушку был урок 10.
  • Одно нажатие — сразу две звёздочки. Дребезг сильнее обычного: увеличьте паузу после нажатия с 30 до 50–80 миллисекунд.
  • Какая-то кнопка молчит. Проверьте её пару проводов: сигнал должен уходить с верхней стороны канавки, земля — с нижней. Если оба провода с одной стороны, кнопка не работает — обе ножки там соединены всегда.
  • Сервопривод дрожит и жужжит у крайнего положения. Он упирается в край хода. Поменяйте углы с 0 и 90 на 10 и 80 — смыслу это не мешает.
  • При повороте засова экран мигает или плата перезагружается. Сервопривод под нагрузкой берёт много тока. Уберите с качалки лишний вес, проверьте, что красный провод сидит на «+» плотно, и подключайте плату в порт компьютера напрямую, без длинных удлинителей.
  • Сейф говорит «Nevernyy kod» на верный код. Проверьте порядок кнопок: левая должна сидеть на выводе 7, средняя на 8, правая на 10. Перепутанные провода — это другой код.

Задания для самостоятельной работы

  • Задание 1. Смените код на свой и удлините его до пяти нажатий. Понадобится поменять длину кода в одном месте и добавить пятое число в секрет — всё остальное программа пересчитает сама. Заодно звёздочек станет пять.
  • Задание 2. Пусть открытый сейф закрывается не по отсчёту, а по нажатию любой кнопки — «закрой за мной». Пригодится цикл ожидания из урока 21: стоять на месте, пока все три кнопки отпущены.
  • Задание со звёздочкой ⭐. Озвучьте сейф зуммером: короткий щелчок при каждом нажатии, весёлая трель при открытии, низкий гудок при ошибке и сирена во время блокировки. Зуммер сядет на свободный вывод 6.
Разбор задания со звёздочкой

Зуммер ставится в свободную середину макетки: ножка со знаком «+» — в столбик 12 (ряд b), вторая — в столбик 14. Из столбика 12 провод идёт на вывод 6, а из столбика 14 — на верхнюю шину «−». Звуки — это tone из урока 19: у щелчка частота высокая и длительность крошечная, у гудка — наоборот. Сирена — два тона по очереди в каждом витке отсчёта блокировки.

// Урок 29. Разбор задания: сейф со звуком — щелчки, трель и сирена.

#include <LiquidCrystal.h>
#include <Servo.h>

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);
Servo zasov;

const int BUTTONS[3] = {7, 8, 10};
const int SERVO_PIN = 9;
const int BUZZER_PIN = 6;   // зуммер поселился на свободном выводе 6

const int CODE_LEN = 4;
const int SECRET[CODE_LEN] = {1, 3, 2, 2};

const int ANGLE_LOCKED = 90;
const int ANGLE_OPEN = 0;
const int OPEN_SECONDS = 5;
const int MAX_FAILS = 3;
const int LOCKOUT_SECONDS = 15;

int attempt[CODE_LEN];
int entered = 0;
bool wasPressed[3] = {false, false, false};
int fails = 0;

void setup() {
  lcd.begin(16, 2);
  for (int i = 0; i < 3; i++) {
    pinMode(BUTTONS[i], INPUT_PULLUP);
  }
  zasov.attach(SERVO_PIN);
  zasov.write(ANGLE_LOCKED);
  showInput();
}

void loop() {
  for (int i = 0; i < 3; i++) {
    bool pressed = (digitalRead(BUTTONS[i]) == LOW);
    if (pressed && !wasPressed[i]) {
      addPress(i + 1);
      delay(30);
    }
    wasPressed[i] = pressed;
  }
}

void addPress(int number) {
  tone(BUZZER_PIN, 3000, 20);   // короткий щелчок, как у домофона
  attempt[entered] = number;
  entered = entered + 1;
  showInput();
  if (entered == CODE_LEN) {
    checkCode();
  }
}

void checkCode() {
  if (codeCorrect()) {
    fails = 0;
    tone(BUZZER_PIN, 800, 120);    // трель открытия: две ноты вверх
    delay(150);
    tone(BUZZER_PIN, 1200, 200);
    openSafe();
  } else {
    fails = fails + 1;
    tone(BUZZER_PIN, 200, 400);    // низкий гудок ошибки
    if (fails >= MAX_FAILS) {
      lockout();
    } else {
      lcd.clear();
      lcd.print("Nevernyy kod");
      lcd.setCursor(0, 1);
      lcd.print("Popytka ");
      lcd.print(fails);
      lcd.print(" iz ");
      lcd.print(MAX_FAILS);
      delay(2000);
    }
  }
  entered = 0;
  showInput();
}

void lockout() {
  for (int s = LOCKOUT_SECONDS; s >= 1; s--) {
    lcd.clear();
    lcd.print("STOP! 3 oshibki");
    lcd.setCursor(0, 1);
    lcd.print("Zhdi ");
    lcd.print(s);
    lcd.print(" s");
    tone(BUZZER_PIN, 700, 220);   // сирена: два тона каждую секунду
    delay(500);
    tone(BUZZER_PIN, 500, 220);
    delay(500);
  }
  fails = 0;
}

void openSafe() {
  zasov.write(ANGLE_OPEN);
  for (int s = OPEN_SECONDS; s >= 1; s--) {
    lcd.clear();
    lcd.print("OTKRYTO!");
    lcd.setCursor(0, 1);
    lcd.print("Zakroyu cherez ");
    lcd.print(s);
    delay(1000);
  }
  zasov.write(ANGLE_LOCKED);
}

bool codeCorrect() {
  for (int i = 0; i < CODE_LEN; i++) {
    if (attempt[i] != SECRET[i]) {
      return false;
    }
  }
  return true;
}

void showInput() {
  lcd.clear();
  lcd.print("Vvedi kod:");
  lcd.setCursor(0, 1);
  for (int i = 0; i < CODE_LEN; i++) {
    if (i < entered) {
      lcd.print("* ");
    } else {
      lcd.print("_ ");
    }
  }
}

Обратите внимание: звук нигде не останавливает программу — у каждого tone задана длительность, и он заканчивается сам. А в цикле блокировки две ноты вместе с двумя паузами по 500 миллисекунд как раз складываются в одну секунду отсчёта.

Что мы узнали

  • секрет удобно хранить массивом, а нажатия — копить во второй такой же;
  • два списка сравнивают циклом, по одной паре клеток за раз;
  • прервать проверку на первом несовпадении — нормально: ответ уже известен;
  • счётчик ошибок и блокировка — та же защита от подбора, что в телефонах;
  • блокирующее ожидание уместно, когда ждать — и есть вся работа устройства;
  • детали из разных уроков собираются в устройство, которое хранит секрет.

В следующем уроке кнопки и зуммер останутся на макетке: соберём игру «Повтори за мной» — она сама придумывает мелодии-задания и проверяет вашу память на скорость.

Частые вопросы

Как сделать кодовый замок на Arduino из обычных кнопок?

Код — это последовательность нажатий, например 1-3-2-2. Секрет хранится в массиве, нажатия копятся во второй такой же массив, а после четвёртого нажатия программа сравнивает их циклом — пара за парой. На экране вместо цифр показываются звёздочки.

Почему нельзя сравнить два массива одним знаком равенства?

Знак сравнения в Arduino сравнивает два отдельных числа, а не списки целиком. Массивы сравнивают циклом: первая клетка с первой, вторая со второй. На первом же несовпадении проверку можно прерывать — ответ уже известен.

Как Arduino двигает засов сейфа?

Засов — сервопривод SG90 из набора на выводе 9: угол 90 градусов — заперто, 0 — открыто. К качалке скотчем крепится картонная полоска, она и держит дверцу. Через пять секунд после открытия программа сама возвращает засов на место.

Что происходит при неверном коде?

Сейф считает ошибки и честно показывает счёт: «Popytka 1 iz 3». После третьей ошибки подряд включается блокировка — пятнадцать секунд сейф не реагирует на кнопки и ведёт отсчёт на экране. Верный код обнуляет счётчик.

Можно ли сделать код длиннее четырёх нажатий?

Да. Длина кода задана одной константой: поменяйте её, допишите числа в секрет — и всё остальное программа пересчитает сама. С тремя кнопками и длиной пять получается 243 варианта кода.

← Все уроки курса