Files
barrier-controller/barrier_node/barrier_node.ino
T

303 lines
10 KiB
Arduino
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Barrier Node firmware для ESP32-ETH01 V1.4
// Управление реле → IO4
// HTTP API: GET /open (заголовок X-Token)
// Веб-морда + OTA обновление
#include <WiFi.h>
#include <ETH.h>
#include <Preferences.h>
#include <WebServer.h>
#include <Update.h>
#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 FW_VERSION "1.2"
#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, LOW); // LOW = включить (NC размыкается)
delay(500);
digitalWrite(RELAY_PIN, HIGH); // HIGH = выключить
}
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"(<!DOCTYPE html><html><head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Barrier Node</title>
<style>
body{font-family:sans-serif;max-width:500px;margin:20px auto;padding:0 16px;background:#f0f2f5}
h2{color:#222;margin-bottom:2px}
.sub{font-size:12px;color:#888;margin-bottom:16px}
.card{background:#fff;border-radius:12px;padding:16px;margin-bottom:14px;box-shadow:0 1px 3px rgba(0,0,0,.08)}
h3{margin:0 0 12px;font-size:14px;color:#555;text-transform:uppercase;letter-spacing:.5px}
label{display:block;margin-top:10px;font-size:13px;color:#555}
input[type=text],input[type=password]{width:100%;box-sizing:border-box;padding:9px;border:1px solid #ddd;border-radius:7px;font-size:14px;margin-top:3px}
.hint{font-size:11px;color:#aaa;margin-top:3px}
.btn{display:inline-block;padding:10px 0;border:none;border-radius:8px;cursor:pointer;font-size:14px;font-weight:500}
.btn-primary{background:#2196F3;color:#fff;width:100%;margin-top:12px}
.btn-open{background:#4CAF50;color:#fff;width:100%}
.btn-ota{background:#ff5722;color:#fff;width:100%;margin-top:8px}
.msg{background:#e8f5e9;color:#2e7d32;padding:10px 14px;border-radius:8px;margin-bottom:12px;font-size:14px}
.err{background:#ffebee;color:#c62828;padding:10px 14px;border-radius:8px;margin-bottom:12px;font-size:14px}
.sep{border:none;border-top:1px solid #f0f0f0;margin:14px 0}
</style></head><body>
<h2>)";
html += cfg_name;
html += R"(</h2>
<div class='sub'>IP: )";
html += currentIP();
html += " &nbsp;·&nbsp; v" + String(FW_VERSION) + "</div>";
String ok = server.hasArg("ok") ? server.arg("ok") : "";
if (ok == "1") msg = "✅ Команда выполнена";
if (msg.startsWith(""))
html += "<div class='err'>" + msg + "</div>";
else if (msg.length() > 0)
html += "<div class='msg'>" + msg + "</div>";
// Управление
html += R"(<div class='card'>
<h3>Управление</h3>
<form action='/open' method='post'>
<button class='btn btn-open'> Открыть шлагбаум</button>
</form>
</div>)";
// Настройки
html += R"(<div class='card'>
<form action='/save' method='post'>
<h3>Устройство</h3>
<label>Название</label>
<input type='text' name='name' value=')" + cfg_name + R"('>
<label>API токен</label>
<input type='text' name='token' value=')" + cfg_token + R"('>
<hr class='sep'>
<h3>Сеть WiFi</h3>
<label>SSID</label>
<input type='text' name='ssid' value=')" + cfg_ssid + R"('>
<label>Пароль</label>
<input type='password' name='pass' placeholder='оставь пустым не менять'>
<hr class='sep'>
<h3>IP этого устройства</h3>
<label>Статический IP <span class='hint'>(пусто = DHCP)</span></label>
<input type='text' name='self_ip' value=')" + cfg_self_ip + R"(' placeholder='например 192.168.15.10'>
<label>Шлюз (gateway)</label>
<input type='text' name='gateway' value=')" + cfg_gateway + R"(' placeholder='например 192.168.15.1'>
<label>Маска подсети</label>
<input type='text' name='subnet' value=')" + cfg_subnet + R"('>
<button class='btn btn-primary' type='submit'>💾 Сохранить и перезагрузить</button>
</form>
</div>)";
// OTA
html += R"(<div class='card'>
<h3>Обновление прошивки</h3>
<form action='/update' method='post' enctype='multipart/form-data'>
<input type='file' name='firmware' accept='.bin' style='margin-bottom:8px;font-size:13px'>
<button class='btn btn-ota' type='submit'> Загрузить .bin файл</button>
</form>
</div>
</body></html>)";
return html;
}
void setupRoutes() {
server.on("/", HTTP_GET, []() {
server.send(200, "text/html", buildPage());
});
// Открытие через браузер (POST + redirect — обновление страницы не повторяет команду)
server.on("/open", HTTP_POST, []() {
triggerRelay();
server.sendHeader("Location", "/?ok=1");
server.send(302);
});
// Открытие от контроллера (GET + токен)
server.on("/open", HTTP_GET, []() {
String token = server.header("X-Token");
if (token != cfg_token) {
server.send(401, "text/plain", "unauthorized");
return;
}
triggerRelay();
server.send(200, "text/plain", "ok");
});
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);
digitalWrite(RELAY_PIN, HIGH); // HIGH = реле выключено (инвертированная логика)
pinMode(RELAY_PIN, OUTPUT);
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);
}