GSM-реле для електрозамка й сигналізації на Arduino Nano і SIM800L
Стан проєкту
Це реле, яким керують звичайним дзвінком: телефонуєте на SIM-карту пристрою, модуль SIM800L відповідає, а цифрами з клавіатури телефону ви вмикаєте чи вимикаєте навантаження. Без інтернету, застосунків і хмарних сервісів — працює скрізь, де є мобільний звʼязок. Ми керуємо ним електроригельним замком вхідних дверей: дзвінок — і двері відчинено. Так само роблять GSM-розетку, вмикають котел перед приїздом на дачу чи відчиняють ворота.
Заодно пристрій стереже ці двері: якщо під охороною їх відчинити, він сам зателефонує власникові. Акумулятор 18650 тримає його, коли зникає світло, — саме тоді, коли охорона потрібна найбільше.
Що вміє
| Клавіша | Дія | Підказка |
|---|---|---|
# | поставити на охорону й зачинити замок | «Охорону ввімкнено» |
* | зняти з охорони | «Охорону вимкнено» |
4 | увімкнути реле: відчинити замок і зняти охорону | «Двері відчинено» |
2 | вимкнути реле: зачинити замок | «Двері зачинено» |
- Відповідає лише номерам із білого списку, решту дзвінків скидає.
- За бажанням просить PIN-код після відповіді. Номер того, хто дзвонить, можна підробити, тож для замка вхідних дверей код варто задати обовʼязково. Правильний код пристрій підтверджує голосом, а після трьох хибних кладе слухавку.
- Відчиняючи замок, заодно знімає охорону, щоб двері, відчинені власником, не підняли тривогу. Замок лишається відчиненим, доки не наберете
2або#— постановка на охорону теж зачиняє замок. - Під охороною, коли відчиняють двері, обдзвонює номери власників, доки хтось не підтвердить тривогу. Той, хто відповів, чує «Тривога» й натискає будь-яку клавішу, а зірочка заодно знімає охорону. Поки клавішу не натиснуто, фраза повторюється, а за 40 секунд пристрій кладе слухавку й дзвонить далі. Так обдзвін не зупинить голосова пошта, яка «відповідає» замість вимкненого телефона.
- Після зникнення живлення повертається під охорону, якщо був під нею. Реле під час запуску завжди вимкнено.
Що потрібно
- Arduino Nano (ATmega328), без штирьових розʼємів — дроти паяються просто в отвори.
- GSM-модуль SIM800L і плоска антена на кабелі з розʼємом IPEX.
- Модуль реле 5 В на 1 канал, вузький, з контактами COM, NO і NC.
- Електроригельний замок на 12 В. У нас — накладний SEVEN Z-727: без живлення відчинений, з вбудованим таймером затримки.
- Модуль заряджання TP4056 із захистом, з гніздом micro-USB, і резистор 2,4 кОм, щоб зменшити струм заряду.
- Акумулятор 18650 у тримачі.
- Безперебійний блок живлення 12 В з акумулятором, від 2 А: для самого пристрою й замка. Чому саме безперебійний — нижче, в розділі про замок.
- Понижувальний модуль LM2596 — дає 5 В для заряджання.
- Підвищувальний модуль MT3608 (або XL6009) — дає 5 В від акумулятора.
- Геркон із магнітом, резистор 4,7 кОм і конденсатор 100 нФ до нього.
- Два світлодіоди й два резистори 330 Ом.
- Конденсатор 1000 мкФ на 6,3 В або більше.
- Два резистори 10 кОм для дільника.
- Кремнієвий діод 1N4148 (або 1N914) — не Шотткі.
- Запобіжник на 2 А з тримачем і дріт від 1 мм² для кола замка.
- SIM-карта з голосовими дзвінками, PIN-код на ній вимкнено. SIM800L працює лише в мережах 2G: перевірте, що оператор у вашій місцевості їх підтримує.
Корпус
Корпус складено в конструкторі BoxForma: 120 × 78 × 33 мм усередині. Nano, SIM800L і модуль заряджання лежать на дні в друкованих притискачах, а реле й LM2596 стоять на ребрі в пазах. Гніздо micro-USB модуля заряджання й mini-USB Nano виходять у стінки, поруч — круглий отвір 7 мм під дроти. Уздовж бічної стінки лишається місце під акумулятор у тримачі, а прорізи в передній стінці провітрюють корпус.
Друкували так (Cura):
| Параметр | Значення |
|---|---|
| Принтер | Creality Ender-3, сопло 0.4 мм |
| Слайсер | Cura 5.13 |
| Пластик | PETG: сопло 240 °C, стіл 70 °C |
| Шар | 0.2 мм, перший 0.3 мм |
| Стінки | 1.2 мм, три лінії |
| Заповнення | 15 % |
| Підтримки | не потрібні |
| Прилипання | кайма 5 мм |
| Швидкість | 40 мм/с, стінки 25 мм/с |
| Корпус без кришки | 8 год 36 хв, 61 г пластику |
| Кришка | 4 год 41 хв, 27 г пластику за розрахунком Cura |
Антену не виводять назовні: плоска плата на кабелі вміщується всередині, пластик сигнал майже не глушить. Приклейте її двостороннім скотчем до кришки, подалі від акумулятора й LM2596, а кабель біля розʼєму прихопіть краплею термоклею — розʼєм IPEX тендітний.
Схема підключення
Живлення побудовано так, щоб пристрій нічого не перемикав, коли зникає світло: усе, крім заряджання, завжди живиться від акумулятора, а блок живлення лише тримає його зарядженим.
- 12 В. Від блока живлення струм іде двома шляхами: через запобіжник і контакти реле на замок і на понижувальний модуль LM2596. Запобіжник беріть трохи більший за найбільший струм замка: для SEVEN Z-727 (до 1,2 А) — на 2 А. Без нього коротке замикання в замку чи дротах до дверей спалить доріжки реле.
- Мінус замка ведіть окремим дротом від 1 мм² прямо на мінус блока живлення, а не через плати. Інакше струм замка піде тонкою спільною землею, і Nano з модемом почнуть збоїти.
- Заряджання. LM2596 знижує 12 В до 5 В і подає їх на вхід IN+ модуля заряджання TP4056. Перш ніж підключати TP4056, виставте на виході LM2596 5,0 В підстроювальним резистором. Акумулятор підʼєднано до B+ і B−.
- Струм заряду. Модуль TP4056 за замовчуванням заряджає струмом 1 А й розсіює на собі до 1,3 Вт — у закритому корпусі він сильно нагрівається. Замініть на ньому резистор R_PROG (здебільшого це R3 з маркуванням 122, тобто 1,2 кОм) на 2,4 кОм (маркування 242): струм заряду стане близько 0,5 А. Акумулятор і так увесь час підзаряджається, великий струм йому не потрібен.
- 5 В для Nano й реле. З виходу OUT+ і OUT− модуля заряджання напруга акумулятора (3–4,2 В) іде на підвищувальний модуль MT3608, а той дає 5,0 В на вивід 5V Nano й на VCC модуля реле. Вихід MT3608 теж виставте на 5,0 В до підключення Nano. Для MT3608 у корпусі немає окремого кріплення: приклейте його двостороннім скотчем на вільне місце.
- SIM800L — напряму від акумулятора. Модем підʼєднано до тих самих OUT+ і OUT−, паралельно до входу MT3608, а не до 5 В. Йому потрібно 3,4–4,4 В, тобто якраз напруга акумулятора: від 5 В модуль згорить. Ще й під час передачі він бере короткі імпульси до 2 А: акумулятор віддає їх без зусиль, а маленький підвищувальний модуль просідав би, і модем перезапускався б. Конденсатор 1000 мкФ поставте просто на виводи живлення модема, дроти живлення — короткі, від 0,5 мм².
- USB. Коли Nano підключено до компʼютера для завантаження скетча, 5 В із USB не потрапляють в акумулятор: підвищувальний модуль пропускає струм лише в один бік.
- Модем → Nano. Логіка SIM800L розрахована на 2,8 В, а Nano на 5 В гарантовано бачить «одиницю» лише від 3,0 В. Тому TXD модема йде на D10 через кремнієвий діод 1N4148: анод — до D10, катод — до TXD. Скетч вмикає на D10 вбудований підтягувальний резистор до +5 В, а діод притискає D10 до напруги на TXD плюс своє падіння: «одиниця» — близько 3,3 В, «нуль» — близько 0,5 В. Діод Шотткі не годиться: у нього падіння замале, і до 3,0 В не дотягне.
- Nano → модем. D11 іде на RXD через дільник: D11 → 10 кОм → RXD → 10 кОм → GND, тобто половина: 2,5–2,6 В із 5,0–5,2 В. Модулю потрібно від 2,1 до 3,1 В.
- Геркон — між D8 і GND. Наш геркон розімкнутий, коли двері зачинено, і замикається, коли їх відчиняють. Дріт до дверей довгий і ловить наводки, тож просто біля Nano поставте резистор 4,7 кОм від D8 до +5 В і конденсатор 100 нФ від D8 до GND — без них можливі хибні тривоги.
- Реле — на D12, не на D13: виводом D13 під час скидання блимає завантажувач, і реле клацало б.
- Земля. GND усіх плат і мінус блока живлення зʼєднайте разом, інакше Nano й модем не порозуміються.
Електроригельний замок
У нас стоїть накладний електроригельний замок SEVEN Z-727: 12 В, близько 200 мА, поки тримає ригель, і до 1,2 А в момент спрацювання. Без живлення він відчинений, тобто ригель висунутий лише тоді, коли на замок подано 12 В. Такі замки ставлять на виходах, які мають відчинятися, коли зникає живлення, наприклад під час пожежі.
- Контакти реле — COM і NC. Плюс 12 В через запобіжник іде на COM, а з NC — на плюс замка. Поки реле вимкнене, контакт NC замкнений, замок під напругою й двері зачинено.
4вмикає реле: NC розмикається, замок знеструмлюється, ригель ховається.2вимикає реле, напруга повертається, і замок знову зачиняє двері. Під час запуску пристрою реле вимкнене, тож двері зачинені. - Таймер замка. Після того як живлення повертається, Z-727 чекає кілька секунд (у нас — 3) і лише тоді висуває ригель, щоб двері встигли зачинитися. Скетчу про це знати не треба: він лише вмикає й вимикає реле.
- Дроти замка підключайте за його інструкцією: крім живлення, такі замки часто мають виходи стану ригеля чи дверей. Діод паралельно до замка зі своєю електронікою не ставте без потреби: захист від викиду напруги в таких замках зазвичай уже є, а чи потрібен зовнішній, скаже інструкція.
Безперебійний блок живлення обовʼязковий
Якщо ж реле вмикає інше навантаження — котел, насос, ворота, — підключайте його до COM і NO, щоб без команди воно було вимкнене. Для котушки, насоса чи двигуна без власної електроніки поставте паралельно до навантаження діод 1N4007 катодом до плюса: він гасить викид напруги, коли реле розмикає коло, і береже контакти.
Одна тонкість модуля TP4056: поки з виходу OUT+ живиться пристрій, модуль бачить цей струм як струм заряду й може так і не завершити заряджання, тримаючи акумулятор на 4,2 В. Це безпечно, але скорочує строк служби акумулятора. Якщо пристрій працюватиме роками, акумулятор варто міняти раз на кілька років.
Обережно з 230 В
Скетч
Завантажте syhnalizatsiia.ino і відкрийте в Arduino IDE (вона запропонує створити для нього теку — погодьтеся). Плата — Arduino Nano; для китайських клонів процесор зазвичай «ATmega328P (Old Bootloader)».
Перед завантаженням у плату змініть налаштування на початку файлу:
| Налаштування | Що задати |
|---|---|
ALARM_PHONES | кому дзвонити під час тривоги, по черзі |
ALLOWED_PHONES | хто може зателефонувати й керувати |
DTMF_PIN | PIN-код після відповіді; "" — без коду |
RELAY_ON / RELAY_OFF | поміняйте місцями, якщо ваше реле вмикається високим рівнем |
DOOR_OPEN_LEVEL | LOW для геркона, як на схемі; HIGH, якщо геркон на +5 В |
UNLOCK_DISARMS | true — 4 заодно знімає охорону, а # зачиняє замок; false — реле й охорона незалежні (для котла, насоса, воріт) |
Скетч не чекає на модем, зупиняючи все інше: він розбирає відповіді SIM800L рядок за рядком і веде дзвінок як послідовність станів. Тому геркон опитується й під час розмови. Раз на пів хвилини скетч перевіряє, що модем живий, у мережі й не загубив налаштувань, а якщо той сам перезапустився від просідання живлення — налаштовує його знову. Під час першого запуску скетч записує в модем швидкість 9600 замість автопідбору: в автопідборі модем після перезапуску мовчить, і скетч не дізнався б про нього. З AT+COLP=1 модем повідомляє про відповідь, лише коли абонент справді підняв слухавку, тож тривожне повідомлення не прозвучить у гудки.
Увесь код скетча
/*
GSM-сигналізація і реле з керуванням тонами DTMF
Arduino Nano + SIM800L
© 2026 BoxForma (boxforma.com). Ліцензія MIT, повний текст — у LICENSE.txt:
можна вільно копіювати, змінювати й поширювати, зберігши ці рядки.
Надається «як є», без жодних гарантій; автори не відповідають за наслідки
використання. Пристрій ви збираєте й підключаєте на власний ризик.
Зателефонуйте на SIM-карту пристрою з номера з білого списку: він
відповість, і далі команди набирають на клавіатурі телефону.
# — поставити на охорону * — зняти з охорони
4 — увімкнути реле 2 — вимкнути реле
(для електрозамка: 4 — відчинити, 2 — зачинити)
Якщо під охороною відчинити двері, пристрій обдзвонить номери з
ALARM_PHONES. Той, хто відповів, почує повідомлення про тривогу й має
підтвердити його будь-якою клавішею (зірочка заодно знімає охорону). Без
підтвердження дзвінок вважається неприйнятим і обдзвін триває: так його не
зупинить голосова пошта чи автовідповідач.
Голосові підказки — файли C:\User\N.amr у памʼяті SIM800L:
2 — під охороною, 3 — охорону знято, 4 — тривога,
5 — реле ввімкнено (двері відчинено), 6 — реле вимкнено (двері зачинено),
7 — код прийнято, 8 — хибний код (лише якщо задано DTMF_PIN).
Як записати й завантажити свої — див. статтю на boxforma.com/statti.
*/
#include <SoftwareSerial.h>
#include <EEPROM.h>
#include <avr/wdt.h>
// ============================ НАЛАШТУВАННЯ ============================
// Кому дзвонити під час тривоги, по черзі
// Замініть на свої номери у форматі +380XXXXXXXXX
const char* const ALARM_PHONES[] = {"+380XXXXXXXXX"};
// Хто може зателефонувати й керувати
const char* const ALLOWED_PHONES[] = {
"+380XXXXXXXXX", "+380YYYYYYYYY",
};
// Номери порівнюються за останніми цифрами: +380501234567 і 0501234567 — один номер
const uint8_t MATCH_DIGITS = 9;
// PIN-код, який треба набрати після відповіді, перш ніж пристрій прийме
// команди. "" — без коду. Номер того, хто дзвонить, можна підробити, тож
// для реле на котлі чи воротах код краще задати (не довший за 7 цифр).
// Після правильного коду звучить 7.amr, після хибного — 8.amr.
const char DTMF_PIN[] = "";
const uint8_t PIN_MAX_TRIES = 3; // стільки хибних кодів — і слухавку покладено
// Реле керує замком дверей, які стереже геркон: «4» (відчинити) заодно знімає
// охорону, інакше двері, відчинені власником, одразу підняли б тривогу.
// І навпаки, «#» заодно зачиняє замок, щоб охорону не поставили з відчиненим.
// false — реле й охорона незалежні (для котла, насоса, воріт).
const bool UNLOCK_DISARMS = true;
const uint8_t ALARM_ROUNDS = 3; // скільки разів обійти ALARM_PHONES, якщо ніхто не відповів
const unsigned long DIAL_PAUSE_MS = 5000; // пауза між дзвінками обдзвону
const unsigned long DIAL_TIMEOUT_MS = 60000UL; // стільки чекаємо відповіді на вихідний дзвінок
const unsigned long ALARM_ACK_MS = 40000UL; // стільки чекаємо клавішу-підтвердження в тривожному дзвінку
const unsigned long ALARM_REPEAT_MS = 6000; // як часто повторювати тривожне повідомлення, поки не підтвердили
const unsigned long CALL_MAX_MS = 180000UL; // будь-яку розмову кладемо через 3 хвилини
const unsigned long DOOR_DEBOUNCE_MS = 300; // стільки геркон має тримати стан, щоб йому повірити
const unsigned long MODEM_CHECK_MS = 30000UL; // як часто перевіряти модем і мережу
// Модуль реле вмикається низьким рівнем: під час запуску на виводі HIGH,
// «4» дає LOW, «2» — HIGH. Якщо ваше реле насправді вмикається високим
// рівнем, поміняйте ці два рядки місцями. Електрозамок, відчинений без
// живлення (як SEVEN Z-727), підключають до контактів COM і NC: поки реле
// вимкнене, замок під напругою й двері зачинено.
#define RELAY_ON LOW
#define RELAY_OFF HIGH
// Сторожовий таймер перезапустить плату, якщо вона зависне. Вмикайте (1),
// лише якщо скетч завантажується з «Processor: ATmega328P», а не «(Old
// Bootloader)»: старий завантажувач клонів Nano з ним іде в нескінченне
// перезавантаження, і плату потім рятує лише програматор.
#define USE_WATCHDOG 0
// Рівень на D8, коли двері відчинено. Геркон розімкнутий, поки двері
// зачинено, і замикається, коли їх відчиняють.
// LOW — геркон між D8 і GND (як на схемі монтажу). D8 тягне до плюса
// вбудований резистор, а для довгого дроту до дверей біля Nano ще
// 4,7 кОм від D8 до +5 В і 100 нФ від D8 до GND — проти хибних тривог;
// HIGH — геркон між +5 В і D8, резистор 10 кОм тягне D8 до GND; вбудований
// резистор тоді вимкнено, щоб не сперечався із зовнішнім.
#define DOOR_OPEN_LEVEL LOW
// Номери звукових файлів C:\User\N.amr: у кожної дії своя фраза
enum Sound : uint8_t {
SND_ARMED = 2,
SND_DISARMED = 3,
SND_ALARM = 4,
SND_RELAY_ON = 5,
SND_RELAY_OFF = 6,
SND_PIN_OK = 7,
SND_PIN_BAD = 8,
};
// ============================== ВИВОДИ ==============================
// RX, TX. Логіка модуля — 2,8 В, а Nano на 5 В гарантовано бачить «одиницю» лише від 3,0 В:
// TXD модема → D10 через кремнієвий діод 1N4148: анод — до D10, катод — до TXD.
// SoftwareSerial сам вмикає на D10 вбудований підтягувальний резистор до +5 В,
// а діод притискає D10 до напруги на TXD плюс своє падіння:
// «одиниця» — близько 3,3 В, «нуль» — близько 0,5 В.
// Діод Шотткі не годиться: у нього падіння замале, і до 3,0 В не дотягне;
// D11 → RXD через дільник: D11 — 10 кОм — RXD — 10 кОм — GND, це половина:
// 2,5–2,6 В із 5,0–5,2 В (модулю треба від 2,1 до 3,1 В)
SoftwareSerial SIM800(10, 11);
#define PIN_RELAY 12 // не D13: ним під час скидання блимає завантажувач, і реле клацало б
#define PIN_DOOR 8 // геркон дверей, див. DOOR_OPEN_LEVEL
#define PIN_LED_DOOR A0 // під охороною й двері зачинено
#define PIN_LED_ARMED A1 // під охороною
// Комірка EEPROM: охорона зберігається, коли зникає живлення чи плата перезапускається
#define EE_ARMED 0
// ============================== СТАН ==============================
enum CallState : uint8_t { CALL_IDLE, CALL_INCOMING, CALL_DIALING, CALL_ACTIVE };
CallState callState = CALL_IDLE;
bool callIsAlarm = false; // поточний дзвінок — наш тривожний
unsigned long callStartedAt = 0;
unsigned long lastRingAt = 0;
unsigned long lastCallEndedAt = 0;
bool callerChecked = false; // номер вхідного вже перевірено
bool armed = false;
bool alarmLatched = false; // тривога спрацювала й чекає, поки двері знову зачинять
bool alarmCalling = false; // триває обдзвін: тривогу ще ніхто не підтвердив клавішею
uint8_t alarmAttempt = 0;
unsigned long alarmMsgAt = 0; // коли востаннє звучало тривожне повідомлення
bool unlocked = false; // PIN набрано (або він не потрібен)
char pinBuf[8];
uint8_t pinLen = 0;
uint8_t pinTries = 0;
// Розбір рядків модема лише ставить прапорці, а команди модему надсилає loop():
// так відповідь на одну команду не переплутається з іншою.
bool wantAnswer = false;
bool wantHangup = false;
bool wantAlarmMessage = false;
bool wantModemInit = false;
char keys[8];
uint8_t keyCount = 0;
bool registered = false;
bool baudFixed = false; // у модемі збережено швидкість 9600, а не автопідбір
uint8_t modemFails = 0;
unsigned long lastModemCheck = 0;
bool doorOpen = false;
bool doorRaw = false;
unsigned long doorChangedAt = 0;
char line[96];
uint8_t lineLen = 0;
static_assert(sizeof(DTMF_PIN) <= sizeof(pinBuf), "DTMF_PIN не довший за 7 цифр");
// ============================== ЗАПУСК ==============================
void setup() {
MCUSR = 0;
wdt_disable();
pinMode(PIN_LED_DOOR, OUTPUT);
pinMode(PIN_LED_ARMED, OUTPUT);
// під час запуску реле вимкнено: рівень задаємо до того, як вивід стане виходом,
// інакше він на мить піде в LOW і реле клацне
digitalWrite(PIN_RELAY, RELAY_OFF);
pinMode(PIN_RELAY, OUTPUT);
pinMode(PIN_DOOR, DOOR_OPEN_LEVEL == HIGH ? INPUT : INPUT_PULLUP);
// охорона — така, як була до зникнення живлення
armed = EEPROM.read(EE_ARMED) == 1;
doorRaw = doorOpen = digitalRead(PIN_DOOR) == DOOR_OPEN_LEVEL;
// двері відчинено вже під час увімкнення: тривога лише тоді, коли їх зачинять і знову відчинять
alarmLatched = armed && doorOpen;
Serial.begin(9600);
SIM800.begin(9600);
Serial.println(F("Старт"));
#if USE_WATCHDOG
wdt_enable(WDTO_8S);
#endif
initModem();
}
void loop() {
feedWatchdog();
while (readLine()) {
logLine(line);
handleLine(line);
}
doQueuedActions();
serviceCall();
serviceDoor();
serviceAlarm();
serviceModem();
updateLeds();
// команди модему з монітора порту, для налагодження
while (Serial.available()) SIM800.write(Serial.read());
}
// ============================== МОДЕМ ==============================
void feedWatchdog() {
#if USE_WATCHDOG
wdt_reset();
#endif
}
// Збирає рядок відповіді модема в line; true, коли надійшов цілий непорожній рядок
bool readLine() {
while (SIM800.available()) {
char c = SIM800.read();
if (c == '\r') continue;
if (c == '\n') {
if (lineLen == 0) continue;
line[lineLen] = '\0';
lineLen = 0;
return true;
}
if (lineLen < sizeof(line) - 1) line[lineLen++] = c;
}
return false;
}
void logLine(const char* s) {
Serial.print(F("<< "));
Serial.println(s);
}
// Надсилає команду й чекає OK чи ERROR; усе інше, що надійде, розбирається як завжди
bool sendAT(const char* cmd, unsigned long timeout = 2000) {
Serial.print(F(">> "));
Serial.println(cmd);
SIM800.println(cmd);
unsigned long start = millis();
while (millis() - start < timeout) {
feedWatchdog();
if (!readLine()) continue;
logLine(line);
if (strcmp(line, "OK") == 0) return true;
if (strcmp(line, "ERROR") == 0 || strncmp(line, "+CME ERROR", 10) == 0) return false;
handleLine(line);
}
Serial.println(F("Немає відповіді"));
return false;
}
bool initModem() {
// модему потрібно кілька секунд після ввімкнення, а швидкість він підбирає за першими AT
bool alive = false;
for (uint8_t i = 0; i < 20 && !alive; i++) alive = sendAT("AT", 1000);
if (!alive) {
Serial.println(F("Модем не відповідає"));
return false;
}
sendAT("ATE0"); // без луни команд: так простіше розбирати відповіді
// Швидкість 9600 замість автопідбору. В автопідборі модем, перезапустившись
// сам, мовчить (без RDY і Call Ready), доки не отримає AT, і скетч не
// дізнався б, що налаштування нижче скинуто. Записуємо в памʼять модема
// лише раз, коли там ще автопідбір.
baudFixed = false;
sendAT("AT+IPR?");
if (!baudFixed && sendAT("AT+IPR=9600")) sendAT("AT&W");
sendAT("AT+CLIP=1"); // номер вхідного дзвінка
sendAT("AT+DDET=1,0,0"); // розпізнавання тонів DTMF
sendAT("AT+COLP=1"); // ATD відповідає OK, лише коли абонент підняв слухавку
sendAT("AT+CREG?");
modemFails = 0;
return true;
}
// Раз на пів хвилини, поки немає дзвінків: модем живий, SIM-карта в мережі,
// налаштування на місці
void serviceModem() {
if (callState != CALL_IDLE || millis() - lastModemCheck < MODEM_CHECK_MS) return;
lastModemCheck = millis();
if (sendAT("AT+CREG?")) {
modemFails = 0;
if (!registered) Serial.println(F("Немає мережі"));
// якщо модем непомітно перезапустився, тут буде 0, і handleLine попросить налаштувати його знову
sendAT("AT+CLIP?");
} else if (++modemFails >= 3) {
wantModemInit = true;
}
}
// Рядок від модема: лише змінює стан і ставить прапорці, команд не надсилає
void handleLine(const char* s) {
if (strcmp(s, "RING") == 0) {
lastRingAt = millis();
if (callState == CALL_IDLE) {
callState = CALL_INCOMING;
callStartedAt = millis();
callerChecked = false;
}
return;
}
if (strncmp(s, "+CLIP:", 6) == 0) {
const char* p = s + 6;
while (*p == ' ') p++;
if (isdigit(*p)) { // відповідь на AT+CLIP? («+CLIP: 1,1»), а не номер того, хто дзвонить
if (*p != '1') {
Serial.println(F("Налаштування модема скинуто"));
wantModemInit = true;
}
return;
}
if (callState == CALL_INCOMING && !callerChecked) {
callerChecked = true;
char number[24];
extractQuoted(s, number, sizeof(number));
if (isAllowed(number)) {
wantAnswer = true;
} else {
Serial.print(F("Чужий номер: "));
Serial.println(number);
wantHangup = true;
}
}
return;
}
if (strncmp(s, "+DTMF:", 6) == 0) {
const char* p = s + 6;
while (*p == ' ') p++;
if (*p && callState == CALL_ACTIVE && keyCount < sizeof(keys)) keys[keyCount++] = *p;
return;
}
if (strncmp(s, "+CREG:", 6) == 0) {
const char* comma = strchr(s, ',');
int stat = comma ? atoi(comma + 1) : 0;
registered = stat == 1 || stat == 5; // 1 — домашня мережа, 5 — роумінг
return;
}
if (strncmp(s, "+IPR:", 5) == 0) {
baudFixed = atol(s + 5) == 9600; // 0 — автопідбір
return;
}
if (callState == CALL_DIALING) {
if (strncmp(s, "+COLP:", 6) == 0 || strcmp(s, "OK") == 0) { // абонент відповів
callState = CALL_ACTIVE;
callStartedAt = millis();
unlocked = true; // ми самі зателефонували власникові
// обдзвін не завершуємо: могла відповісти голосова пошта, тож чекаємо клавішу
wantAlarmMessage = true;
return;
}
if (strcmp(s, "ERROR") == 0 || strncmp(s, "+CME ERROR", 10) == 0) {
onCallEnded();
return;
}
}
if (strcmp(s, "NO CARRIER") == 0 || strcmp(s, "BUSY") == 0 ||
strcmp(s, "NO ANSWER") == 0 || strcmp(s, "NO DIALTONE") == 0) {
onCallEnded();
return;
}
// модем перезапустився чи вимкнувся (наприклад, просіло живлення): налаштування зникли.
// UNDER-VOLTAGE WARNNING (саме так, з помилкою) — лише попередження, модем працює
// далі, і дзвінок, під час якого воно зазвичай і надходить, обривати не можна.
if (strcmp(s, "RDY") == 0 || strcmp(s, "Call Ready") == 0 ||
strcmp(s, "UNDER-VOLTAGE POWER DOWN") == 0 || strcmp(s, "NORMAL POWER DOWN") == 0) {
wantModemInit = true;
}
}
void doQueuedActions() {
if (wantModemInit) {
wantModemInit = false;
if (callState != CALL_IDLE) onCallEnded();
initModem();
}
if (wantHangup) {
wantHangup = false;
sendAT("ATH");
onCallEnded();
}
if (wantAnswer) {
wantAnswer = false;
if (sendAT("ATA", 5000)) {
callState = CALL_ACTIVE;
callIsAlarm = false;
callStartedAt = millis();
unlocked = DTMF_PIN[0] == '\0';
pinLen = 0;
pinTries = 0;
} else {
onCallEnded();
}
}
if (wantAlarmMessage) {
wantAlarmMessage = false;
alarmMsgAt = millis();
playSound(SND_ALARM);
}
for (uint8_t i = 0; i < keyCount; i++) onKey(keys[i]);
keyCount = 0;
}
void playSound(uint8_t n) {
char cmd[48];
snprintf(cmd, sizeof(cmd), "AT+CREC=4,\"C:\\User\\%d.amr\",1,100", (int)n);
sendAT(cmd, 3000);
}
// ============================== ДЗВІНКИ ==============================
void onCallEnded() {
// тривожний дзвінок скінчився, а клавішу так і не натиснули: не відповіли,
// відповіла голосова пошта або поклали слухавку, не підтвердивши
bool alarmNotAnswered = callIsAlarm && alarmCalling;
callState = CALL_IDLE;
callIsAlarm = false;
unlocked = false;
pinLen = 0;
pinTries = 0;
keyCount = 0;
lastCallEndedAt = millis();
if (alarmNotAnswered) alarmAttempt++; // не додзвонилися — далі за списком
}
void serviceCall() {
unsigned long now = millis();
switch (callState) {
case CALL_INCOMING:
// той, хто дзвонив, скинув, не дочекавшись відповіді, або номер так і не надійшов
if (now - lastRingAt > 15000UL) onCallEnded();
else if (!callerChecked && now - callStartedAt > 10000UL) wantHangup = true;
break;
case CALL_DIALING:
if (now - callStartedAt > DIAL_TIMEOUT_MS) wantHangup = true;
break;
case CALL_ACTIVE:
if (now - callStartedAt > CALL_MAX_MS) {
wantHangup = true;
} else if (callIsAlarm && alarmCalling) {
// тривогу ще не підтвердили: повторюємо повідомлення, а якщо клавіші
// так і немає — кладемо слухавку й дзвонимо далі за списком
if (now - callStartedAt > ALARM_ACK_MS) wantHangup = true;
else if (now - alarmMsgAt > ALARM_REPEAT_MS) wantAlarmMessage = true;
}
break;
default:
break;
}
}
void dial(const char* number) {
char cmd[32];
snprintf(cmd, sizeof(cmd), "ATD%s;", number);
Serial.print(F(">> "));
Serial.println(cmd);
// відповіді не чекаємо: OK надійде, коли абонент підніме слухавку (AT+COLP=1)
SIM800.println(cmd);
callState = CALL_DIALING;
callIsAlarm = true;
callStartedAt = millis();
}
// Номер із білого списку: порівнюємо останні MATCH_DIGITS цифр
bool isAllowed(const char* number) {
for (uint8_t i = 0; i < sizeof(ALLOWED_PHONES) / sizeof(ALLOWED_PHONES[0]); i++) {
if (sameNumber(number, ALLOWED_PHONES[i])) return true;
}
return false;
}
bool sameNumber(const char* a, const char* b) {
char ta[MATCH_DIGITS + 1], tb[MATCH_DIGITS + 1];
return tailDigits(a, ta) && tailDigits(b, tb) && strcmp(ta, tb) == 0;
}
// Останні MATCH_DIGITS цифр номера; false, якщо цифр менше (прихований чи службовий номер)
bool tailDigits(const char* s, char* out) {
char digits[24];
uint8_t n = 0;
for (; *s && n < sizeof(digits) - 1; s++) {
if (isdigit(*s)) digits[n++] = *s;
}
if (n < MATCH_DIGITS) return false;
memcpy(out, digits + n - MATCH_DIGITS, MATCH_DIGITS);
out[MATCH_DIGITS] = '\0';
return true;
}
// Перше значення в лапках: +CLIP: "+380...",145,... -> +380...
void extractQuoted(const char* s, char* out, uint8_t size) {
out[0] = '\0';
const char* a = strchr(s, '"');
if (!a) return;
a++;
const char* b = strchr(a, '"');
if (!b) return;
uint8_t n = b - a;
if (n > size - 1) n = size - 1;
memcpy(out, a, n);
out[n] = '\0';
}
// ============================== КОМАНДИ ==============================
void onKey(char k) {
Serial.print(F("Клавіша: "));
Serial.println(k);
if (callIsAlarm && alarmCalling) {
// будь-яка клавіша в тривожному дзвінку: людина почула, обдзвін завершено
alarmCalling = false;
Serial.println(F("Тривогу підтверджено"));
}
if (!unlocked) {
enterPin(k);
return;
}
switch (k) {
case '#':
if (UNLOCK_DISARMS) setRelay(false); // охорона — лише з зачиненим замком
setArmed(true);
playSound(SND_ARMED);
break;
case '*': setArmed(false); playSound(SND_DISARMED); break;
case '4':
if (UNLOCK_DISARMS && armed) setArmed(false);
setRelay(true);
playSound(SND_RELAY_ON);
break;
case '2': setRelay(false); playSound(SND_RELAY_OFF); break;
default: break;
}
}
void enterPin(char k) {
const uint8_t need = sizeof(DTMF_PIN) - 1;
pinBuf[pinLen++] = k;
if (pinLen < need) return;
pinBuf[pinLen] = '\0';
pinLen = 0;
if (strcmp(pinBuf, DTMF_PIN) == 0) {
unlocked = true;
Serial.println(F("PIN правильний"));
playSound(SND_PIN_OK);
return;
}
playSound(SND_PIN_BAD);
if (++pinTries >= PIN_MAX_TRIES) {
Serial.println(F("PIN хибний, кладу слухавку"));
wantHangup = true;
} else {
Serial.println(F("PIN хибний"));
}
}
void setRelay(bool on) {
digitalWrite(PIN_RELAY, on ? RELAY_ON : RELAY_OFF);
}
void setArmed(bool on) {
armed = on;
EEPROM.update(EE_ARMED, on ? 1 : 0);
alarmCalling = false;
// ставлять на охорону з відчиненими дверима: тривога, коли їх зачинять і знову відчинять
alarmLatched = on && doorOpen;
Serial.println(on ? F("Під охороною") : F("Охорону знято"));
}
// ============================== ОХОРОНА ==============================
void serviceDoor() {
bool raw = digitalRead(PIN_DOOR) == DOOR_OPEN_LEVEL;
if (raw != doorRaw) {
doorRaw = raw;
doorChangedAt = millis();
}
if (raw != doorOpen && millis() - doorChangedAt >= DOOR_DEBOUNCE_MS) {
doorOpen = raw;
Serial.println(doorOpen ? F("Двері відчинено") : F("Двері зачинено"));
}
if (armed && doorOpen && !alarmLatched) triggerAlarm();
// обдзвін завершено й двері знову зачинено: наступне відчинення — нова тривога
if (alarmLatched && !alarmCalling && !doorOpen) alarmLatched = false;
}
void triggerAlarm() {
Serial.println(F("ТРИВОГА"));
alarmLatched = true;
alarmCalling = true;
alarmAttempt = 0;
lastCallEndedAt = millis() - DIAL_PAUSE_MS; // перший дзвінок — одразу
}
void serviceAlarm() {
if (!alarmCalling || callState != CALL_IDLE) return;
if (millis() - lastCallEndedAt < DIAL_PAUSE_MS) return;
const uint8_t count = sizeof(ALARM_PHONES) / sizeof(ALARM_PHONES[0]);
if (alarmAttempt >= count * ALARM_ROUNDS) {
Serial.println(F("Ніхто не відповів, обдзвін завершено"));
alarmCalling = false;
return;
}
dial(ALARM_PHONES[alarmAttempt % count]);
}
void updateLeds() {
digitalWrite(PIN_LED_DOOR, armed && !doorOpen ? HIGH : LOW);
digitalWrite(PIN_LED_ARMED, armed ? HIGH : LOW);
}
Перевірити геркон можна ще до першого дзвінка: відкрийте монітор порту на 9600, піднесіть магніт і заберіть. Має зʼявлятися «Двері зачинено» з магнітом і «Двері відчинено» без нього.
Лише замок, без сигналізації
Якщо охорона не потрібна, візьміть простіший скетч zamok.ino. Він лише відчиняє й зачиняє замок: білий список номерів, PIN-код, клавіші 4 і 2 і голосові підказки — так само, як у повному. Геркон, світлодіоди й деталі до них тоді не потрібні, решта схеми не змінюється. Саме з цим скетчем ми й перевіряли зібраний пристрій.
- Налаштування ті самі, крім охоронних:
ALLOWED_PHONES,DTMF_PIN,RELAY_ON/RELAY_OFF. - Підказки потрібні лише
5.amrі6.amr, а з PIN-кодом ще7.amrі8.amr. - Після зникнення живлення чи перезапуску реле вимкнено, тобто замок зачиняє двері.
Увесь код простого скетча
/*
GSM-реле для електрозамка з керуванням тонами DTMF
Arduino Nano + SIM800L
© 2026 BoxForma (boxforma.com). Ліцензія MIT, повний текст — у LICENSE.txt:
можна вільно копіювати, змінювати й поширювати, зберігши ці рядки.
Надається «як є», без жодних гарантій; автори не відповідають за наслідки
використання. Пристрій ви збираєте й підключаєте на власний ризик.
Простий варіант скетча syhnalizatsiia.ino: лише замок, без охорони й геркона.
Зателефонуйте на SIM-карту пристрою з номера з білого списку: він
відповість, і далі команди набирають на клавіатурі телефону.
4 — відчинити замок (реле ввімкнено)
2 — зачинити замок (реле вимкнено)
Голосові підказки — файли C:\User\N.amr у памʼяті SIM800L:
5 — двері відчинено, 6 — двері зачинено,
7 — код прийнято, 8 — хибний код (лише якщо задано DTMF_PIN).
Як записати й завантажити свої — див. статтю на boxforma.com/statti.
*/
#include <SoftwareSerial.h>
#include <avr/wdt.h>
// ============================ НАЛАШТУВАННЯ ============================
// Хто може зателефонувати й керувати
// Замініть на свої номери у форматі +380XXXXXXXXX
const char* const ALLOWED_PHONES[] = {
"+380XXXXXXXXX", "+380YYYYYYYYY",
};
// Номери порівнюються за останніми цифрами: +380501234567 і 0501234567 — один номер
const uint8_t MATCH_DIGITS = 9;
// PIN-код, який треба набрати після відповіді, перш ніж пристрій прийме
// команди. "" — без коду. Номер того, хто дзвонить, можна підробити, тож
// для замка вхідних дверей код краще задати (не довший за 7 цифр).
// Після правильного коду звучить 7.amr, після хибного — 8.amr.
const char DTMF_PIN[] = "";
const uint8_t PIN_MAX_TRIES = 3; // стільки хибних кодів — і слухавку покладено
// Клавіші команд
const char KEY_OPEN = '4';
const char KEY_CLOSE = '2';
const unsigned long CALL_MAX_MS = 180000UL; // будь-яку розмову кладемо через 3 хвилини
const unsigned long MODEM_CHECK_MS = 30000UL; // як часто перевіряти модем і мережу
// Модуль реле вмикається низьким рівнем: під час запуску на виводі HIGH,
// «4» дає LOW, «2» — HIGH. Якщо ваше реле насправді вмикається високим
// рівнем, поміняйте ці два рядки місцями. Електрозамок, відчинений без
// живлення (як SEVEN Z-727), підключають до контактів COM і NC: поки реле
// вимкнене, замок під напругою й двері зачинено.
#define RELAY_ON LOW
#define RELAY_OFF HIGH
// Сторожовий таймер перезапустить плату, якщо вона зависне. Вмикайте (1),
// лише якщо скетч завантажується з «Processor: ATmega328P», а не «(Old
// Bootloader)»: старий завантажувач клонів Nano з ним іде в нескінченне
// перезавантаження, і плату потім рятує лише програматор.
#define USE_WATCHDOG 0
// Номери звукових файлів C:\User\N.amr: у кожної дії своя фраза
enum Sound : uint8_t {
SND_OPENED = 5,
SND_CLOSED = 6,
SND_PIN_OK = 7,
SND_PIN_BAD = 8,
};
// ============================== ВИВОДИ ==============================
// RX, TX. Логіка модуля — 2,8 В, а Nano на 5 В гарантовано бачить «одиницю» лише від 3,0 В:
// TXD модема → D10 через кремнієвий діод 1N4148: анод — до D10, катод — до TXD.
// SoftwareSerial сам вмикає на D10 вбудований підтягувальний резистор до +5 В,
// а діод притискає D10 до напруги на TXD плюс своє падіння:
// «одиниця» — близько 3,3 В, «нуль» — близько 0,5 В.
// Діод Шотткі не годиться: у нього падіння замале, і до 3,0 В не дотягне;
// D11 → RXD через дільник: D11 — 10 кОм — RXD — 10 кОм — GND, це половина:
// 2,5–2,6 В із 5,0–5,2 В (модулю треба від 2,1 до 3,1 В)
SoftwareSerial SIM800(10, 11);
#define PIN_RELAY 12 // не D13: ним під час скидання блимає завантажувач, і реле клацало б
// ============================== СТАН ==============================
enum CallState : uint8_t { CALL_IDLE, CALL_INCOMING, CALL_ACTIVE };
CallState callState = CALL_IDLE;
unsigned long callStartedAt = 0;
unsigned long lastRingAt = 0;
bool callerChecked = false; // номер вхідного вже перевірено
bool unlocked = false; // PIN набрано (або він не потрібен)
char pinBuf[8];
uint8_t pinLen = 0;
uint8_t pinTries = 0;
// Розбір рядків модема лише ставить прапорці, а команди модему надсилає loop():
// так відповідь на одну команду не переплутається з іншою.
bool wantAnswer = false;
bool wantHangup = false;
bool wantModemInit = false;
char keys[8];
uint8_t keyCount = 0;
bool registered = false;
bool modemReady = false; // модем прийняв CLIP, DDET і DTAM
bool baudFixed = false; // у модемі збережено швидкість 9600, а не автопідбір
uint8_t modemFails = 0;
unsigned long lastModemCheck = 0;
char line[96];
uint8_t lineLen = 0;
static_assert(sizeof(DTMF_PIN) <= sizeof(pinBuf), "DTMF_PIN не довший за 7 цифр");
// ============================== ЗАПУСК ==============================
void setup() {
MCUSR = 0;
wdt_disable();
// під час запуску реле вимкнено, замок зачинено: рівень задаємо до того,
// як вивід стане виходом, інакше він на мить піде в LOW і реле клацне
digitalWrite(PIN_RELAY, RELAY_OFF);
pinMode(PIN_RELAY, OUTPUT);
Serial.begin(9600);
SIM800.begin(9600);
Serial.println(F("Старт"));
#if USE_WATCHDOG
wdt_enable(WDTO_8S);
#endif
initModem();
}
void loop() {
feedWatchdog();
while (readLine()) {
logLine(line);
handleLine(line);
}
doQueuedActions();
serviceCall();
serviceModem();
// команди модему з монітора порту, для налагодження
while (Serial.available()) SIM800.write(Serial.read());
}
// ============================== МОДЕМ ==============================
void feedWatchdog() {
#if USE_WATCHDOG
wdt_reset();
#endif
}
// Збирає рядок відповіді модема в line; true, коли надійшов цілий непорожній рядок
bool readLine() {
while (SIM800.available()) {
char c = SIM800.read();
if (c == '\r') continue;
if (c == '\n') {
if (lineLen == 0) continue;
line[lineLen] = '\0';
lineLen = 0;
return true;
}
if (lineLen < sizeof(line) - 1) line[lineLen++] = c;
}
return false;
}
void logLine(const char* s) {
Serial.print(F("<< "));
Serial.println(s);
}
// Надсилає команду й чекає OK чи ERROR; усе інше, що надійде, розбирається як завжди
bool sendAT(const char* cmd, unsigned long timeout = 2000) {
Serial.print(F(">> "));
Serial.println(cmd);
SIM800.println(cmd);
unsigned long start = millis();
while (millis() - start < timeout) {
feedWatchdog();
if (!readLine()) continue;
logLine(line);
if (strcmp(line, "OK") == 0) return true;
if (strcmp(line, "ERROR") == 0 || strncmp(line, "+CME ERROR", 10) == 0) return false;
handleLine(line);
}
Serial.println(F("Немає відповіді"));
return false;
}
bool initModem() {
// модему потрібно кілька секунд після ввімкнення, а швидкість він підбирає за першими AT
bool alive = false;
for (uint8_t i = 0; i < 20 && !alive; i++) alive = sendAT("AT", 1000);
if (!alive) {
Serial.println(F("Модем не відповідає"));
modemReady = false;
return false;
}
sendAT("ATE0"); // без луни команд: так простіше розбирати відповіді
// Швидкість 9600 замість автопідбору. В автопідборі модем, перезапустившись
// сам, мовчить (без RDY і Call Ready), доки не отримає AT, і скетч не
// дізнався б, що налаштування нижче скинуто. Записуємо в памʼять модема
// лише раз, коли там ще автопідбір.
baudFixed = false;
sendAT("AT+IPR?");
if (!baudFixed && sendAT("AT+IPR=9600")) sendAT("AT&W");
// Без цих трьох пристрій не впізнає номер, не почує клавіш чи підказки звучатимуть
// не тому. Поки SIM-карта не готова, модем відкидає частину з них — тоді serviceModem
// повторить налаштування, щойно модем зареєструється в мережі.
bool ok = sendAT("AT+CLIP=1"); // номер вхідного дзвінка
ok = sendAT("AT+DDET=1,0,0") && ok; // розпізнавання тонів DTMF
// підказки AT+CREC під час дзвінка — тому, хто дзвонить (1), а не в динамік модуля
// (0 — типово). Не зберігається, тож задаємо щоразу
ok = sendAT("AT+DTAM=1") && ok;
modemReady = ok;
sendAT("AT+CREG?");
modemFails = 0;
return true;
}
// Раз на пів хвилини, поки немає дзвінків: модем живий, SIM-карта в мережі,
// налаштування на місці
void serviceModem() {
if (callState != CALL_IDLE || millis() - lastModemCheck < MODEM_CHECK_MS) return;
lastModemCheck = millis();
if (sendAT("AT+CREG?")) {
modemFails = 0;
if (!registered) Serial.println(F("Немає мережі"));
if (registered && !modemReady) {
Serial.println(F("Модем відкинув налаштування, повторюю"));
wantModemInit = true;
return;
}
// якщо модем непомітно перезапустився, тут буде 0 (AT+DTAM не зберігається),
// і handleLine попросить налаштувати його знову. Не AT+CLIP?: та питає мережу,
// і модем щоразу виходив би в ефір — тріск у колонках і зайві імпульси струму
sendAT("AT+DTAM?");
} else if (++modemFails >= 3) {
wantModemInit = true;
}
}
// Рядок від модема: лише змінює стан і ставить прапорці, команд не надсилає
void handleLine(const char* s) {
if (strcmp(s, "RING") == 0) {
lastRingAt = millis();
if (callState == CALL_IDLE) {
callState = CALL_INCOMING;
callStartedAt = millis();
callerChecked = false;
}
return;
}
if (strncmp(s, "+DTAM:", 6) == 0) { // відповідь на AT+DTAM?
if (atoi(s + 6) != 1) {
Serial.println(F("Налаштування модема скинуто"));
wantModemInit = true;
}
return;
}
if (strncmp(s, "+CLIP:", 6) == 0) {
if (callState == CALL_INCOMING && !callerChecked) {
callerChecked = true;
char number[24];
extractQuoted(s, number, sizeof(number));
if (isAllowed(number)) {
wantAnswer = true;
} else {
Serial.print(F("Чужий номер: "));
Serial.println(number);
wantHangup = true;
}
}
return;
}
if (strncmp(s, "+DTMF:", 6) == 0) {
const char* p = s + 6;
while (*p == ' ') p++;
if (*p && callState == CALL_ACTIVE && keyCount < sizeof(keys)) keys[keyCount++] = *p;
return;
}
if (strncmp(s, "+CREG:", 6) == 0) {
const char* comma = strchr(s, ',');
int stat = comma ? atoi(comma + 1) : 0;
registered = stat == 1 || stat == 5; // 1 — домашня мережа, 5 — роумінг
return;
}
if (strncmp(s, "+IPR:", 5) == 0) {
baudFixed = atol(s + 5) == 9600; // 0 — автопідбір
return;
}
if (strcmp(s, "NO CARRIER") == 0 || strcmp(s, "BUSY") == 0 ||
strcmp(s, "NO ANSWER") == 0 || strcmp(s, "NO DIALTONE") == 0) {
onCallEnded();
return;
}
// модем перезапустився чи вимкнувся (наприклад, просіло живлення): налаштування зникли.
// UNDER-VOLTAGE WARNNING (саме так, з помилкою) — лише попередження, модем працює
// далі, і дзвінок, під час якого воно зазвичай і надходить, обривати не можна.
if (strcmp(s, "RDY") == 0 || strcmp(s, "Call Ready") == 0 ||
strcmp(s, "UNDER-VOLTAGE POWER DOWN") == 0 || strcmp(s, "NORMAL POWER DOWN") == 0) {
wantModemInit = true;
}
}
void doQueuedActions() {
if (wantModemInit) {
wantModemInit = false;
if (callState != CALL_IDLE) onCallEnded();
initModem();
}
if (wantHangup) {
wantHangup = false;
sendAT("ATH");
onCallEnded();
}
if (wantAnswer) {
wantAnswer = false;
if (sendAT("ATA", 5000)) {
callState = CALL_ACTIVE;
callStartedAt = millis();
unlocked = DTMF_PIN[0] == '\0';
pinLen = 0;
pinTries = 0;
} else {
onCallEnded();
}
}
for (uint8_t i = 0; i < keyCount; i++) onKey(keys[i]);
keyCount = 0;
}
// AT+CREC=4,<файл>,<канал>,<гучність>: канал 0 — основний аудіоканал (1 — додатковий,
// у SIM800L не виведений); кому звучить у дзвінку, задає AT+DTAM в initModem
void playSound(uint8_t n) {
char cmd[48];
snprintf(cmd, sizeof(cmd), "AT+CREC=4,\"C:\\User\\%d.amr\",0,100", (int)n);
if (sendAT(cmd, 3000)) return;
// попередня фраза ще звучить (клавіші натиснули швидко одну за одною),
// і модем нову не приймає: зупиняємо стару й пробуємо ще раз
sendAT("AT+CREC=5");
sendAT(cmd, 3000);
}
// ============================== ДЗВІНКИ ==============================
void onCallEnded() {
callState = CALL_IDLE;
unlocked = false;
pinLen = 0;
pinTries = 0;
keyCount = 0;
}
void serviceCall() {
unsigned long now = millis();
switch (callState) {
case CALL_INCOMING:
// той, хто дзвонив, скинув, не дочекавшись відповіді, або номер так і не надійшов
if (now - lastRingAt > 15000UL) onCallEnded();
else if (!callerChecked && now - callStartedAt > 10000UL) wantHangup = true;
break;
case CALL_ACTIVE:
if (now - callStartedAt > CALL_MAX_MS) wantHangup = true;
break;
default:
break;
}
}
// Номер із білого списку: порівнюємо останні MATCH_DIGITS цифр
bool isAllowed(const char* number) {
for (uint8_t i = 0; i < sizeof(ALLOWED_PHONES) / sizeof(ALLOWED_PHONES[0]); i++) {
if (sameNumber(number, ALLOWED_PHONES[i])) return true;
}
return false;
}
bool sameNumber(const char* a, const char* b) {
char ta[MATCH_DIGITS + 1], tb[MATCH_DIGITS + 1];
return tailDigits(a, ta) && tailDigits(b, tb) && strcmp(ta, tb) == 0;
}
// Останні MATCH_DIGITS цифр номера; false, якщо цифр менше (прихований чи службовий номер)
bool tailDigits(const char* s, char* out) {
char digits[24];
uint8_t n = 0;
for (; *s && n < sizeof(digits) - 1; s++) {
if (isdigit(*s)) digits[n++] = *s;
}
if (n < MATCH_DIGITS) return false;
memcpy(out, digits + n - MATCH_DIGITS, MATCH_DIGITS);
out[MATCH_DIGITS] = '\0';
return true;
}
// Перше значення в лапках: +CLIP: "+380...",145,... -> +380...
void extractQuoted(const char* s, char* out, uint8_t size) {
out[0] = '\0';
const char* a = strchr(s, '"');
if (!a) return;
a++;
const char* b = strchr(a, '"');
if (!b) return;
uint8_t n = b - a;
if (n > size - 1) n = size - 1;
memcpy(out, a, n);
out[n] = '\0';
}
// ============================== КОМАНДИ ==============================
void onKey(char k) {
Serial.print(F("Клавіша: "));
Serial.println(k);
if (!unlocked) {
enterPin(k);
return;
}
if (k == KEY_OPEN) {
setRelay(true);
playSound(SND_OPENED);
} else if (k == KEY_CLOSE) {
setRelay(false);
playSound(SND_CLOSED);
}
}
void enterPin(char k) {
const uint8_t need = sizeof(DTMF_PIN) - 1;
pinBuf[pinLen++] = k;
if (pinLen < need) return;
pinBuf[pinLen] = '\0';
pinLen = 0;
if (strcmp(pinBuf, DTMF_PIN) == 0) {
unlocked = true;
Serial.println(F("PIN правильний"));
playSound(SND_PIN_OK);
return;
}
playSound(SND_PIN_BAD);
if (++pinTries >= PIN_MAX_TRIES) {
Serial.println(F("PIN хибний, кладу слухавку"));
wantHangup = true;
} else {
Serial.println(F("PIN хибний"));
}
}
// Реле вмикається — NC розмикається, замок без живлення, двері відчинено
void setRelay(bool on) {
digitalWrite(PIN_RELAY, on ? RELAY_ON : RELAY_OFF);
Serial.println(on ? F("Замок відчинено") : F("Замок зачинено"));
}
Голосові підказки
Підказки — короткі файли .amr у памʼяті SIM800L, у теці C:\User\. Запишіть їх своїм голосом на диктофон телефона: одна фраза — один запис, 1–3 секунди, у тихій кімнаті.
| Файл | Коли звучить | Приклад фрази |
|---|---|---|
2.amr | після # | «Охорону ввімкнено» |
3.amr | після * | «Охорону вимкнено» |
4.amr | тривожний дзвінок; повторюється, доки не натиснуть клавішу | «Тривога! Відчинено двері. Натисніть будь-яку клавішу» |
5.amr | після 4 | «Двері відчинено» |
6.amr | після 2 | «Двері зачинено» |
7.amr | правильний PIN-код | «Код прийнято» |
8.amr | хибний PIN-код | «Хибний код» |
7.amr і 8.amr потрібні, лише якщо в скетчі задано DTMF_PIN.
- Перекиньте записи на компʼютер, перейменуйте на
2.m4a…8.m4aі покладіть поруч perekoduvaty.bat. Для нього потрібна безкоштовна програма ffmpeg:winget install Gyan.FFmpeg. Подвійне клацання — і поруч зʼявляться файли.amr: моно, 8 кГц, без тиші по краях і з вирівняною гучністю. - Завантажте в Nano скетч-міст sim800_mist.ino: він просто пересилає дані між компʼютером і модемом тими самими дротами.
- Установіть пакет
pip install pyserial, закрийте монітор порту в Arduino IDE й запустіть zavantazhyty_zvuky.py з номером свого порту:
python zavantazhyty_zvuky.py COM5 2.amr 3.amr 4.amr 5.amr 6.amr 7.amr 8.amr
Скрипт дочекається модема, для кожного файлу видалить старий, запише новий командами AT+FSCREATE і AT+FSWRITE і звірить розмір. Наприкінці поверніть у Nano основний скетч. Модем під час завантаження має живитися від акумулятора, як на схемі: самого USB йому замало.
Перевірка

- Дзвінок із номера з білого списку — модуль відповідає; з чужого — скидає.
#,*,4і2— спрацьовують, звучать підказки.- Під охороною набрати
4— ригель ховається, охорону знято, двері відчиняються без тривоги;2— за кілька секунд замок знову зачиняє двері. - Під охороною відчинити двері — надходить дзвінок, фраза «Тривога» повторюється, доки не натиснете клавішу; зірочкою охорону знімають.
- Під час тривожного дзвінка нічого не натискати — за 40 секунд пристрій покладе слухавку й зателефонує знову.
- Набрати
4, потім#— замок зачиняє двері, пристрій під охороною. - Вимкнути блок живлення — модем лишається в мережі; після ввімкнення пристрій знову під охороною, якщо був.
Ліцензія
Скетчі й скрипти поширюються за ліцензією MIT: їх можна вільно копіювати, змінювати й використовувати у своїх проєктах, зберігши рядки про ліцензію на початку файлу. Повний текст — у LICENSE.txt.
Схему, скетчі й поради з цієї статті надано «як є», без жодних гарантій. Ви збираєте й підключаєте пристрій на власний ризик, а автори не відповідають за шкоду чи збитки від його використання. Особливо уважно поставтеся до всього, що підключено до мережі 230 В.
Файли
- syhnalizatsiia.ino — скетч для Arduino Nano
- zamok.ino — простий скетч: лише замок, без сигналізації
- perekoduvaty.bat — перекодування записів із диктофона в AMR
- zavantazhyty_zvuky.py — завантаження звуків у SIM800L
- sim800_mist.ino — скетч-міст на час завантаження звуків
- LICENSE.txt — текст ліцензії MIT



