avk013.blogspot.com - другой мой блог "C# and etc"

четверг, 4 июня 2026 г.

watch dog на Arduino Nano + ENC28J60

 абсолютно неактуальная плата, которая лежала 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();
}
===

Комментариев нет:

Отправить комментарий