Проверка подключения Arduino
ip arduino /status
#include <ETH.h>
#include <WebServer.h>
#include <Preferences.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <vector>
// --- НАСТРОЙКИ WT32-ETH01 ---
#define ETH_PHY_TYPE ETH_PHY_LAN8720
#define ETH_PHY_ADDR 1
#define ETH_PHY_MDC 23
#define ETH_PHY_MDIO 18
#define ETH_PHY_POWER 16
#define ETH_CLK_MODE ETH_CLOCK_GPIO0_IN
const int sensorPin = 4;
WebServer server(80);
Preferences prefs;
// Глобальные переменные сети
String local_ip, tablo_ip, open_id;
IPAddress l_ip, gateway(172, 16, 1, 1), subnet(255, 255, 255, 0);
// --- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ СОСТОЯНИЯ ---
bool system_is_open = false;
String last_event_time = "—";
String imtf_last_error = "—";
bool _imtf_override_active = false;
struct SavedZero {
int cid;
std::vector<int> zeroIds;
};
std::vector<SavedZero> _saved_tablo_zeros;
void loadSettings() {
prefs.begin("imtf_cfg", true);
local_ip = prefs.getString("l_ip", "172.16.1.77");
tablo_ip = prefs.getString("t_ip", "172.16.1.85");
open_id = prefs.getString("sc_id", "3");
prefs.end();
l_ip.fromString(local_ip);
}
String getTimeStr() {
unsigned long sec = millis() / 1000;
return String(sec / 3600) + ":" + String((sec % 3600) / 60) + ":" + String(sec % 60);
}
// --- ЛОГИКА ОТКРЫТИЯ (ОПРОС И СМЕНА ТАБЛО) ---
void imtf_on_cabinet_opened(JsonArray connections, HTTPClient& http, const String& apiBase) {
if (_imtf_override_active) {
Serial.println("IMTF: шкаф уже в режиме предупреждения, повтор OPEN пропущен");
return;
}
_saved_tablo_zeros.clear();
for (JsonObject conn : connections) {
int cid = conn["id"];
std::vector<int> prev_ids;
JsonArray zeros = conn["zeroScenarioIds"];
for (int z : zeros) {
prev_ids.push_back(z);
}
_saved_tablo_zeros.push_back({cid, prev_ids});
zeros.clear();
zeros.add(open_id.toInt());
http.begin(apiBase);
http.addHeader("Content-Type", "application/json");
String output;
serializeJson(conn, output);
http.POST(output);
http.end();
http.begin(apiBase + "/reset/" + String(cid));
http.POST("");
http.end();
Serial.printf("IMTF: conn id=%d -> zeroScenarioIds=[%s]\n", cid, open_id.c_str());
}
_imtf_override_active = true;
imtf_last_error = "—";
}
// --- ЛОГИКА ЗАКРЫТИЯ (ВОССТАНОВЛЕНИЕ ТАБЛО) ---
void imtf_on_cabinet_closed(JsonArray connections, HTTPClient& http, const String& apiBase) {
if (!_imtf_override_active) {
Serial.println("IMTF: CLOSE без активного переключения — восстановление не требуется");
return;
}
for (auto& saved : _saved_tablo_zeros) {
JsonObject target_conn;
bool found = false;
for (JsonObject conn : connections) {
if (conn["id"] == saved.cid) {
target_conn = conn;
found = true;
break;
}
}
if (!found) {
Serial.printf("IMTF: conn id=%d не найден при восстановлении\n", saved.cid);
continue;
}
JsonArray zeros = target_conn["zeroScenarioIds"];
zeros.clear();
for (int z : saved.zeroIds) {
zeros.add(z);
}
http.begin(apiBase);
http.addHeader("Content-Type", "application/json");
String output;
serializeJson(target_conn, output);
http.POST(output);
http.end();
http.begin(apiBase + "/reset/" + String(saved.cid));
http.POST("");
http.end();
Serial.printf("IMTF: conn id=%d восстановлено\n", saved.cid);
}
_saved_tablo_zeros.clear();
_imtf_override_active = false;
imtf_last_error = "—";
}
void triggerIMTF(bool open) {
HTTPClient http;
String apiBase = "http://" + tablo_ip + ":8081/api/nova-connection";
http.begin(apiBase);
int httpCode = http.GET();
if (httpCode == 200) {
DynamicJsonDocument doc(16384);
DeserializationError err = deserializeJson(doc, http.getString());
http.end();
if (!err) {
JsonArray connections = doc["connections"];
if (open) imtf_on_cabinet_opened(connections, http, apiBase);
else imtf_on_cabinet_closed(connections, http, apiBase);
} else {
imtf_last_error = "Ошибка парсинга JSON";
Serial.println(imtf_last_error);
}
} else {
imtf_last_error = "Ошибка связи HTTP: " + String(httpCode);
Serial.println(imtf_last_error);
http.end();
}
}
// --- НОВЫЙ ЧИСТЫЙ ЭНДПОИНТ /status (API JSON) ---
void handleStatus() {
String json = "{";
json += "\"is_open\":" + String(system_is_open ? "true" : "false") + ",";
json += "\"status_text\":\"" + String(system_is_open ? "OPEN" : "CLOSED") + "\",";
json += "\"uptime_sec\":" + String(millis() / 1000) + ",";
json += "\"uptime_formatted\":\"" + getTimeStr() + "\"";
json += "}";
server.send(200, "application/json; charset=utf-8", json);
}
/* --- ВЕБ-ИНТЕРФЕЙС И НАСТРОЙКИ ЗАКОММЕНТИРОВАНЫ ---
void handleRoot() {
// Код старой HTML страницы скрыт
}
void handleConfig() {
// Код старой админки настроек скрыт
}
void handleSave() {
// Код сохранения настроек через веб скрыт
}
-------------------------------------------------- */
void setup() {
Serial.begin(115200);
pinMode(sensorPin, INPUT_PULLUP);
loadSettings();
ETH.begin(ETH_PHY_TYPE, ETH_PHY_ADDR, ETH_PHY_MDC, ETH_PHY_MDIO, ETH_PHY_POWER, ETH_CLK_MODE);
ETH.config(l_ip, gateway, subnet);
// Регистрация эндпоинтов (веб-страницы отключены)
// server.on("/", handleRoot);
// server.on("/config", handleConfig);
// server.on("/save", handleSave);
server.on("/status", handleStatus); // Оставляем только JSON API
server.begin();
system_is_open = digitalRead(sensorPin);
last_event_time = "Система запущена в " + getTimeStr();
Serial.println("Система готова (Режим API).");
}
void loop() {
server.handleClient();
bool current = digitalRead(sensorPin);
if (current != system_is_open) {
delay(60); // Антидребезг
if (digitalRead(sensorPin) == current) {
system_is_open = current;
last_event_time = "дверь " + String(system_is_open ? "открыта" : "закрыта") + " в " + getTimeStr();
triggerIMTF(system_is_open);
}
}
}
Код предназначен для ESP32 на базе платы WT32-ETH01 (Ethernet). Он реализует:
digitalRead возвращает 0/1, но с PULLUP открытие даёт 0? На самом деле при замкнутом датчике на GND – 0, при размыкании – 1. Код использует system_is_open = digitalRead(sensorPin); без инверсии – значит «открыта» соответствует true, если датчик выдаёт HIGH. Это зависит от схемы, но допустим.)open_id, отправляет POST на /api/nova-connection и затем POST на /reset/{cid}./status с информацией о состоянии двери, uptime./, /config, /save) полностью закомментирован, остался только API.Проблема:
В imtf_on_cabinet_opened сначала сохраняются старые zero-сценарии в глобальный вектор _saved_tablo_zeros, а затем выполняются HTTP-запросы (POST на изменение и reset). Если какой-то запрос завершится ошибкой (таймаут, 4xx/5xx), флаг _imtf_override_active всё равно станет true, и система будет считать, что переключение выполнено. При закрытии она попытается восстановить сохранённые значения, но на табло они могли и не измениться – в результате состояние рассинхронизируется.
Решение:
_imtf_override_active.Проблема:
В imtf_on_cabinet_closed повторно запрашивается список connections через GET. Если между открытием и закрытием на табло добавился или удалился какой-то connection, то для сохранённого cid может не найтись соответствующего объекта в свежем JSON. В этом случае восстановление для этого connection не выполняется, а его старые zero-сценарии остаются перезаписанными навсегда. Флаг _imtf_override_active при этом сбрасывается, но часть данных не восстановлена.
Решение:
connections (только zero-сценарии которых были изменены). При открытии можно сохранить не только старые zeroIds, но и полную структуру connection или хотя бы гарантировать, что восстановление делается по тому же списку, который был при открытии.cid уникален и не меняется, и при отсутствии connection в свежем списке – пропустить восстановление, но залогировать ошибку и, возможно, повторять попытку через некоторое время.Проблема:
В imtf_on_cabinet_opened и imtf_on_cabinet_closed не проверяются коды ответов после http.POST() и http.POST("/reset/..."). Если какой-то запрос упадёт, система продолжит работу, считая, что всё в порядке.
Решение:
http.POST() проверять httpCode и, если он не в диапазоне 200–299, генерировать ошибку, возможно прерывая весь цикл и откатывая изменения.DynamicJsonDocument размером 16 КБ на стекеПроблема:
DynamicJsonDocument doc(16384); создаёт на стеке 16 КБ. На ESP32 стек обычно ограничен 8–16 КБ, и такая большая переменная может привести к переполнению стека, особенно если в проекте используются другие функции с большими локальными буферами.
Решение:
std::unique_ptr<DynamicJsonDocument> или make_unique.StaticJsonDocument<...> если максимальный размер JSON известен и не превышает, скажем, 8 КБ.HTTPClient после http.end() и повторное использованиеПроблема (не критичная, но неоптимальная):
В triggerIMTF вызывается http.end() после GET, затем объект http передаётся в функции, которые снова вызывают http.begin(). Технически это допустимо, но сбивает с толку. Кроме того, внутри цикла по connections для каждого запроса создаётся новый TCP-соединение, что медленно.
Решение:
HTTPClient для каждой серии запросов или переиспользовать один, но явно вызывать http.end() только после завершения всех операций. Можно вынести логику POST и reset в отдельные функции, которые принимают ссылку на HTTPClient и управляют его жизненным циклом.Проблема:
setup() запускает ETH.begin() и сразу регистрирует сервер, но не дожидается получения IP-адреса. Если triggerIMTF вызовется сразу после старта (например, датчик замкнут), HTTP-запросы упадут с ошибкой, так как сеть ещё не поднята.
Решение:
ETH.linkUp() и ETH.localIP() != IPAddress(0,0,0,0) с таймаутом.loop() перед обработкой датчика проверять ETH.linkUp().triggerIMTFПроблема:
Если во время выполнения HTTP-запросов (сотни миллисекунд) датчик изменит состояние ещё раз (дребезг или быстрое открытие/закрытие), новое событие будет обработано сразу после возврата из triggerIMTF, но старое ещё не завершилось корректно. Это может привести к наложению переходов и нарушению логики _imtf_override_active.
Решение:
bool imtf_in_progress = false;. При входе в triggerIMTF проверять и ставить, по выходу снимать.imtf_last_error не сбрасывается при успешной операцииПроблема:
Если произошла ошибка (HTTP-код или парсинг JSON), imtf_last_error устанавливается. При следующем успешном открытии/закрытии внутри imtf_on_cabinet_opened или imtf_on_cabinet_closed она сбрасывается в "—", но только если был успешный GET и вызов этих функций. Если же ошибка случилась на этапе GET (нет ответа), то imtf_last_error останется старой, даже если последующие вызовы станут успешными.
Решение:
triggerIMTF установить imtf_last_error = "—"; или "ожидание...".else.handleStatusПроблема:
Конкатенация строк для JSON опасна: если в uptime_formatted или других полях появятся кавычки или спецсимволы, JSON станет невалидным. Сейчас поля безопасны (getTimeStr возвращает только цифры и двоеточия), но потенциально это уязвимость при будущих доработках.
Решение:
ArduinoJson для сериализации, даже для простых ответов. Это гарантирует корректное экранирование и упростит расширение.delay или yield в loop()Проблема:
При отсутствии входящих HTTP-запросов server.handleClient() быстро возвращает управление, и цикл постоянно читает датчик (тысячи раз в секунду). Это излишне нагружает процессор и увеличивает энергопотребление.
Решение:
delay(10); или vTaskDelay(10 / portTICK_PERIOD_MS); в конце loop() (но не внутри обработки антидребезга).std::vector и глобальным конструкторомПроблема:
std::vector<SavedZero> _saved_tablo_zeros; – глобальный объект, конструктор которого вызывается до setup(). На ESP32 это нормально, но если в проекте используются статические объекты с динамическим выделением памяти, может возникнуть фрагментация. В данном случае серьёзной проблемы нет, но стоит иметь в виду.
Решение:
setup() через .reserve() или вообще сделать указатель, выделяемый в куче при первом использовании. Однако для простоты можно оставить как есть.Проблема:
Если при открытии произошёл сбой и _imtf_override_active остался true (например, из-за ошибки в одном из POST), то последующие открытия будут игнорироваться (if (_imtf_override_active) return;). Система навсегда застрянет в режиме «переключено», и восстановление не произойдёт до тех пор, пока не поступит событие закрытия.
Решение:
_imtf_override_active истинно дольше N секунд (например, 60), принудительно вызвать закрытие или сбросить флаг.imtf_on_cabinet_opened сбрасывать _imtf_override_active и очищать _saved_tablo_zeros.Проблема:
В loop() после обнаружения изменения вызывается delay(60), затем повторное чтение. Если за эти 60 мс датчик вернулся в прежнее состояние, то событие всё равно обрабатывается (сравнение с current, которое было до delay). Это может привести к ложным срабатываниям.
Решение:
triggerIMTF.