Привет всем любителям электроники и IoT-разработок! Сегодня я расскажу о своём пет-проекте "GSM Module Version 1" — компактном устройстве на базе ESP32S, которое объединяет GSM-связь, управление реле, обработку входных сигналов и удобный веб-интерфейс. Этот модуль можно использовать для автоматизации систем безопасности, удалённого управления и IoT-приложений. Я собрал его постепенно, сталкиваясь с различными вызовами, и в итоге получилась рабочая версия для тестирования и доработки. Давайте разберёмся по шагам: от идей до кода. 😊
Введение в проектМодуль "GSM Module Version 1" — это устройство, которое:
- Обрабатывает звонки и SMS: При поступлении звонка с известного номера включает реле на определённое время; команды SMS позволяют управлять реле удалённо.
- Хранит базу номеров: В EEPROM содержатся до 5000 телефонных номеров с адресами для авторизации звонков.
- Управляет нагрузками: 4 реле с задержками включения/выключения, привязанными к оптронным входам.
- Предоставляет веб-доступ: Точка доступа Wi-Fi с паролем, четыре страницы настроек (адаптированные для мобильных устройств).
- Мониторит состояния: Лог событий в реальном времени, индикация статуса через светодиоды и кнопку.
Проект основан на открытом ПО и Arduino IDE, с использованием библиотек TinyGSM, ESP32 WebServer и Wire. Общая стоимость компонентов — около 2000–3000 рублей, зависит от источников.
Необходимые компонентыПеред сборкой соберите материалы. Вот полный список:
- Микроконтроллер: ESP32S (модуль с ESP32-D0WDQ6).
- GSM-модуль: SIM800L в плате с антенной и разъёмом для SIM-карты.
- Память: EEPROM модуль (например, AT24C256, 256Кб) для хранения номеров.
- Реле: 4 релейных модуля (5V, с оптопарами) для управления нагрузками.
- Оптронные входы: 4 модуля оптронов (для гальванической развязки входных сигналов).
- Индикация: 2 светодиода (красный и зелёный), 2 резистора 220 Ом.
- Кнопка: Одна тактовая кнопка для переключения индикации.
- Питание: Стабилизатор 5V/2A для реле, батарея/адаптер для ESP32.
- Дополнительно: Макетная плата, провода, SIM-карта с GPRS.
Библиотеки для Arduino IDE:
- TinyGsm (для SIM800L).
- ESP32 Web Server (встроена в Arduino ESP32 ядро).
- Wire (для I2C).
Подключите компоненты к ESP32S по следующим пинам (GND и VCC общие, где 3.3V для ESP32, 5V для реле и SIM800L):
- SIM800L: RX (GPIO16), TX (GPIO17), Reset (GPIO19).
- EEPROM: SDA (GPIO21), SCL (GPIO22).
- Реле: GPIO2, GPIO4, GPIO5, GPIO13.
- Оптронные входы: GPIO14, GPIO25, GPIO26, GPIO27 (входы с внутренней подтяжкой).
- Светодиоды: GPIO18 (в противофазе: красный анод через 220Ω to GPIO, катод to GND; зелёный катод через 220Ω to GPIO, анод to VCC).
- Кнопка: GPIO32 to GND.
ESP32S
- GPIO16 (RX) --> SIM800L TX
- GPIO17 (TX) --> SIM800L RX
- GPIO19 (RST) --> SIM800L Reset/PWRKEY
- GPIO21 (SDA) --> EEPROM SDA
- GPIO22 (SCL) --> EEPROM SCL
- GPIO2 --> Реле 1
- GPIO4 --> Реле 2
- GPIO5 --> Реле 3
- GPIO13 --> Реле 4
- GPIO14 --> Оптрон 1 --> GND
- GPIO25 --> Оптрон 2 --> GND
- GPIO26 --> Оптрон 3 --> GND
- GPIO27 --> Оптрон 4 --> GND
- GPIO18 --> [220Ω] --> Анод красного LED --> GND
[220Ω] --> Катод зелёного LED --> VCC
- GPIO32 --> Кнопка --> GND
Для защиты используйте диоды на реле и оптроны для соединения с внешними сигналами.
Программная часть: Код Arduino IDE
Полный код проекта (разделён на части для удобства вставки в файл .ino). Установите библиотеки через менеджер Arduino IDE.
Часть 1: Включаемые файлы, структуры и глобальные переменные
#include <WiFi.h>
#include <WebServer.h>
#include <Wire.h>
#include <TinyGsmClient.h>
// Определения пинов (на основе нашего проекта)
#define SIM800_RESET_PIN 19
HardwareSerial SerialGSM(2); // UART2 для SIM800L
TinyGsm modem(SerialGSM);
// I2C EEPROM адреса
#define EEPROM_I2C_ADDR 0x50
#define MAX_NUMBERS 5000
#define NUMBERS_PER_PAGE 100
// Структура для номера телефона
struct PhoneEntry {
char phone[16]; // Номер телефона
char address[50]; // Адрес
};
// Выходы реле
#define RELAY1_PIN 2
#define RELAY2_PIN 4
#define RELAY3_PIN 5
#define RELAY4_PIN 13
// Входы оптронов
#define INPUT1_PIN 14
#define INPUT2_PIN 25
#define INPUT3_PIN 26
#define INPUT4_PIN 27
// Индикация состояния (противофазные LED на одном пине)
#define LED_STATE_PIN 18
bool isGreen = false;
// Кнопка для переключения режима
#define BUTTON_PIN 32
// WEB-сервер
WebServer server(80);
const char* wifi_ssid = "GSM";
const char* wifi_password = "98765432";
const char* web_password = "J9562876m"; // Пароль для веб-интерфейса
IPAddress apIP(192, 168, 10, 1);
IPAddress subnet(255, 255, 255, 0);
// Клиентские настройки Wi-Fi (замени на свои)
const char* client_ssid = "YourHomeWiFi"; // SSID домашней сети
const char* client_password = "YourHomePassword"; // Пароль
// Режимы
bool apMode = false; // true = AP, false = Client
unsigned long buttonPressStart = 0;
bool buttonPressed = false;
// Таймеры для мигания LED в AP режиме
unsigned long lastBlink = 0;
bool blinkState = false;
// Настройки
char gsm_apn[32] = "internet";
char gsm_user[16] = "";
char gsm_pass[16] = "";
int relay_delay_on[4] = {0, 0, 0, 0};
int relay_delay_off[4] = {5000, 0, 0, 0};
bool relay_trigger[4] = {false, false, false, false};
unsigned long relay_timer[4] = {0, 0, 0, 0};
int input_threshold[4] = {LOW, LOW, LOW, LOW};
int input_relay_bind[4] = {-1, -1, -1, -1};
String input_history = "";
int pin[4] = {RELAY1_PIN, RELAY2_PIN, RELAY3_PIN, RELAY4_PIN};
String cssMobile = "@media (max-width: 600px) { body { font-size: 14px; padding: 10px; } table { font-size: 12px; } input { width: 90%; } } body { font-family: Arial; text-align: center; } input { margin: 5px; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #ddd; padding: 8px; }";
// =================================================================================
// ФУНКЦИИ ДЛЯ EEPROM
// =================================================================================
void initEEPROM() {
Wire.begin(21, 22); // SDA GPIO21, SCL GPIO22
}
void savePhoneToEEPROM(int index, const char* phone, const char* address) {
if (index >= MAX_NUMBERS) return;
Wire.beginTransmission(EEPROM_I2C_ADDR);
Wire.write((index * sizeof(PhoneEntry)) >> 8);
Wire.write(index * sizeof(PhoneEntry));
Wire.write(phone, strlen(phone) + 1);
Wire.write(address, strlen(address) + 1);
Wire.endTransmission();
delay(5);
}
void loadPhoneFromEEPROM(int index, char* phone, char* address) {
if (index >= MAX_NUMBERS) return;
Wire.beginTransmission(EEPROM_I2C_ADDR);
Wire.write((index * sizeof(PhoneEntry)) >> 8);
Wire.write(index * sizeof(PhoneEntry));
Wire.endTransmission();
Wire.requestFrom(EEPROM_I2C_ADDR, sizeof(PhoneEntry));
Wire.readBytes(phone, 16);
Wire.readBytes(address, 50);
}
bool isPhoneInDatabase(const char* caller) {
PhoneEntry entry;
for (int i = 0; i < MAX_NUMBERS; i++) {
loadPhoneFromEEPROM(i, entry.phone, entry.address);
if (strcmp(entry.phone, caller) == 0) return true;
}
return false;
}
// Конец Част 1 — перейди к Час 2
// =================================================================================
// ФУНКЦИИ ДЛЯ GSM
// =================================================================================
void initGSM() {
SerialGSM.begin(9600, SERIAL_8N1, 16, 17); // RX GPIO16, TX GPIO17
pinMode(SIM800_RESET_PIN, OUTPUT);
digitalWrite(SIM800_RESET_PIN, HIGH);
modem.init();
modem.waitForNetwork();
if (!modem.gprsConnect(gsm_apn, gsm_user, gsm_pass)) {
Serial.println("GPRS failed");
}
}
void handleCall() {
if (modem.callReceive()) {
char caller[20] = {0};
modem.callNumber(caller);
modem.callHangup();
if (isPhoneInDatabase(caller)) {
relay_timer[0] = millis() + relay_delay_on[0];
digitalWrite(RELAY1_PIN, HIGH);
input_history += String(millis()) + ": Звонок от " + String(caller) + ", Реле1 вкл\n";
}
}
}
void handleSMS() {
int smsCount = modem.getSmsCount();
for (int i = 0; i < smsCount; i++) {
String sms = modem.getSmsText(i);
String sender = modem.getSmsSender(i);
if (sms.length() > 0) {
if (sms.indexOf("RELAY1 ON") >= 0) digitalWrite(RELAY1_PIN, HIGH);
else if (sms.indexOf("RELAY1 OFF") >= 0) digitalWrite(RELAY1_PIN, LOW);
modem.sendSms(sender, "Команда принята");
modem.deleteSms(i);
}
}
SerialGSM.print("AT+CMGD=1,4\r");
delay(500);
while (SerialGSM.available()) SerialGSM.read();
}
// Конец Част 2 — перейди к Час 3
// =================================================================================
// ФУНКЦИИ ДЛЯ РЕЛЕ И ВХОДОВ И МИГАНИЯ LED
// =================================================================================
void initRelaysAndInputs() {
pinMode(RELAY1_PIN, OUTPUT);
pinMode(RELAY2_PIN, OUTPUT);
pinMode(RELAY3_PIN, OUTPUT);
pinMode(RELAY4_PIN, OUTPUT);
pinMode(INPUT1_PIN, INPUT_PULLUP);
pinMode(INPUT2_PIN, INPUT_PULLUP);
pinMode(INPUT3_PIN, INPUT_PULLUP);
pinMode(INPUT4_PIN, INPUT_PULLUP);
pinMode(LED_STATE_PIN, OUTPUT);
digitalWrite(LED_STATE_PIN, HIGH); // Изначально красный
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void handleRelaysAndInputs() {
unsigned long now = millis();
bool inputStates[4] = {digitalRead(INPUT1_PIN) == input_threshold[0], digitalRead(INPUT2_PIN) ==
...digitalRead(INPUT2_PIN) == input_threshold[1], digitalRead(INPUT3_PIN) == input_threshold[2], digitalRead(INPUT4_PIN) == input_threshold[3]};
for (int i = 0; i < 4; i++) {
if (inputStates[i] && input_relay_bind[i] >= 0) {
relay_timer[input_relay_bind[i]] = now + relay_delay_on[input_relay_bind[i]];
digitalWrite(pin[i], HIGH);
input_history += String(now) + ": Вход " + String(i+1) + " -> Реле " + String(input_relay_bind[i]+1) + "\n";
}
}
for (int i = 0; i < 4; i++) {
if (relay_timer[i] > 0 && now >= relay_timer[i]) {
if (digitalRead(pin[i]) == HIGH) {
digitalWrite(pin[i], LOW);
relay_timer[i] = now + relay_delay_off[i];
} else {
relay_timer[i] = 0;
}
}
}
// Обработка кнопки для переключения режима (удержание > 3 сек)
bool buttonState = digitalRead(BUTTON_PIN);
if (buttonState == LOW && !buttonPressed) {
buttonPressStart = now;
buttonPressed = true;
}
if (buttonState == HIGH && buttonPressed) {
if (now - buttonPressStart > 3000) {
apMode = !apMode;
Serial.println(apMode ? "Перешел в режим AP" : "Перешел в режим Client");
setupWiFi();
}
buttonPressed = false;
}
// Мигание LED в AP режиме: попеременно красный (HIGH) / зелёный (LOW)
if (apMode && now - lastBlink > 500) {
lastBlink = now;
blinkState = !blinkState;
digitalWrite(LED_STATE_PIN, blinkState ? HIGH : LOW); // HIGH = красный, LOW = зелёный
} else if (!apMode) {
digitalWrite(LED_STATE_PIN, isGreen ? LOW : HIGH); // Возврат к нормальному состоянию
}
}
// Конец Част 3 — перейди к Час 4
// =================================================================================
// ФУНКЦИИ ДЛЯ WI-ФИ И WEB-ИНТЕРФЕЙСА
// =================================================================================
void setupWiFi() {
WiFi.disconnect();
if (apMode) {
WiFi.softAPConfig(apIP, apIP, subnet);
WiFi.softAP(wifi_ssid, wifi_password);
Serial.println("Режим AP активен");
} else {
WiFi.begin(client_ssid, client_password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
}
Serial.println("Подключен к клиентской сети, IP: " + WiFi.localIP().toString());
}
server.begin();
}
void setupWebServer() {
server.on("/", []() { if (!server.authenticate("admin", web_password)) return server.requestAuthentication(); handleRoot(); });
server.on("/numbers", []() { if (!server.authenticate("admin", web_password)) return server.requestAuthentication(); handleNumbers(); });
server.on("/relays", []() { if (!server.authenticate("admin", web_password)) return server.requestAuthentication(); handleRelays(); });
server.on("/inputs", []() { if (!server.authenticate("admin", web_password)) return server.requestAuthentication(); handleInputs(); });
server.on("/save", HTTP_POST, []() { server.send(200, "text/html", "<p>Настройки сохранены</p>"); });
server.on("/save_number", HTTP_POST, []() {
int id = server.arg("id").toInt();
String phone = server.arg("phone");
String address = server.arg("address");
savePhoneToEEPROM(id, phone.c_str(), address.c_str());
server.send(200, "text/html", "<p>Номер сохранён</p>");
});
server.on("/save_relay", HTTP_POST, []() {
int relay = server.arg("relay").toInt();
relay_delay_on[relay] = server.arg("delay_on").toInt();
relay_delay_off[relay] = server.arg("delay_off").toInt();
relay_trigger[relay] = server.arg("trigger") == "on";
server.send(200, "text/html", "<p>Реле обновлено</p>");
});
server.on("/save_input", HTTP_POST, []() {
int input = server.arg("input").toInt();
input_relay_bind[input] = server.arg("bind").toInt();
input_threshold[input] = server.arg("threshold").toInt();
server.send(200, "text/html", "<p>Вход обновлён</p>");
});
}
void handleRoot() {
String html = "<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Главная</title><style>" + cssMobile + "</style></head><body>";
html += "<h1>Главная страница GSM Module V1 (Режим: " + String(apMode ? "AP" : "Client") + ")</h1>";
html += "<form action=\"/save\" method=\"POST\"><p>APN: <input type=\"text\" name=\"apn\" value=\"" + String(gsm_apn) + "\"></p>";
html += "<p>Wi-Fi Пароль: <input type=\"text\" name=\"wifi_pass\" value=\"" + String("автоматически") + "\"></p>";
html += "<p><input type=\"submit\" value=\"Сохранить\"></p></form>";
html += "<h2>Мониторинг</h2><div><pre>" + input_history + "</pre></div>";
html += "</body></html>";
server.send(200, "text/html", html);
}
// Конец Част 4 — перейди к Час 5
void handleNumbers() {
int currentPage = server.arg("page").toInt();
if (currentPage < 0) currentPage = 0;
int totalPages = (MAX_NUMBERS + NUMBERS_PER_PAGE - 1) / NUMBERS_PER_PAGE;
if (currentPage >= totalPages) currentPage = totalPages - 1;
int startIndex = currentPage * NUMBERS_PER_PAGE;
int endIndex = min(startIndex + NUMBERS_PER_PAGE, MAX_NUMBERS);
String html = "<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>База номеров</title><style>" + cssMobile + "</style></head><body>";
html += "<h1>База номеров (страница " + String(currentPage + 1) + " из " + String(totalPages) + ")</h1>";
html += "<form action=\"/save_number\" method=\"POST\"><p>ID: <input type=\"number\" name=\"id\" min=\"0\" max=\"4999\" value=\"" + String(startIndex) + "\"></p>";
html += "<p>Номер: <input type=\"text\" name=\"phone\" placeholder=\"+71234567890\"></p>";
html += "<p>Адрес: <input type=\"text\" name=\"address\" placeholder=\"Описание\"></p>";
html += "<p><input type=\"submit\" value=\"Добавить/Обновить\"></p></form>";
html += "<table><tr><th>ID</th><th>Номер</th><th>Адрес</th></tr>";
for (int i = startIndex; i < endIndex; i++) {
PhoneEntry entry;
loadPhoneFromEEPROM(i, entry.phone, entry.address);
html += "<tr><td>" + String(i) + "</td><td>" + String(entry.phone) + "</td><td>" + String(entry.address) + "</td></tr>";
}
html += "</table>";
html += "<div>";
if (currentPage > 0) html += "<a href=\"/numbers?page=" + String(currentPage - 1) + "\">« Предыдущая</a> | ";
for (int p = max(0, currentPage - 2); p <= min(totalPages - 1, currentPage + 2); p++) {
if (p == currentPage) html += "<strong>" + String(p + 1) + "</strong> ";
else html += "<a href=\"/numbers?page=" + String(p) + "\">" + String(p + 1) + "</a> ";
}
if (currentPage < totalPages - 1) html += " | <a href=\"/numbers?page=" + String(currentPage + 1) + "\">Следующая »</a>";
html += "</div></body></html>";
server.send(200, "text/html", html);
}
void handleRelays() {
String html = "<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Настройки реле</title><style>" + cssMobile + "</style></head><body><h1>Настройки реле</h1>";
for (int i = 0; i < 4; i++) {
html += "<h2>Реле " + String(i+1) + "</h2><form action=\"/save_relay\" method=\"POST\">";
html += "<p><input type=\"hidden\" name=\"relay\" value=\"" + String(i) + "\"></p>";
html += "<p>Задержка вкл (мс): <input type=\"number\" name=\"delay_on\" value=\"" + String(relay_delay_on[i]) + "\"></p>";
html += "<p>Задержка выкл (мс): <input type=\"number\" name=\"delay_off\" value=\"" + String(relay_delay_off[i]) + "\"></p>";
html += "<p>Триггер: <input type=\"checkbox\" name=\"trigger\" " + (relay_trigger[i] ? "checked" : "") + "></p>";
html += "<p><input type=\"submit\" value=\"Сохранить\"></p></form>";
}
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleInputs() {
String html = "<html><head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Настройки входов</title><style>" + cssMobile + "</style></head><body><h1>Настройки входов</h1>";
html += "<p>Привязка к реле, пороги, история.</p>";
for (int i = 0; i < 4; i++) {
html += "<form action=\"/save_input\" method=\"POST\">";
html += "<p><input type=\"hidden\" name=\"input\" value=\"" + String(i) + "\"></p>";
html += "<p>Привязка к реле: <input type=\"number\" name=\"bind\" value=\"" + String(input_relay_bind[i]) + "\"></p>";
html += "<p>Порог: <select name=\"threshold\"><option value=\"0\">LOW</option><option value=\"1\">HIGH</option></select></p>";
html += "<p><input type=\"submit\" value=\"Сохранить\"></p></form>";
}
html += "<h2>История</h2><pre>" + input_history + "</pre>";
html += "</body></html>";
server.send(200, "text/html", html);
}
// =================================================================================
// SETUP И LOOP
// =================================================================================
void setup() {
Serial.begin(115200);
initEEPROM();
initGSM();
initRelaysAndInputs();
setupWiFi(); // Начальный режим Client (по умолчанию)
setupWebServer();
Serial.println("GSM Module V1 готов! Держи кнопку >3 сек для переключения режима.");
}
void loop() {
handleCall();
handleSMS();
handleRelaysAndInputs();
server.handleClient();
delay(100);
}
// Конец программы — все 5 частей вставлены