From 6192da8ec5b152ba2a4b13e010042f6926318bfa Mon Sep 17 00:00:00 2001 From: Aleksei Date: Mon, 15 Jun 2026 10:53:00 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20barrier=5Fcontroller=20=D0=B8=20barrier?= =?UTF-8?q?=5Fnode=20=D0=BF=D1=80=D0=BE=D1=88=D0=B8=D0=B2=D0=BA=D0=B8=20v1?= =?UTF-8?q?.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 29 +- barrier_controller/barrier_controller.ino | 330 ++++++++++++++++++++++ barrier_node/barrier_node.ino | 297 +++++++++++++++++++ 3 files changed, 654 insertions(+), 2 deletions(-) create mode 100644 barrier_controller/barrier_controller.ino create mode 100644 barrier_node/barrier_node.ino diff --git a/README.md b/README.md index 7d6d7e2..4069eb3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,28 @@ -# barrier-controller +# Barrier Controller — ESP32-ETH01 V1.4 -ESP32-ETH01 barrier controller firmware \ No newline at end of file +Прошивки для управления шлагбаумами DoorHan через ESP32-ETH01. + +## Устройства + +### barrier_controller — пульт управления +- Кнопка 1 → IO4 (шлагбаум 1) +- Кнопка 2 → IO2 (шлагбаум 2) +- Веб-морда с настройками и OTA +- ETH (основной) + WiFi (резервный) +- AP режим при первом запуске: `Barrier-Setup` / `barrier123` + +### barrier_node — узел на шлагбауме +- Реле → IO4 (импульс 500мс) +- HTTP API: `GET /open` с заголовком `X-Token` +- Веб-морда с настройками и OTA +- AP режим при первом запуске: `Barrier-Node-Setup` / `barrier123` + +## Сеть +- Контроллер: `192.168.15.20` (по умолчанию) +- Узел 1: `192.168.15.10` +- Узел 2: `192.168.15.11` + +## Прошивка +Arduino IDE, Board: ESP32 Dev Module, esp32 by Espressif 3.3.10+ + +При прошивке: IO0 на GND, отключить/подключить питание при `Connecting...` diff --git a/barrier_controller/barrier_controller.ino b/barrier_controller/barrier_controller.ino new file mode 100644 index 0000000..8a10f57 --- /dev/null +++ b/barrier_controller/barrier_controller.ino @@ -0,0 +1,330 @@ +// Controller firmware для ESP32-ETH01 V1.4 +// Кнопка 1 → IO4, Кнопка 2 → IO2 +// Веб-конфигуратор + статический IP + OTA + +#include +#include +#include +#include +#include +#include + +#define BTN1_PIN 4 +#define BTN2_PIN 2 + +#define ETH_POWER_PIN 16 +#define ETH_MDC_PIN 23 +#define ETH_MDIO_PIN 18 +#define ETH_CLK_MODE ETH_CLOCK_GPIO17_OUT + +#define AP_SSID "Barrier-Setup" +#define AP_PASS "barrier123" + +Preferences prefs; +WebServer server(80); + +bool ethConnected = false; +bool wifiConnected = false; +bool staticIpApplied = false; + +String cfg_ssid = ""; +String cfg_pass = ""; +String cfg_ip1 = "192.168.15.10"; +String cfg_ip2 = "192.168.15.11"; +String cfg_token = "barrier_token_2026"; +String cfg_self_ip = ""; // пусто = DHCP +String cfg_gateway = ""; +String cfg_subnet = "255.255.255.0"; + +unsigned long lastPress1 = 0; +unsigned long lastPress2 = 0; +const unsigned long DEBOUNCE = 1500; + +void WiFiEvent(WiFiEvent_t event) { + switch (event) { + case ARDUINO_EVENT_ETH_CONNECTED: + Serial.println("ETH: кабель подключён"); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + if (cfg_self_ip.length() > 0 && !staticIpApplied) { + staticIpApplied = true; + IPAddress ip, gw, sn, dns; + ip.fromString(cfg_self_ip); + gw.fromString(cfg_gateway.length() > 0 ? cfg_gateway : "192.168.1.1"); + sn.fromString(cfg_subnet); + dns.fromString(cfg_gateway.length() > 0 ? cfg_gateway : "8.8.8.8"); + ETH.config(ip, gw, sn, dns); + Serial.println("ETH статический IP: " + cfg_self_ip); + } else if (cfg_self_ip.length() == 0) { + Serial.print("ETH IP (DHCP): "); + Serial.println(ETH.localIP()); + } + ethConnected = true; + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + Serial.println("ETH: отключён"); + ethConnected = false; + break; + default: break; + } +} + +void loadConfig() { + prefs.begin("cfg", true); + cfg_ssid = prefs.getString("ssid", ""); + cfg_pass = prefs.getString("pass", ""); + cfg_ip1 = prefs.getString("ip1", "192.168.15.10"); + cfg_ip2 = prefs.getString("ip2", "192.168.15.11"); + cfg_token = prefs.getString("token", "barrier_token_2026"); + cfg_self_ip = prefs.getString("self_ip", ""); + cfg_gateway = prefs.getString("gateway", ""); + cfg_subnet = prefs.getString("subnet", "255.255.255.0"); + prefs.end(); +} + +void saveConfig() { + prefs.begin("cfg", false); + prefs.putString("ssid", cfg_ssid); + prefs.putString("pass", cfg_pass); + prefs.putString("ip1", cfg_ip1); + prefs.putString("ip2", cfg_ip2); + prefs.putString("token", cfg_token); + prefs.putString("self_ip", cfg_self_ip); + prefs.putString("gateway", cfg_gateway); + prefs.putString("subnet", cfg_subnet); + prefs.end(); +} + +void sendCommand(String ip) { + if (!ethConnected && !wifiConnected) { + Serial.println("Нет сети!"); + return; + } + HTTPClient http; + http.begin("http://" + ip + "/open"); + http.addHeader("X-Token", cfg_token); + int code = http.GET(); + Serial.println("Ответ от " + ip + ": " + String(code)); + http.end(); +} + +String currentIP() { + if (ethConnected) return ETH.localIP().toString(); + if (wifiConnected) return WiFi.localIP().toString(); + return "192.168.4.1 (AP)"; +} + +String buildPage(String msg = "") { + String html = R"( + + +Barrier Controller + +

Barrier Controller

+
IP: )"; + html += currentIP(); + html += "
"; + + if (msg.startsWith("❌")) + html += "
" + msg + "
"; + else if (msg.length() > 0) + html += "
" + msg + "
"; + + // Управление + html += R"(
+

Управление

+
+
+ + +
+
+ + +
+
+
)"; + + // Настройки шлагбаумов + html += R"(
+
+

Шлагбаумы

+ + + + + + + +
+

Сеть WiFi

+ + + + + +
+

IP этого устройства

+ + + + + + + + +
+
)"; + + // OTA + html += R"(
+

Обновление прошивки

+
+ + +
+
+)"; + + return html; +} + +void setupRoutes() { + server.on("/", HTTP_GET, []() { + server.send(200, "text/html", buildPage()); + }); + + server.on("/cmd", HTTP_GET, []() { + String b = server.arg("b"); + if (b == "1") { sendCommand(cfg_ip1); server.send(200, "text/html", buildPage("✅ Команда → шлагбаум 1")); } + else if (b == "2") { sendCommand(cfg_ip2); server.send(200, "text/html", buildPage("✅ Команда → шлагбаум 2")); } + else server.send(400, "text/plain", "bad request"); + }); + + server.on("/save", HTTP_POST, []() { + cfg_ip1 = server.arg("ip1"); + cfg_ip2 = server.arg("ip2"); + cfg_token = server.arg("token"); + cfg_ssid = server.arg("ssid"); + cfg_self_ip = server.arg("self_ip"); + cfg_gateway = server.arg("gateway"); + cfg_subnet = server.arg("subnet"); + String np = server.arg("pass"); + if (np.length() > 0) cfg_pass = np; + saveConfig(); + server.send(200, "text/html", buildPage("✅ Сохранено. Перезагрузка...")); + delay(1500); + ESP.restart(); + }); + + server.on("/update", HTTP_POST, + []() { + server.send(200, "text/html", + Update.hasError() + ? buildPage("❌ Ошибка обновления") + : buildPage("✅ Обновление OK! Перезагрузка...")); + delay(1000); + ESP.restart(); + }, + []() { + HTTPUpload& upload = server.upload(); + if (upload.status == UPLOAD_FILE_START) { + Serial.printf("OTA: %s\n", upload.filename.c_str()); + if (!Update.begin(UPDATE_SIZE_UNKNOWN)) Update.printError(Serial); + } else if (upload.status == UPLOAD_FILE_WRITE) { + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) Update.printError(Serial); + } else if (upload.status == UPLOAD_FILE_END) { + if (Update.end(true)) Serial.printf("OTA OK: %u байт\n", upload.totalSize); + else Update.printError(Serial); + } + } + ); + + // HTTP API (для вызова из других систем) + server.on("/open", HTTP_GET, []() { + if (server.header("X-Token") != cfg_token) { + server.send(401, "text/plain", "unauthorized"); + return; + } + String b = server.arg("b"); + if (b == "2") sendCommand(cfg_ip2); + else sendCommand(cfg_ip1); + server.send(200, "text/plain", "ok"); + }); +} + +void setup() { + Serial.begin(115200); + pinMode(BTN1_PIN, INPUT_PULLUP); + pinMode(BTN2_PIN, INPUT_PULLUP); + + loadConfig(); + + WiFi.onEvent(WiFiEvent); + ETH.begin(ETH_PHY_LAN8720, 1, ETH_MDC_PIN, ETH_MDIO_PIN, ETH_POWER_PIN, ETH_CLK_MODE); + + Serial.println("Жду ETH 5 сек..."); + delay(5000); + + if (!ethConnected && cfg_ssid.length() > 0) { + Serial.println("ETH нет — пробую WiFi: " + cfg_ssid); + WiFi.begin(cfg_ssid.c_str(), cfg_pass.c_str()); + int tries = 0; + while (WiFi.status() != WL_CONNECTED && tries < 20) { + delay(500); tries++; + } + if (WiFi.status() == WL_CONNECTED) { + wifiConnected = true; + Serial.println("WiFi IP: " + WiFi.localIP().toString()); + } + } + + if (!ethConnected && !wifiConnected) { + Serial.println("Нет сети — AP: " + String(AP_SSID)); + WiFi.softAP(AP_SSID, AP_PASS); + Serial.println("AP IP: " + WiFi.softAPIP().toString()); + } + + const char* headers[] = {"X-Token"}; + server.collectHeaders(headers, 1); + setupRoutes(); + server.begin(); + Serial.println("Веб-сервер запущен на " + currentIP()); +} + +void loop() { + server.handleClient(); + + unsigned long now = millis(); + + if (digitalRead(BTN1_PIN) == LOW && now - lastPress1 > DEBOUNCE) { + lastPress1 = now; + Serial.println("Кнопка 1"); + sendCommand(cfg_ip1); + } + if (digitalRead(BTN2_PIN) == LOW && now - lastPress2 > DEBOUNCE) { + lastPress2 = now; + Serial.println("Кнопка 2"); + sendCommand(cfg_ip2); + } + + delay(10); +} diff --git a/barrier_node/barrier_node.ino b/barrier_node/barrier_node.ino new file mode 100644 index 0000000..0a3e4c0 --- /dev/null +++ b/barrier_node/barrier_node.ino @@ -0,0 +1,297 @@ +// Barrier Node firmware для ESP32-ETH01 V1.4 +// Управление реле → IO4 +// HTTP API: GET /open (заголовок X-Token) +// Веб-морда + OTA обновление + +#include +#include +#include +#include +#include + +#define RELAY_PIN 4 + +#define ETH_POWER_PIN 16 +#define ETH_MDC_PIN 23 +#define ETH_MDIO_PIN 18 +#define ETH_CLK_MODE ETH_CLOCK_GPIO17_OUT + +#define AP_SSID "Barrier-Node-Setup" +#define AP_PASS "barrier123" + +Preferences prefs; +WebServer server(80); + +bool ethConnected = false; +bool wifiConnected = false; +bool staticIpApplied = false; + +String cfg_ssid = ""; +String cfg_pass = ""; +String cfg_token = "barrier_token_2026"; +String cfg_self_ip = ""; +String cfg_gateway = ""; +String cfg_subnet = "255.255.255.0"; +String cfg_name = "Шлагбаум 1"; // имя для идентификации + +void WiFiEvent(WiFiEvent_t event) { + switch (event) { + case ARDUINO_EVENT_ETH_CONNECTED: + Serial.println("ETH: кабель подключён"); + break; + case ARDUINO_EVENT_ETH_GOT_IP: + if (cfg_self_ip.length() > 0 && !staticIpApplied) { + staticIpApplied = true; + IPAddress ip, gw, sn, dns; + ip.fromString(cfg_self_ip); + gw.fromString(cfg_gateway.length() > 0 ? cfg_gateway : "192.168.1.1"); + sn.fromString(cfg_subnet); + dns.fromString(cfg_gateway.length() > 0 ? cfg_gateway : "8.8.8.8"); + ETH.config(ip, gw, sn, dns); + Serial.println("ETH статический IP: " + cfg_self_ip); + } else if (cfg_self_ip.length() == 0) { + Serial.print("ETH IP (DHCP): "); + Serial.println(ETH.localIP()); + } + ethConnected = true; + break; + case ARDUINO_EVENT_ETH_DISCONNECTED: + Serial.println("ETH: отключён"); + ethConnected = false; + break; + default: break; + } +} + +void loadConfig() { + prefs.begin("cfg", true); + cfg_ssid = prefs.getString("ssid", ""); + cfg_pass = prefs.getString("pass", ""); + cfg_token = prefs.getString("token", "barrier_token_2026"); + cfg_self_ip = prefs.getString("self_ip", ""); + cfg_gateway = prefs.getString("gateway", ""); + cfg_subnet = prefs.getString("subnet", "255.255.255.0"); + cfg_name = prefs.getString("name", "Шлагбаум 1"); + prefs.end(); +} + +void saveConfig() { + prefs.begin("cfg", false); + prefs.putString("ssid", cfg_ssid); + prefs.putString("pass", cfg_pass); + prefs.putString("token", cfg_token); + prefs.putString("self_ip", cfg_self_ip); + prefs.putString("gateway", cfg_gateway); + prefs.putString("subnet", cfg_subnet); + prefs.putString("name", cfg_name); + prefs.end(); +} + +void triggerRelay() { + Serial.println("Реле: импульс 500мс"); + digitalWrite(RELAY_PIN, HIGH); + delay(500); + digitalWrite(RELAY_PIN, LOW); +} + +String currentIP() { + if (ethConnected) return ETH.localIP().toString(); + if (wifiConnected) return WiFi.localIP().toString(); + return "192.168.4.1 (AP)"; +} + +String buildPage(String msg = "") { + String html = R"( + + +Barrier Node + +

)"; + html += cfg_name; + html += R"(

+
IP: )"; + html += currentIP(); + html += "
"; + + if (msg.startsWith("❌")) + html += "
" + msg + "
"; + else if (msg.length() > 0) + html += "
" + msg + "
"; + + // Управление + html += R"(
+

Управление

+
+ +
+
)"; + + // Настройки + html += R"(
+
+

Устройство

+ + + + + +
+

Сеть WiFi

+ + + + + +
+

IP этого устройства

+ + + + + + + + +
+
)"; + + // OTA + html += R"(
+

Обновление прошивки

+
+ + +
+
+)"; + + return html; +} + +void setupRoutes() { + server.on("/", HTTP_GET, []() { + server.send(200, "text/html", buildPage()); + }); + + // Открытие через браузер (без токена) + server.on("/open", HTTP_GET, []() { + String token = server.header("X-Token"); + // Если запрос из браузера (нет токена) — разрешаем + // Если запрос от контроллера — проверяем токен + if (token.length() > 0 && token != cfg_token) { + server.send(401, "text/plain", "unauthorized"); + return; + } + triggerRelay(); + if (token.length() > 0) { + server.send(200, "text/plain", "ok"); + } else { + server.send(200, "text/html", buildPage("✅ Команда выполнена")); + } + }); + + server.on("/save", HTTP_POST, []() { + cfg_name = server.arg("name"); + cfg_token = server.arg("token"); + cfg_ssid = server.arg("ssid"); + cfg_self_ip = server.arg("self_ip"); + cfg_gateway = server.arg("gateway"); + cfg_subnet = server.arg("subnet"); + String np = server.arg("pass"); + if (np.length() > 0) cfg_pass = np; + saveConfig(); + server.send(200, "text/html", buildPage("✅ Сохранено. Перезагрузка...")); + delay(1500); + ESP.restart(); + }); + + server.on("/update", HTTP_POST, + []() { + server.send(200, "text/html", + Update.hasError() + ? buildPage("❌ Ошибка обновления") + : buildPage("✅ Обновление OK! Перезагрузка...")); + delay(1000); + ESP.restart(); + }, + []() { + HTTPUpload& upload = server.upload(); + if (upload.status == UPLOAD_FILE_START) { + Serial.printf("OTA: %s\n", upload.filename.c_str()); + if (!Update.begin(UPDATE_SIZE_UNKNOWN)) Update.printError(Serial); + } else if (upload.status == UPLOAD_FILE_WRITE) { + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) Update.printError(Serial); + } else if (upload.status == UPLOAD_FILE_END) { + if (Update.end(true)) Serial.printf("OTA OK: %u байт\n", upload.totalSize); + else Update.printError(Serial); + } + } + ); + + // Статус + server.on("/status", HTTP_GET, []() { + String json = "{\"name\":\"" + cfg_name + "\",\"ip\":\"" + currentIP() + "\",\"eth\":" + (ethConnected ? "true" : "false") + "}"; + server.send(200, "application/json", json); + }); +} + +void setup() { + Serial.begin(115200); + + pinMode(RELAY_PIN, OUTPUT); + digitalWrite(RELAY_PIN, LOW); // реле выключено при старте + + loadConfig(); + + WiFi.onEvent(WiFiEvent); + ETH.begin(ETH_PHY_LAN8720, 1, ETH_MDC_PIN, ETH_MDIO_PIN, ETH_POWER_PIN, ETH_CLK_MODE); + + Serial.println("Жду ETH 5 сек..."); + delay(5000); + + if (!ethConnected && cfg_ssid.length() > 0) { + Serial.println("ETH нет — пробую WiFi: " + cfg_ssid); + WiFi.begin(cfg_ssid.c_str(), cfg_pass.c_str()); + int tries = 0; + while (WiFi.status() != WL_CONNECTED && tries < 20) { + delay(500); tries++; + } + if (WiFi.status() == WL_CONNECTED) { + wifiConnected = true; + Serial.println("WiFi IP: " + WiFi.localIP().toString()); + } + } + + if (!ethConnected && !wifiConnected) { + Serial.println("Нет сети — AP: " + String(AP_SSID)); + WiFi.softAP(AP_SSID, AP_PASS); + Serial.println("AP IP: " + WiFi.softAPIP().toString()); + } + + const char* headers[] = {"X-Token"}; + server.collectHeaders(headers, 1); + setupRoutes(); + server.begin(); + Serial.println("Веб-сервер запущен на " + currentIP()); +} + +void loop() { + server.handleClient(); + delay(10); +}