Skocz do zawartości

Pomocna odpowiedź

Napisano (edytowany)

Cześć, mój ostatni projekt: bardziej do zabawy i chęci nauki niż z praktycznej potrzeby. Jako, że kiedyś tu poległem nad projektem czytania tablic rejestracyjnych przez raspberry, wróciłem do tematu w inny sposób z pełnym sukcesem(?).

Mam 2 urządzenia: esp32u(było c3 ale nie dawało rady) i esp32 cam.
Esp32u działa na dwóch wątkach, jeden obsługuje suplę i pojedynczy kanał wirtualnego przekaźnika, a na drugim wątku łaczy się poprzez API z rejestratorem Dahua i nasłuchuje informacji o przekroczeniu linii(wirtualnej) na kamerze która patrzy przed bramę. W momencie przekroczenia linii, esp włącza vr na 2 sekundy i tutaj kończy się jego praca,
a zaczyna robota esp32 cam, który przez MQTT nasłuchuje powyższy vr i kiedy ten jest włączony, esp wykonuje zdjęcie i wysyła je do AI gemini z prośbą o podanie numeru tablicy rejestracyjnej i marki samochodu. AI odpowiada, esp porównuje odpowiedź z bazą samochodów, jeśli pasuje marka i numer, przez MQTT otwiera bramę.
Skrótowo: jedno esp nasłuchuje rejestratora i kiedy ten mówi "coś wjechało przed bramę" esp włącza na moment virtualrelay. Na tą informację czeka już esp cam który robi zdjęcie i jeśli pasuje marka i numer otwiera bramę.

Kod dla esp32 cam:

//płytka AI Thinker ESP32-CAM

#include "esp_camera.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "Base64.h"
#include <PubSubClient.h>
#include <WiFiClientSecure.h>

#include <WebServer.h>
WebServer server(80);
uint8_t* last_photo_buf = NULL;
size_t last_photo_len = 0;
void handleRoot() {
  if (last_photo_buf != NULL && last_photo_len > 0) {
    server.setContentLength(last_photo_len);
    server.send(200, "image/jpeg", ""); // Wysyła tylko nagłówek
    server.client().write(last_photo_buf, last_photo_len); // Wysyła czyste bajty
  } else {
    server.send(200, "text/plain; charset=UTF-8", "Brak zdjęcia. Wyzwol analize MQTT.");
  }
}

// --- WIFI CONFIGURATION ---
const char* ssid = "x";
const char* password = "x";

// --- GEMINI CONFIGURATION ---
const char* apiKey = "x";

// --- MQTT SUPLA CONFIGURATION ---
const char* mqtt_server = "mqttx.supla.org";
const int mqtt_port = 8883; 
const char* mqtt_user = "x";
const char* mqtt_pass = "x";

// LISTENING TOPIC (Trigger)
const char* topic_sub = "supla/x/devices/x/channels/x/state/on";
// SENDING TOPIC (Opening the gate )
const char* topic_pub = "supla/x/devices/x/channels/x/execute_action";

WiFiClientSecure espClient;
PubSubClient client(espClient);

bool triggerPhoto = false;

struct Samochod {
  String tablica;
  String marka;
};

// Twoja lista uprawnionych pojazdów
Samochod baza[] = {
  {"SPS xxx", "VOLVO"},
  {"SPS xxx", "OPEL"}
};
int ileWBazie = 2;

// --- PINS ESP32-CAM AI-THINKER ---
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22

void callback(char* topic, byte* payload, unsigned int length) {
  String message = "";
  for (int i = 0; i < length; i++) message += (char)payload[i];
  
  // In Supla, the ON state is usually "1"
  if (message == "true") {
    Serial.println(">>> ON signal received via MQTT! Starting analysis...");
    triggerPhoto = true;
  }
}

void reconnect() {
  while (!client.connected()) {
    Serial.print("Connecting to MQTT...");
    if (client.connect("ESP32_CAM_LPR_Node", mqtt_user, mqtt_pass)) {
      Serial.println("Connected!");
      client.subscribe(topic_sub);
    } else {
      Serial.print("Error: ");
      Serial.print(client.state());
      delay(5000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM; config.pin_d1 = Y3_GPIO_NUM; config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM; config.pin_d4 = Y6_GPIO_NUM; config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM; config.pin_d7 = Y9_GPIO_NUM; config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM; config.pin_vsync = VSYNC_GPIO_NUM; config.pin_href = HREF_GPIO_NUM;
  config.pin_sscb_sda = SIOD_GPIO_NUM; config.pin_sscb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM; config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 10000000; config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_VGA; // Optimal for RAM
  config.jpeg_quality = 12;
  config.fb_count = 1;

  esp_camera_init(&config);
  sensor_t * s = esp_camera_sensor_get();
  s->set_vflip(s, 1);
  s->set_hmirror(s, 0);

  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
  Serial.println("\nWiFi OK");
  Serial.println("MQTT config...");
  espClient.setInsecure();
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
   server.on("/", handleRoot);
  server.begin();
  Serial.print("Podglad pod: http://");
  Serial.println(WiFi.localIP());
}

void loop() {
  if (!client.connected()) reconnect();
  client.loop();
  server.handleClient();
  if (triggerPhoto) {
    triggerPhoto = false;
    analyzeWithGemini();
  }
}

void analyzeWithGemini() {
   camera_fb_t * fb_warmup = esp_camera_fb_get();
  if (fb_warmup) esp_camera_fb_return(fb_warmup);
  delay(500); 
  
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) { Serial.println("Camera error"); return; }
  // --- KOPIOWANIE DLA PODGLĄDU ---
  if (last_photo_buf) free(last_photo_buf); 
  last_photo_buf = (uint8_t*)malloc(fb->len);
  if (last_photo_buf) {
    memcpy(last_photo_buf, fb->buf, fb->len);
    last_photo_len = fb->len;
  }
  String base64Image = base64::encode(fb->buf, fb->len);
  esp_camera_fb_return(fb);

  HTTPClient http;
  http.setTimeout(30000); 
  String url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=" + String(apiKey);
  http.begin(url);
  http.addHeader("Content-Type", "application/json");

  String prompt = "Analyze this image. Return ONLY a JSON object with fields: plate, brand. Format: {\\\"plate\\\":\\\"...\\\", \\\"brand\\\":\\\"...\\\"}. Do not add any extra text or markdown.";
  String payload = "{\"contents\":[{\"parts\":[{\"text\":\"" + prompt + "\"},{\"inline_data\":{\"mime_type\":\"image/jpeg\",\"data\":\"" + base64Image + "\"}}]}]}";

  int httpResponseCode = http.POST(payload);
  
  if (httpResponseCode == 200) {
    String response = http.getString();
    DynamicJsonDocument doc(8192);
    deserializeJson(doc, response);   
    String rawJson = doc["candidates"][0]["content"]["parts"][0]["text"];
    Serial.print("ODPOWIEDŹ GEMINI: ");
    Serial.println(rawJson);
    rawJson.replace("```json", "");
    rawJson.replace("```", "");
    rawJson.trim();

    DynamicJsonDocument car(1024);
    deserializeJson(car, rawJson);
    
    String odczytanaTablica = car["plate"];
    String odczytanaMarka = car["brand"];

    Serial.println("\n--- WYNIK ANALIZY ---");
    Serial.printf("TABLICA: %s | %s %s (%s)\n", odczytanaTablica.c_str(), odczytanaMarka.c_str());

    bool znaleziono = false;
    for (int i = 0; i < ileWBazie; i++) { 
      // 1. Przygotowanie tablic (bez spacji, wielkie litery)
      String t1 = odczytanaTablica; t1.replace(" ", ""); t1.toUpperCase();
      String t2 = baza[i].tablica; t2.replace(" ", ""); t2.toUpperCase();

      // 2. Przygotowanie marek (wielkie litery, usunięcie zbędnych spacji)
      String m1 = odczytanaMarka; m1.trim(); m1.toUpperCase();
      String m2 = baza[i].marka; m2.trim(); m2.toUpperCase();

      // WARUNEK: Musi zgadzać się tablica ORAZ marka musi pasować do odczytu
      if (t1 == t2 && (m1.indexOf(m2) != -1 || m2.indexOf(m1) != -1)) {
        Serial.println(">>> DOSTĘP PRZYZNANY: " + baza[i].marka + " [" + baza[i].tablica + "]");
        client.publish(topic_pub, "OPEN");//_CLOSE
        Serial.println("Wysłano MQTT: Otwieram bramę");
        znaleziono = true;
        break;
      }
    }
    if (!znaleziono) {
      Serial.println("XXX POJAZD NIEZGODNY LUB NIEZNANY XXX");
    }
  } else {
    Serial.printf("HTTP error: %d\n", httpResponseCode);
  }
  http.end();    
}

Dodatkowo jest tam jeszcze lokalny podgląd w przeglądarce na zrobione zdjęcie, potrzebne do testów.

Kod dla obsługi zdarzeń rejestratora(miało być wywoływane czujnikiem ruchu, ale po co sobie ułatwiać życie):

CAŁY
//curl -g --digest -u xx:xxx "http://192.x.x.x/cgi-bin/eventManager.cgi?action=attach&codes=[CrossLineDetection]"
//płytka esp32 dev module
#include <WiFi.h>
#include <MD5Builder.h>
#define SUPLA_LOG_LEVEL 3 
#include <SuplaDevice.h>
#include <supla/network/esp_wifi.h>
#include <supla/control/virtual_relay.h>

// --- KONFIGURACJA ---
const char* ssid = "x";
const char* password = "x";
const char* host = "x";
const char* user = "x";
const char* pass = "x";
const char* url  = "/cgi-bin/eventManager.cgi?action=attach&codes=[CrossLineDetection]&channel=2&heartbeat=5";

char GUID[16] = {x};
char AUTHKEY[16] = {x};

WiFiClient d_client;
Supla::ESPWifi supla_wifi(ssid, password); 
auto vRelay = new Supla::Control::VirtualRelay();

unsigned long lastEventTime = 0;
unsigned long lastDataTime = 0;

// --- FUNKCJE POMOCNICZE ---
String md5(String payload) {
  MD5Builder _md5; _md5.begin(); _md5.add(payload); _md5.calculate();
  return _md5.toString();
}

String getDigestAuth(String header) {
  auto getParam = [&](String param) {
    int start = header.indexOf(param + "=\"");
    if (start == -1) return String("");
    start += param.length() + 2;
    return header.substring(start, header.indexOf("\"", start));
  };
  String realm = getParam("realm"), nonce = getParam("nonce"), qop = "auth", nc = "00000001", cnonce = "abcdefgh";
  String ha1 = md5(String(user) + ":" + realm + ":" + String(pass));
  String ha2 = md5("GET:" + String(url));
  String resp = md5(ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2);
  return "Digest username=\"" + String(user) + "\", realm=\"" + realm + "\", nonce=\"" + nonce + 
         "\", uri=\"" + url + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\"" + cnonce + 
         "\", response=\"" + resp + "\", algorithm=MD5";
}

// --- WĄTEK DAHUA (URUCHAMIANY NA RDZENIU 0) ---
void dahuaTask(void * pvParameters) {
  Serial.println("[Dahua] Wątek wystartował na Core 0.");
  
  while(1) {
    if (WiFi.status() == WL_CONNECTED) {
      if (!d_client.connected()) {
        Serial.println("[Dahua] Próba połączenia (Krok 1/2)...");
        
        if (d_client.connect(host, 80)) {
          // Wysłanie zapytania o Digest (udajemy curl dla stabilności)
          d_client.print(String("GET ") + url + " HTTP/1.1\r\n" +
                         "Host: " + host + "\r\n" +
                         "User-Agent: curl/7.68.0\r\n" +
                         "Accept: */*\r\n" +
                         "Connection: close\r\n\r\n");

          String response = "";
          unsigned long t = millis();
          // Czekamy na wyzwanie Digest max 3s
          while ((d_client.connected() || d_client.available()) && (millis() - t < 3000)) {
            if (d_client.available()) {
              char c = d_client.read();
              response += c;
              if (response.indexOf("\r\n\r\n") != -1) break;
            }
            vTaskDelay(1 / portTICK_PERIOD_MS);
          }

          int authPos = response.indexOf("WWW-Authenticate: Digest");
          if (authPos != -1) {
            String authLine = response.substring(authPos, response.indexOf("\r", authPos));
            Serial.println("[Dahua] Logowanie (Krok 2/2)...");
            
            d_client.stop();
            vTaskDelay(150 / portTICK_PERIOD_MS);
            
            if (d_client.connect(host, 80)) {
              d_client.print(String("GET ") + url + " HTTP/1.1\r\n" +
                             "Host: " + host + "\r\n" +
                             "Authorization: " + getDigestAuth(authLine) + "\r\n" +
                             "User-Agent: curl/7.68.0\r\n" +
                             "Accept: */*\r\n" +
                             "Connection: keep-alive\r\n\r\n");
              
              Serial.println("[Dahua] POŁĄCZONO I NASŁUCHUJĘ!");
              lastDataTime = millis();
            }
          } else {
            Serial.println("[Dahua] BŁĄD: Brak autoryzacji Digest w odpowiedzi.");
            d_client.stop();
          }
        }
        if (!d_client.connected()) vTaskDelay(5000 / portTICK_PERIOD_MS);
      }

      // --- STABILNY ODBIÓR STRUMIENIA ZNAK PO ZNAKU ---
      String currentLine = "";
      currentLine.reserve(256); // Optymalizacja pamięci

      while (d_client.connected()) {
        if (d_client.available()) {
          char c = d_client.read();
          lastDataTime = millis(); // Reset timeoutu - coś przyszło!
          
          // Opcjonalne: Serial.print(c); // Odkomentuj, by widzieć "surowy" zrzut danych
          
          if (c == '\n') {
            // Analiza kompletnej linii
            if (currentLine.indexOf("Code=CrossLineDetection") != -1) {
              if (currentLine.indexOf("action=Start") != -1) {
                Serial.println("\n>>> [DAHUA] !!! ALARM PRZEKROCZENIA LINII !!! <<<");
                vRelay->turnOn();
                lastEventTime = millis();
              } else if (currentLine.indexOf("action=Stop") != -1) {
                Serial.println("\n[Dahua] Koniec zdarzenia.");
              }
            }
            currentLine = ""; // Czyścimy bufor po znaku nowej linii
          } 
          else if (c != '\r') {
            currentLine += c;
          }

          // Zabezpieczenie przed zbyt długą linią (brak znaku \n)
          if (currentLine.length() > 512) currentLine = "";
        }

        // Timeout: Dahua milczy (brak ruchu). Restart po 2 minutach ciszy.
        if (millis() - lastDataTime > 120000) { 
          Serial.println("\n[Dahua] Timeout (2 min) - restart połączenia...");
          d_client.stop();
          break;
        }

        vTaskDelay(1 / portTICK_PERIOD_MS); // Kluczowe dla stabilności WiFi/Supla
      }
    } else {
      vTaskDelay(1000 / portTICK_PERIOD_MS); // Czekaj na WiFi
    }
    vTaskDelay(100 / portTICK_PERIOD_MS);
  }
}


void setup() {
  Serial.begin(115200);
  // Start Supli na domyślnym rdzeniu (1)
  SuplaDevice.begin(GUID, "svrx.supla.org", "[email protected]", AUTHKEY);

  // Uruchomienie wątku Dahua na rdzeniu 0
  xTaskCreatePinnedToCore(dahuaTask, "DahuaTask", 8192, NULL, 1, NULL, 0);
  
  Serial.println("System gotowy (Dual Core).");
}

void loop() {
  SuplaDevice.iterate();

  if (vRelay->isOn() && (millis() - lastEventTime > 2000)) {
    vRelay->turnOff();
    Serial.println("[Supla] OFF.");
  }
}

Potestuję jeszcze kilka dni, a potem muszę tą kamerkę jakoś zainstalować. 
Trzeba oczywiście też zmienić prompt tak by gemini odrzucał wszelkie "zdjęcia ze zdjęciem". W aktualnym prompcie wystarczy pokazać zdjęcie naszego auta i już brama się otwiera:).
Bawiąc się z rejestratorem, coś naknociłem bo już nie dostaję na moim telefonie powiadomień push z apki Dahua, pewnie będę musiał ją przeinstalować  .
Projekt powstał z chęci zrobienia czegoś ciekawego. Przy jego powstawaniu ogromnie korzystałem z pomocy AI(również gemini). Potwierdza się pogląd, że z AI krótko piszesz ale długo debugujesz:).

Jeszcze jedna informacja, od pojawienia się na podjeździe do momentu włączenia otwierania bramy mija maksymalnie 5 sekund. Także dość użyteczne. Będę tego używał chyba tylko dla zabawy bo wolę otwierać bramę wcześniej, a nie czekać aż się otworzy.
Jednak projekt dość rozwojowy bo przecież AI może rozpoznawać wszystko, czy pada deszcz(zamknij okna dachowe), jest ciemno(włącz światło), kury uciekły z kurnika i biegają po podwórku(wyślij powiadomienie push z Supli) i wiele innych....

To by było na tyle. 

Wszelki feedback mile widziany, a dobre rady szczególnie. Przede wszystkim chodzi mi o połączenie z rejestratorem. Niestety nie odsyła żadnego heartbeat-u, po nawiązaniu połączenia zwyczajnie nic nie wysyła, chyba, że jest wydarzenie. Z tego co się dowiedziałem to bardzo trudne połączenie do utrzymania, bo nie wiadomo czy ono nadal jest czy go nie ma. Nie znam się na sieciach, routerach http itp. Jakby ktoś z tutejszych magików mógł zajrzeć do tego kodu i coś doradzić czy coś poprawić. Teraz to działa, ale nie wiem czy jest to poprawnie zrobione. Będę testował czy na pewno wyłapuje wszystkie alarmy, bo dodatkowo przychodzi mail z powiadomieniem z rejestratora, więc mam porównanie.

 

Edytowano przez SOYER
  • Lubię! 2

Podoba Ci się ten projekt? Zostaw pozytywny komentarz i daj znać autorowi, że zbudował coś fajnego!

Masz uwagi? Napisz kulturalnie co warto zmienić. Doceń pracę autora nad konstrukcją oraz opisem.

Nadal niestety walczę z połączeniem z rejestratorem. Niby jest ale część powiadomień nie dochodzi. Skrócę timeout reconnecta, bo teraz mam 5 minut zobaczymy. Ale tu znowu pewnie pojawi się problem ilości logowań i blokady ze strony rejestratora. Po południu wrzucę aktualny szkic, może poratujecie.

(edytowany)

Nie testowałem ->  click ale pewnie wypróbowałbym coś takiego. esp cam wstępnie musiałby rozpoznać że coś jest na podjeździe i jeśli uzna że to może być samochód, dodatkowo wysłać do sieci celem pełnej weryfikacji. W ogóle, dałbym sobie w małym palcu paznokieć przyciąć że widziałem takie coś w przykładach od espressif. 

 

Edytowano przez _LM_
(edytowany)
//płytka esp32 dev module
/////////////////////////////////////
#define SUPLA_LOG_LEVEL 0 
/////////////////////////////////////
#include <WiFi.h>
#include <lwip/sockets.h>
#include <MD5Builder.h>
#include <SuplaDevice.h>
#include <supla/network/esp_wifi.h>
#include <supla/control/virtual_relay.h>

// --- KONFIGURACJA ---
const char* ssid = "x";
const char* password = "x";
const char* host = "x";
const char* user = "x";
const char* pass = "x";
const char* url  = "/cgi-bin/eventManager.cgi?action=attach&codes=[CrossLineDetection]&channel=2&heartbeat=30";
IPAddress local_IP(x, x, x, x); // Wybierz wolny adres, np. .150
IPAddress gateway(192, 168, 0, 1);    // Adres Twojego Archera
IPAddress subnet(255, 255, 255, 0);
IPAddress primaryDNS(8, 8, 8, 8);     // DNS Google (opcjonalnie)
char GUID[16] = {x};
char AUTHKEY[16] = {x};

WiFiClient d_client;
Supla::ESPWifi supla_wifi(ssid, password); 
auto vRelay = new Supla::Control::VirtualRelay();

unsigned long lastEventTime = 0;
unsigned long lastDataTime = 0;
String currentLine = "";                 // Deklaracja globalna bufora
volatile bool alarmPending = false;      // Deklaracja globalna flagi (volatile dla Core 0/1)

// --- FUNKCJE POMOCNICZE ---
String md5(String payload) {
  MD5Builder _md5; _md5.begin(); _md5.add(payload); _md5.calculate();
  return _md5.toString();
}

String getDigestAuth(String header) {
  auto getParam = [&](String param) {
    int start = header.indexOf(param + "=\"");
    if (start == -1) return String("");
    start += param.length() + 2;
    return header.substring(start, header.indexOf("\"", start));
  };
  String realm = getParam("realm"), nonce = getParam("nonce"), qop = "auth", nc = "00000001", cnonce = "abcdefgh";
  String ha1 = md5(String(user) + ":" + realm + ":" + String(pass));
  String ha2 = md5("GET:" + String(url));
  String resp = md5(ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2);
  return "Digest username=\"" + String(user) + "\", realm=\"" + realm + "\", nonce=\"" + nonce + 
         "\", uri=\"" + url + "\", qop=" + qop + ", nc=" + nc + ", cnonce=\"" + cnonce + 
         "\", response=\"" + resp + "\", algorithm=MD5";
}

// Funkcja chroniąca przed zrywaniem przez router Archer
void setupKeepAlive(int fd) {
  int enable = 1;
  int idle = 5;     // Wyślij pierwszy pakiet po 5s ciszy
  int interval = 3; // Kolejne co 3s
  int count = 2;     // Po 2 nieudanych próbach zamknij
  setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &enable, sizeof(int));
  setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(int));
  setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(int));
  setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(int));
}

// --- WĄTEK DAHUA (Core 0) ---
void dahuaTask(void * pvParameters) {

  while(1) {
    if (WiFi.status() == WL_CONNECTED) {
      if (!d_client.connected()) {
        d_client.stop(); 
        vTaskDelay(500 / portTICK_PERIOD_MS);
        
        Serial.println("\n[XVR] Logowanie...");
        if (d_client.connect(host, 80)) {
          d_client.print(String("GET ") + url + " HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n");

          String response = "";
          unsigned long t = millis();
          while (millis() - t < 3000) {
            if (d_client.available()) {
              char c = d_client.read(); // Czytamy znak 
              Serial.write(c);          // Wypisujemy go
              response += c;            // Dodajemy do analizy autoryzacji
              if (response.indexOf("\r\n\r\n") != -1) break;
            }
            vTaskDelay(1);
          }
          
          int authPos = response.indexOf("WWW-Authenticate: Digest");
          if (authPos != -1) {
            String authLine = response.substring(authPos, response.indexOf("\r", authPos));
            d_client.stop();
            vTaskDelay(200); 
            
            if (d_client.connect(host, 80)) {
              setupKeepAlive(d_client.fd()); 
              d_client.print(String("GET ") + url + " HTTP/1.1\r\nHost: " + host + 
                             "\r\nAuthorization: " + getDigestAuth(authLine) + 
                             "\r\nConnection: keep-alive\r\n\r\n");
              
              bool loginSuccess = false;
              unsigned long waitStart = millis();
              while (millis() - waitStart < 5000) {
                if (d_client.available()) {
                  String headerLine = d_client.readStringUntil('\n');
                  if (headerLine.indexOf("200 OK") != -1) { loginSuccess = true; break; }
                }
                vTaskDelay(1);
              }

              if (loginSuccess) {
                Serial.println("[XVR] POŁĄCZONO.");
                lastDataTime = millis();
                currentLine = "";

                while (d_client.connected()) {
                  while (d_client.available() > 0) {
                    char c = d_client.read();
                    lastDataTime = millis(); 
                    currentLine += c;

                    // Szukamy alarmu w locie
                    if (currentLine.indexOf("CrossLineDetection") != -1) {
                        if (currentLine.indexOf("Start") != -1) {
                            Serial.println("\n>>> [XVR] ALARM WYKRYTY !!! <<<");
                            alarmPending = true; // USTAWIANIE FLAGI DLA LOOP
                            currentLine = ""; 
                        }
                    }

                    if (currentLine.length() > 400) {
                      currentLine = currentLine.substring(currentLine.length() - 64);
                    }
                  }

                  if (millis() - lastDataTime > 3600000) { 
                    Serial.println("\n[XVR] Timeout 1h - restart.");
                    break;
                  }
                  vTaskDelay(1); 
                }
              }
            }
          }
        }
        d_client.stop();
        vTaskDelay(5000 / portTICK_PERIOD_MS);
      }
    }
    vTaskDelay(100 / portTICK_PERIOD_MS);
  }
}


void setup() {
  Serial.begin(115200);
   if (!WiFi.config(local_IP, gateway, subnet, primaryDNS)) {
    Serial.println("[WiFi] Błąd konfiguracji statycznego IP!");
  }
  WiFi.setSleep(false);
  currentLine.reserve(512); 
  // Start Supla
  SuplaDevice.begin(GUID, "svrx.supla.org", "[email protected]", AUTHKEY);

  // Uruchomienie Dahua na Core 0 (Supla działa na Core 1)
  xTaskCreatePinnedToCore(dahuaTask, "DahuaTask", 8192, NULL, 1, NULL, 0);
  
  Serial.println("System gotowy.");
}

void loop() {
  SuplaDevice.iterate();

  if (alarmPending) {
    vRelay->turnOn();
    lastEventTime = millis();
    alarmPending = false; // Resetujemy flagę po obsłużeniu
    Serial.println("[Supla] Virtual Relay ON.");
  }

  // Automatyczne wyłączenie po 2 sekundach
  if (vRelay->isOn() && (millis() - lastEventTime > 2000)) {
    vRelay->turnOff();
    Serial.println("[Supla] Virtual Relay OFF.");
  }
}

Sprawdzony kod powyżej. Reaguje na wszystkie powiadomienia z rejestratora. Kilka dni testów. Problem był taki, że rejestrator nie wysyła heartbeat. Router po jakimś czasie bezczynności tez podobno rozłącza połączenie.  Nie wiem , nie znam się. Jednak taki kod już działa bezproblemowo. Funkcja keepalive, podobno działa gdzieś "niżej" robi robotę by router nas nie rozłączał. Dowiedziałem się, że jest też coś takiego jak digest. Robiąc coś czego się nie zna, można się wiele dowiedzieć, a najwięcej tego jak mało się wie.

Na razie nie mam ochoty implementować tego fizycznie. Samo otwieranie bramy przy pomocy kamery jest mi niepotrzebne. Jednak jako ciekawa nauka było to bardzo fajne doświadczenie. Zamówię u majfriendów esp32s3 i spróbuję zbierać klatkę ze streamingu rejestratora i taką klatkę wysyłać do gemini. Jedyny problem to usytuowanie kamery która będzie źródłem dla wysyłanej klatki, duży kąt... Zobaczymy co z tego wyjdzie. Wtedy wystarczyłoby jedno esp32 za 20 zeta umieszczone w zasięgu sieci w której jest rejestrator. Choć i tak musiałbym podjechać pod bramę i czekać na otwarcie bramy. Wolę otworzyć na minutę przed wjazdem.... Jednak doświadczenie programistyczne ciekawe. 

Edytowano przez SOYER
  • Lubię! 1

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...