Files
barrier-controller/barrier_controller/barrier_controller.ino
T

396 lines
14 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.
// Controller firmware для ESP32-ETH01 V1.4
// Кнопка 1 → IO4, Кнопка 2 → IO2
// Веб-конфигуратор + статический IP + OTA
#include <WiFi.h>
#include <HTTPClient.h>
#include <ETH.h>
#include <Preferences.h>
#include <WebServer.h>
#include <Update.h>
#define BTN1_PIN 4
#define BTN2_PIN 2
#define LED_PIN 12
#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 FW_VERSION "1.9"
#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";
String cfg_peers[5] = {"", "", "", "", ""};
unsigned long lastPress1 = 0;
unsigned long lastPress2 = 0;
const unsigned long DEBOUNCE = 3500;
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");
for (int i = 0; i < 5; i++) {
cfg_peers[i] = prefs.getString(("peer" + String(i)).c_str(), "");
}
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);
for (int i = 0; i < 5; i++) {
prefs.putString(("peer" + String(i)).c_str(), cfg_peers[i]);
}
prefs.end();
}
void notifyPeer(int btn) {
for (int i = 0; i < 5; i++) {
if (cfg_peers[i].length() == 0) continue;
HTTPClient http;
http.begin("http://" + cfg_peers[i] + "/peer_led?b=" + String(btn));
http.addHeader("X-Token", cfg_token);
http.GET();
http.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"(<!DOCTYPE html><html><head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>Barrier Controller</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}
input[type=text]:focus,input[type=password]:focus{outline:none;border-color:#2196F3}
.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:49%}
.btn-ota{background:#ff5722;color:#fff;width:100%;margin-top:8px}
.row{display:flex;gap:2%}
.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>Barrier Controller</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 = "✅ Команда → шлагбаум 1";
else if (ok == "2") msg = "✅ Команда → шлагбаум 2";
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>
<div class='row'>
<form action='/cmd' method='post' style='width:49%'>
<input type='hidden' name='b' value='1'>
<button class='btn btn-open' style='width:100%'> Шлагбаум 1</button>
</form>
<form action='/cmd' method='post' style='width:49%'>
<input type='hidden' name='b' value='2'>
<button class='btn btn-open' style='width:100%'> Шлагбаум 2</button>
</form>
</div>
</div>)";
// Настройки шлагбаумов
html += R"(<div class='card'>
<form action='/save' method='post'>
<h3>Шлагбаумы</h3>
<label>IP шлагбаума 1</label>
<input type='text' name='ip1' value=')" + cfg_ip1 + R"('>
<label>IP шлагбаума 2</label>
<input type='text' name='ip2' value=')" + cfg_ip2 + 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>Другие пульты (синхронизация LED)</h3>
<label>Пульт 1 <span class='hint'>(пусто = не использовать)</span></label>
<input type='text' name='peer0' value=')" + cfg_peers[0] + R"(' placeholder='192.168.15.x'>
<label>Пульт 2 <span class='hint'>(пусто = не использовать)</span></label>
<input type='text' name='peer1' value=')" + cfg_peers[1] + R"(' placeholder='192.168.15.x'>
<label>Пульт 3 <span class='hint'>(пусто = не использовать)</span></label>
<input type='text' name='peer2' value=')" + cfg_peers[2] + R"(' placeholder='192.168.15.x'>
<label>Пульт 4 <span class='hint'>(пусто = не использовать)</span></label>
<input type='text' name='peer3' value=')" + cfg_peers[3] + R"(' placeholder='192.168.15.x'>
<label>Пульт 5 <span class='hint'>(пусто = не использовать)</span></label>
<input type='text' name='peer4' value=')" + cfg_peers[4] + R"(' placeholder='192.168.15.x'>
<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.1.50'>
<label>Шлюз (gateway)</label>
<input type='text' name='gateway' value=')" + cfg_gateway + R"(' placeholder='например 192.168.1.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("/peer_led", HTTP_GET, []() {
String token = server.header("X-Token");
if (token != cfg_token) { server.send(401, "text/plain", "unauthorized"); return; }
digitalWrite(LED_PIN, HIGH);
lastPress1 = millis(); // блокируем debounce
lastPress2 = millis();
server.send(200, "text/plain", "ok");
});
server.on("/", HTTP_GET, []() {
server.send(200, "text/html", buildPage());
});
server.on("/cmd", HTTP_POST, []() {
String b = server.arg("b");
if (b == "1") { sendCommand(cfg_ip1); }
else if (b == "2") { sendCommand(cfg_ip2); }
else { server.send(400, "text/plain", "bad request"); return; }
// Redirect на главную — при обновлении страницы команда не повторится
server.sendHeader("Location", "/?ok=" + b);
server.send(302);
});
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");
for (int i = 0; i < 5; i++) {
cfg_peers[i] = server.arg(("peer" + String(i)).c_str());
}
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);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_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();
unsigned long now = millis();
if (digitalRead(BTN1_PIN) == LOW && now - lastPress1 > DEBOUNCE) {
lastPress1 = now;
digitalWrite(LED_PIN, HIGH);
Serial.println("Кнопка 1");
sendCommand(cfg_ip1);
notifyPeer(1);
}
// Гасим светодиод когда debounce прошёл
if (now - lastPress1 < DEBOUNCE || now - lastPress2 < DEBOUNCE) {
digitalWrite(LED_PIN, HIGH);
} else {
digitalWrite(LED_PIN, LOW);
}
if (digitalRead(BTN2_PIN) == LOW && now - lastPress2 > DEBOUNCE) {
lastPress2 = now;
digitalWrite(LED_PIN, HIGH);
Serial.println("Кнопка 2");
sendCommand(cfg_ip2);
notifyPeer(2);
}
delay(10);
}