305 lines
10 KiB
Arduino
305 lines
10 KiB
Arduino
// 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.1"
|
||
#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 += " · v" + String(FW_VERSION) + "</div>";
|
||
|
||
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)
|
||
server.on("/open", HTTP_POST, []() {
|
||
triggerRelay();
|
||
server.send(200, "text/html", buildPage("✅ Команда выполнена"));
|
||
});
|
||
|
||
// Открытие от контроллера (GET + токен)
|
||
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);
|
||
|
||
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);
|
||
}
|