абсолютно неактуальная плата, которая лежала 10 лет, ввиду обнищания было решено приспособить для перезагрузки удаленного коммутатора, в виду его случайных проблем по питанию. Адаптировано с КЛОДОМ. >>>ссылка<<<
===
// PING WATCHDOG v4.5
// Arduino Nano + ENC28J60 + реле // ============================================================ // // НАЗНАЧЕНИЕ: // Периодически проверяет доступность хоста (UDP DNS-запрос). // При потерях >= порога — отключает реле на 30 сек, затем // ГАРАНТИРОВАННО включает обратно (два независимых механизма). // Веб: статус открыт, управление по паролю. // Пароль — в EEPROM, меняется через веб без перепрошивки. // Сброс настроек — кнопкой при старте. // WDT 8 сек — защита от зависания. // // ПОЧЕМУ UDP А НЕ TCP: // EthernetClient.connect() в UIPEthernet игнорирует setTimeout() // и блокирует loop() до 30 сек — контроллер зависает. // UDP неблокирующий: отправили → ждём в loop() → таймаут. // Фильтрация мусора: проверяем Transaction ID в ответе (0xABCD). // ICMP unreachable от роутера имеет другой формат → отбрасывается. // // СХЕМА: // ENC28J60 → Nano: VCC→3.3V GND→GND // SCK→D13 SI→D11 SO→D12 CS→D10 // // РЕЛЕ (активный LOW модуль): // D4 → IN прямой (HIGH = нагрузка ВКЛ) // D5 → IN инверсный (LOW = нагрузка ВКЛ, всегда !D4) // Нагрузка на NO контакте реле. // // КНОПКА СБРОСА: // D7 → кнопка → GND (INPUT_PULLUP) // Зажать при подаче питания, держать 3 сек → // сброс IP / интервала / пароля к заводским. // LED мигнёт 5 раз быстро = подтверждение. // // HEARTBEAT LED (внешний): // D8 → 220 Ом → LED → GND // 1 Гц = норма // 5 Гц = реле отключено (идёт перезагрузка нагрузки) // тихо = контроллер завис // // ПЕРВЫЙ ЗАПУСК: // 1. Отключить ENC28J60, прошить, подключить обратно. // 2. http://192.168.3.15 — статус (без пароля). // 3. Пароль по умолчанию: 1234 // // СБРОС (забыт пароль): // Зажать D7, включить питание, держать 3 сек. // Пароль→1234, IP→192.168.3.254, интервал→2 мин. // // ЗАВИСИМОСТИ: // UIPEthernet — https://github.com/UIPEthernet/UIPEthernet // ============================================================ #include <UIPEthernet.h> #include <EEPROM.h> #include <avr/wdt.h> // --- Пины --- #define RLY1 4 #define RLY2 5 #define BTN 7 #define LED 8 // --- Параметры проверки --- #define P_CNT 10 // пакетов в серии #define P_STEP 700UL // интервал между отправками, мс #define P_TOUT 600UL // таймаут ожидания ответа, мс // P_STEP > P_TOUT — новый пакет только // после завершения ожидания предыдущего #define LOSS_T 50 // % потерь для срабатывания // Transaction ID нашего DNS-запроса — по нему фильтруем ответ #define DNS_TID_HI 0xAB #define DNS_TID_LO 0xCD // --- Реле --- #define REBOOT 30000UL #define SAFETY 5000UL // --- Веб --- #define WEB_T 1500UL // --- EEPROM --- #define EE_MAGIC 0xA9 #define EE_ADR 0 #define PW_LEN 13 // макс 12 символов + \0 struct Cfg { byte mag; byte ip[4]; uint8_t intv; char pw[PW_LEN]; } cfg; // ============================================================ // Сеть // ============================================================ byte mac[] = { 0xDE,0xAD,0xBE,0xEF,0xFE,0xED }; IPAddress myIp(192, 168, 3, 15); EthernetServer srv(80); EthernetUDP udp; // DNS-запрос: Transaction ID=0xABCD, запрос A-записи "." static const byte DNSQ[] PROGMEM = { 0xAB,0xCD, 0x01,0x00, 0x00,0x01, 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00, 0x00,0x01, 0x00,0x01 }; // ============================================================ // Состояние // ============================================================ bool rlyOff = false; unsigned long rlyOnT = 0; unsigned long safeT = 0; // Конечный автомат: // IDLE: ждём времени серии // SEND: отправляем UDP, переходим в WAIT // WAIT: ждём ответ (неблокирующий), таймаут → SEND следующего // DONE: итог серии enum St : byte { IDLE, SEND, WAIT, DONE } pSt = IDLE; unsigned long lastSer = 0; unsigned long lastSt = 0; unsigned long sentT = 0; byte pSent = 0, pLost = 0, loss = 0; unsigned long hbT = 0; bool hbSt = false; // 0=RDY 1=PRB 2=OK 3=FAIL 4=OFF30s 5=RST 6=SAV 7=BADPW byte stat = 0; const char m0[] PROGMEM = "RDY"; const char m1[] PROGMEM = "PRB"; const char m2[] PROGMEM = "OK"; const char m3[] PROGMEM = "FAIL"; const char m4[] PROGMEM = "OFF30s"; const char m5[] PROGMEM = "RST"; const char m6[] PROGMEM = "SAV"; const char m7[] PROGMEM = "BADPW"; const char* const MSGS[] PROGMEM = {m0,m1,m2,m3,m4,m5,m6,m7}; // ============================================================ // Утилиты // ============================================================ void getM(byte i, char* b) { strncpy_P(b, (char*)pgm_read_word(&MSGS[i]), 11); b[11] = '\0'; } bool gP(const char* req, const char* key, char* buf, byte sz) { const char* p = strstr(req, key); if (!p) return false; p += strlen(key); byte i = 0; while (*p && *p != '&' && *p != ' ' && i < sz-1) buf[i++] = *p++; buf[i] = '\0'; return i > 0; } bool pIp(const char* s, byte* o) { byte v=0, d=0, n=0; for (byte i=0; ; i++) { char c = s[i]; if (c>='0'&&c<='9') { v=v*10+(c-'0'); n++; } else if ((c=='.'||c=='\0')&&n) { if (v>255) return false; o[d++]=v; v=0; n=0; if (!c) break; } else return false; if (d>4) return false; } return d==4; } void prIp(EthernetClient& c) { c.print(cfg.ip[0]); c.print('.'); c.print(cfg.ip[1]); c.print('.'); c.print(cfg.ip[2]); c.print('.'); c.print(cfg.ip[3]); } void prP(EthernetClient& c, const char* p) { char ch; while ((ch=pgm_read_byte(p++))) c.write(ch); } // ============================================================ // Реле // ============================================================ void setR(bool on) { digitalWrite(RLY1, on ? HIGH : LOW); digitalWrite(RLY2, on ? LOW : HIGH); rlyOff = !on; if (on) { rlyOnT=0; safeT=0; } } // ============================================================ // EEPROM // ============================================================ void defCfg() { cfg.mag = EE_MAGIC; cfg.ip[0] = 192; cfg.ip[1] = 168; cfg.ip[2] = 3; cfg.ip[3] = 254; cfg.intv = 2; strncpy(cfg.pw, "1234", PW_LEN); } void loadC() { EEPROM.get(EE_ADR, cfg); if (cfg.mag != EE_MAGIC) { defCfg(); EEPROM.put(EE_ADR, cfg); } } void rstC() { defCfg(); EEPROM.put(EE_ADR, cfg); for (byte i=0; i<10; i++) { digitalWrite(LED, i&1); delay(80); } } // ============================================================ // Heartbeat // ============================================================ void hbUp(unsigned long now) { unsigned long p = rlyOff ? 100UL : 500UL; if (now-hbT >= p) { hbT=now; hbSt=!hbSt; digitalWrite(LED,hbSt); } } // ============================================================ // UDP-зонд: отправить DNS-запрос // Неблокирующий — только отправка, ответ читаем в WAIT // ============================================================ bool sendP() { static uint16_t prt = 50000; if (++prt < 50001) prt = 50001; udp.stop(); if (!udp.begin(prt)) return false; IPAddress t(cfg.ip[0],cfg.ip[1],cfg.ip[2],cfg.ip[3]); udp.beginPacket(t, 53); for (byte i=0; i<17; i++) udp.write(pgm_read_byte(&DNSQ[i])); return udp.endPacket()==1; } // Проверить ответ: валидный DNS-ответ с нашим Transaction ID? // Возвращает: 1=наш ответ, -1=чужой пакет/мусор, 0=нет пакета int8_t checkReply() { int len = udp.parsePacket(); if (len <= 0) return 0; // нет пакета if (len < 4) { udp.flush(); return -1; } // слишком короткий byte b0 = udp.read(); byte b1 = udp.read(); udp.flush(); // Проверяем Transaction ID — первые 2 байта DNS-ответа if (b0==DNS_TID_HI && b1==DNS_TID_LO) return 1; // наш ответ return -1; // чужой пакет (ICMP unreachable и пр.) } // ============================================================ // Перезагрузка нагрузки // ============================================================ void doReboot() { if (rlyOff) return; pSt = IDLE; setR(false); rlyOnT = millis() + REBOOT; if (!rlyOnT) rlyOnT = 1; safeT = millis(); stat = 4; } // ============================================================ // HTML // ============================================================ const char H1[] PROGMEM = "HTTP/1.1 200 OK\r\nContent-Type:text/html;charset=utf-8\r\n" "Connection:close\r\n\r\n" "<!DOCTYPE html><html><head>" "<meta charset='utf-8'>" "<meta name='viewport' content='width=device-width,initial-scale=1'>" "<title>WD</title></head><body><h3>WD-gofra</h3>" "RLY:"; const char H2[] PROGMEM = "<br>IP:"; const char H3[] PROGMEM = "<br>INT:"; const char H4[] PROGMEM = "m<br>LOSS:"; const char H5[] PROGMEM = "%<br>ST:"; const char HL[] PROGMEM = "<hr><form action='/' method='get'>" "<input type='password' name='p' maxlength='12' placeholder='Password'>" " <input type='submit' value='Login'>" "</form></body></html>"; const char HA1[] PROGMEM = "<hr><a href='/reboot?p="; const char HA2[] PROGMEM = "'>[REBOOT 30s]</a><br><br>" "<form action='/set' method='get'>" "<input type='hidden' name='p' value='"; const char HA3[] PROGMEM = "'>" "IP:<input type='text' name='ip' value='"; const char HA4[] PROGMEM = "' maxlength='15' required><br>" "INT(m):<input type='number' name='int' value='"; const char HA5[] PROGMEM = "' min='1' max='60' required><br>" "NewPW:<input type='password' name='np' maxlength='12'><br>" "<input type='submit' value='SAVE'>" "</form></body></html>"; void sendPg(EthernetClient& c, bool adm, const char* pw) { prP(c,H1); if (rlyOff) { unsigned long rem=0, now=millis(); if (rlyOnT>now) rem=(rlyOnT-now)/1000; c.print(F("OFF(")); c.print(rem); c.print(F("s)")); } else { c.print(F("ON")); } prP(c,H2); prIp(c); prP(c,H3); c.print(cfg.intv); prP(c,H4); c.print(loss); prP(c,H5); char b[12]; getM(stat,b); c.print(b); if (!adm) { prP(c,HL); } else { prP(c,HA1); c.print(pw); prP(c,HA2); c.print(pw); prP(c,HA3); prIp(c); prP(c,HA4); c.print(cfg.intv); prP(c,HA5); } } // ============================================================ // Веб-обработчик // ============================================================ void hWeb() { EthernetClient cl = srv.available(); if (!cl) return; char req[100]; byte rLen=0; unsigned long t=millis(); while (cl.connected() && millis()-t<WEB_T) { if (cl.available()) { char ch=cl.read(); if (ch=='\n') break; if (ch!='\r' && rLen<sizeof(req)-1) req[rLen++]=ch; } } req[rLen]='\0'; { char pv=0; t=millis(); while (cl.connected() && millis()-t<WEB_T) { if (cl.available()) { char ch=cl.read(); if (ch=='\n'&&pv=='\n') break; pv=(ch=='\r')?pv:ch; } } } char pw[PW_LEN]; pw[0]='\0'; gP(req,"p=",pw,sizeof(pw)); bool adm = pw[0] && strcmp(pw,cfg.pw)==0; if (adm) { if (strstr(req,"/reboot")) { doReboot(); } else if (strstr(req,"/set")) { bool ok=false; char ib[16]; if (gP(req,"ip=",ib,sizeof(ib))) { byte tmp[4]; if (pIp(ib,tmp)) { memcpy(cfg.ip,tmp,4); ok=true; } } char nb[4]; if (gP(req,"int=",nb,sizeof(nb))) { int v=atoi(nb); if (v>=1&&v<=60) { cfg.intv=v; ok=true; } } char np[PW_LEN]; if (gP(req,"np=",np,sizeof(np)) && np[0]) { strncpy(cfg.pw,np,PW_LEN-1); cfg.pw[PW_LEN-1]='\0'; ok=true; } if (ok) { EEPROM.put(EE_ADR,cfg); stat=6; } } } else if (pw[0]) { stat=7; } sendPg(cl,adm,pw); cl.stop(); } // ============================================================ // setup() // ============================================================ void setup() { pinMode(RLY1, OUTPUT); pinMode(RLY2, OUTPUT); pinMode(BTN, INPUT_PULLUP); pinMode(LED, OUTPUT); setR(true); if (digitalRead(BTN)==LOW) { unsigned long ts=millis(); while (digitalRead(BTN)==LOW) { if (millis()-ts > 3000UL) { rstC(); break; } digitalWrite(LED, (millis()/200)&1); } } loadC(); Ethernet.begin(mac, myIp); srv.begin(); wdt_enable(WDTO_8S); } // ============================================================ // loop() // ============================================================ void loop() { wdt_reset(); unsigned long now = millis(); hbUp(now); // Гарантированное включение реле (два механизма) if (rlyOff) { if ((long)(now-rlyOnT) >= 0) { setR(true); lastSer=now; stat=5; } else if (now-safeT >= REBOOT+SAFETY) { setR(true); lastSer=now; stat=5; } } // Конечный автомат проверки — полностью неблокирующий if (!rlyOff) { unsigned long iv = (unsigned long)cfg.intv * 60000UL; switch (pSt) { case IDLE: if (now-lastSer >= iv) { pSent=pLost=0; pSt=SEND; lastSt=now; stat=1; } break; case SEND: // Ждём P_STEP после предыдущего пакета, затем отправляем if (now-lastSt >= P_STEP) { lastSt=now; if (sendP()) { sentT=now; pSt=WAIT; } else { // Не смогли отправить — считаем потерей pLost++; pSent++; if (pSent>=P_CNT) pSt=DONE; } } break; case WAIT: // Каждую итерацию loop() проверяем — пришёл ли ответ { int8_t r = checkReply(); if (r == 1) { // Валидный DNS-ответ с нашим TID — успех pSent++; pSt = (pSent>=P_CNT) ? DONE : SEND; lastSt=now; } else if (r == -1) { // Чужой пакет (ICMP мусор) — игнорируем, ждём дальше // но не сбрасываем таймаут } else if (now-sentT >= P_TOUT) { // Таймаут — потеря pLost++; pSent++; pSt = (pSent>=P_CNT) ? DONE : SEND; lastSt=now; } } break; case DONE: loss = (pLost*100)/P_CNT; lastSer=now; pSt=IDLE; if (loss>=LOSS_T) { stat=3; doReboot(); } else stat=2; break; } } hWeb(); Ethernet.maintain(); }===
Комментариев нет:
Отправить комментарий