• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Подглядываем а при желании подслушиваем =) / ESP32-Cam-Spy + Source Code

NMZ

(L2) cache
Пользователь
Регистрация
19.08.2024
Сообщения
386
Реакции
397
Гарант сделки
3
Депозит
1.6 Ł
:smile10: Всем привет

У меня это первая статья для этого раздела, и решил я сделать безделушку, при помощи которой вы сможете подсмотреть что-то, что вам нужно, и подслушать, что вам нужно.

Идея такова: чтобы сделать скрытую камеру с возможностью накатывать свою прошивку, какие-то свои изменения добавлять и чтобы она была скрытной. С этим есть небольшие проблемы, но я дам вам пару идей, как это можно сделать.

Для этого нам понадобиться установить Arduino IDE

1740776985548.png

— в моем случае это версия 2.3.2 — и установить туда поддержку плат ESP.

Что потребуется из компонентов?

1740777345938.png


Собственно, сама ESP-32 Cam. Я буду лично использовать Ai Thinker ESP-32 Cam, более подробно по ней можете почитать тут –

https://github.com/prusa3d/Prusa-Firmware-ESP32-Cam/blob/master/doc/AI_Thinker-ESP32-cam/README.md

1740776842803.png


Также, в необязательных компонентах — какая-то платка зарядки по типу 03962A (как в моем примере) / TP4065 и аккумулятор для неё, компактный Li-On (в примере у меня Li-Po, так как ничего компактного, кроме как его, не нашел), и если будете скрывать камеру, как я в статейке, то — какой-то дешевый блок питания для телефона, который будет не жалко.
1740776207989.png
Приступим к самому разбору того, что нам нужно сделать =)

Начну для начала с того, какие идеи о том, где это можно скрыть, ко мне приходили, и почему я пришел к такому выбору.

В первую очередь нужно понимать, что наша цель — это скрыть нашу камеру, но в тот же момент нужно понимать, что она долго не проживёт на своём аккумуляторе, ну, например, в какой-то изолированной банке с дыркой для камеры. Поэтому нам нужен какой-то источник электричества, который будет постоянно питать наше чудо и
давать возможность вести наблюдение. Из тех вариантов, что мне пришли в голову, — это использовать зарядку для смартфона, так как то, что она в розетке, постоянно никого не смутит. Ну, и ещё были такие варианты, как различные другие приборы, например, кофемашинка (у меня такая модель, куда можно было бы впихнуть легко камеру, но не думаю, что вариант такой уж универсальный).

Решил не выдумывать велосипед с кофемашиной и остановиться на варианте с зарядкой.

Напряжение у зарядки должно быть 5V, но если такой зарядки, подходящей по форм-фактору, у вас нет, то можете взять что-то побольше и просто достать плату из такой китайской зарядки и внедрить её куда-то еще в другой корпус. В моем случае это адаптер на 12V 3A,
что будет многовато для нашей платы зарядки, поэтому выкорчевываем мозги из этого адаптера и подпаиваемся контактами платы зарядки с 220V на 5V к площадке более большого блока.

Когда этот шаг выполнен, то крайний левый контакт и крайний правый контакт с USB нашей зарядки на 5V подпаиваем к + и – нашей платы зарядки 03962A (проверьте мультиметром, перед тем как подпаивать, где у вас +, а где –; хотя даже такое обычно пишут на платках зарядки).

Теперь, когда вы подпаялись к нашей плате зарядки, вы должны подпаять внутренние контакты ± к контактам аккумулятора 3.7V Li-On/Li-Po (зачем это нужно, если у нас зарядка постоянно в розетке? Чтобы, если её вытянули, то хоть какое-то время она продолжала работать; но это ход не обязательный,
и вы можете, вместо того чтобы играться с этим, сразу подпаять контакты зарядки 5V к –/GND, +/+5V на ESP32).

Затем, как вы подпаяли контакты к аккумулятору, нужно подпаять внутренние контакты платы зарядки с маркировкой OUTPUT ± к самой ESP-32 Cam. Вы найдёте на ней много пинов, в том числе VCC, 3V3, 5V, и тут внимательно: если вы пропустили шаг с аккумулятором и платой зарядки аккумулятора и подпаялись сразу к плате зарядки телефона, то вам нужно подключить
5V от зарядки к –/GND, +/+5V на ESP32, а если вы выполнили шаги с платой зарядки аккумулятора, то –/GND, +/+3V3 на ESP32.

Схему того, как это должно выглядеть, приложу. (Надеюсь стало как-то понятнее немного)
1740776476580.png


Теперь, когда у нас готово с этой частью, мы переходим к коду и логике работы этого дела:

Как мы будем получать данные?

ESP-32 создаёт точку доступа с непримечательным названием; я, например, выбрал название случайного роутера от TP-Link с сайта DNS — TP-Link TL-WR841N, но вы можете использовать, само собой, что угодно.
1740776755247.png

(Ну и пароль для подключения к точке тоже можете видеть :zns6:)

1740776149014.png


Вам нужно подключиться к точке доступа и перейти на её локальный адрес 192.168.4.1, и если вы всё сделаете правильно, то увидите своё личико с камеры =)

(Также код сохраняет данные на SD-карту)

https://randomnerdtutorials.com/installing-esp32-arduino-ide-2-0/

Выбираем вашу плату среди подключённых устройств к COM — в моем случае это COM6 — и заливаем туда код ниже, который мне уже банально лень объяснять.

1740776720345.png


C-подобный:
#include <WiFi.h>
#include <WebServer.h>
#include <esp_camera.h>
#include <FS.h>
#include <SPI.h>
#include <SD.h>

const char* AP_SSID = "TP-Link TL-WR841N";
const char* AP_PASS = "1234567891";

#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

#define SD_CS 13
#define SD_MISO 2
#define SD_MOSI 15
#define SD_SCLK 14

#define LED_PIN 4

WebServer server(80);

unsigned long lastFrameTime = 0;
unsigned long videoStartTime = 0;
unsigned long dayLength = 24UL * 3600UL * 1000UL;
String currentFolder = "";
int dayCounter = 0;
unsigned long frameInterval = 2000;

bool cameraInitialized = false;
bool sdInitialized = false;
bool autoDeleteIfFull = false;

sensor_t * s = NULL;

bool initCamera() {
  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 = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_VGA;
  config.jpeg_quality = 10;
  config.fb_count = 2;

  esp_err_t err = esp_camera_init(&config);
  if (err == ESP_OK) {
    s = esp_camera_sensor_get();
    return true;
  } else {
    return false;
  }
}

uint64_t sumDirectorySize(File dir) {
  uint64_t total = 0;
  while (true) {
    File entry = dir.openNextFile();
    if (!entry) break;
    if (entry.isDirectory()) {
      total += sumDirectorySize(entry);
    } else {
      total += entry.size();
    }
    entry.close();
  }
  return total;
}

float getSDUsage() {
  if (!sdInitialized) return 0;
  uint64_t cardSize = SD.cardSize();
  if (cardSize == 0) return 0;
  File root = SD.open("/");
  uint64_t usedSize = sumDirectorySize(root);
  root.close();
  return ((float)usedSize / (float)cardSize) * 100.0;
}

void createNewVideoFolder() {
  currentFolder = "/video_day_" + String(dayCounter);
  if (!SD.exists(currentFolder)) {
    SD.mkdir(currentFolder);
  }
}

void checkDailyRollOver() {
  unsigned long now = millis();
  if (now - videoStartTime >= dayLength) {
    dayCounter++;
    createNewVideoFolder();
    videoStartTime = now;
  }
}

void recordFrame() {
  if (!cameraInitialized || !sdInitialized) return;
  unsigned long now = millis();
  if (now - lastFrameTime >= frameInterval) {
    camera_fb_t * fb = esp_camera_fb_get();
    if (fb) {
      String filename = currentFolder + "/" + String(millis()) + ".jpg";
      File f = SD.open(filename, FILE_WRITE);
      if (f) {
        f.write(fb->buf, fb->len);
        f.close();
      }
      esp_camera_fb_return(fb);
    }
    lastFrameTime = now;
  }
}

void setResolution(framesize_t size) {
  if (s) s->set_framesize(s, size);
}

void setQuality(int quality) {
  if (s) s->set_quality(s, quality);
}

void setPixelFormat(pixformat_t format) {
  if (format == s->pixformat) return;
  esp_camera_deinit();
  camera_config_t cconfig;
  cconfig.ledc_channel = LEDC_CHANNEL_0;
  cconfig.ledc_timer = LEDC_TIMER_0;
  cconfig.pin_d0 = Y2_GPIO_NUM;
  cconfig.pin_d1 = Y3_GPIO_NUM;
  cconfig.pin_d2 = Y4_GPIO_NUM;
  cconfig.pin_d3 = Y5_GPIO_NUM;
  cconfig.pin_d4 = Y6_GPIO_NUM;
  cconfig.pin_d5 = Y7_GPIO_NUM;
  cconfig.pin_d6 = Y8_GPIO_NUM;
  cconfig.pin_d7 = Y9_GPIO_NUM;
  cconfig.pin_xclk = XCLK_GPIO_NUM;
  cconfig.pin_pclk = PCLK_GPIO_NUM;
  cconfig.pin_vsync = VSYNC_GPIO_NUM;
  cconfig.pin_href = HREF_GPIO_NUM;
  cconfig.pin_sscb_sda = SIOD_GPIO_NUM;
  cconfig.pin_sscb_scl = SIOC_GPIO_NUM;
  cconfig.pin_pwdn = PWDN_GPIO_NUM;
  cconfig.pin_reset = RESET_GPIO_NUM;
  cconfig.xclk_freq_hz = 20000000;
  cconfig.pixel_format = format;
  cconfig.frame_size = s->status.framesize;
  cconfig.jpeg_quality = s->status.quality;
  cconfig.fb_count = 2;
  if (esp_camera_init(&cconfig) == ESP_OK) {
    s = esp_camera_sensor_get();
  } else {
    esp_camera_deinit();
    initCamera();
  }
}

void setLED(bool on) {
  digitalWrite(LED_PIN, on ? HIGH : LOW);
}

void cleanupIfNeeded() {
  if (!autoDeleteIfFull || !sdInitialized) return;
  float usage = getSDUsage();
  if (usage < 97.0) return;
  int oldest = 0;
  while (true) {
    String folder = "/video_day_" + String(oldest);
    if (SD.exists(folder)) {
      File dir = SD.open(folder);
      while (true) {
        File entry = dir.openNextFile();
        if (!entry) break;
        String fname = String(entry.name());
        entry.close();
        SD.remove(fname);
      }
      dir.close();
      SD.rmdir(folder);
      usage = getSDUsage();
      if (usage < 97.0) break;
    } else {
      break;
    }
    oldest++;
  }
}

framesize_t framesizeFromString(const String &val) {
  if (val == "qqvga") return FRAMESIZE_QQVGA;
  if (val == "qvga") return FRAMESIZE_QVGA;
  if (val == "vga") return FRAMESIZE_VGA;
  if (val == "svga") return FRAMESIZE_SVGA;
  if (val == "sxga") return FRAMESIZE_SXGA;
  if (val == "uxga") return FRAMESIZE_UXGA;
  return FRAMESIZE_VGA;
}

void handleRoot() {
  String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'>"
                "<meta name='viewport' content='width=device-width, initial-scale=1.0'>"
                "<style>"
                "body { font-family: sans-serif; background: #f0f0f0; color: #333; margin: 20px; }"
                "a.btn { background:#0066cc; color:#fff; padding:10px; border-radius:5px; text-decoration:none; margin:5px; display:inline-block;}"
                "a.btn:hover { background:#005bb5; }"
                "</style>"
                "<title>ESP32-CAM</title></head><body>";
  html += "<h1>ESP32-CAM Наблюдение</h1>";
  if (!cameraInitialized) html += "<p style='color:red;'>Камера не инициализирована!</p>";
  if (!sdInitialized) html += "<p style='color:red;'>SD-карта не инициализирована!</p>";
  html += "<p><a href='/live' class='btn'>Прямой эфир и управление</a></p>";
  html += "<p><a href='/files' class='btn'>Просмотр файлов SD</a></p>";
  html += "<p><a href='/settings' class='btn'>Настройки</a></p>";
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void handleLive() {
  String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'>"
                "<meta name='viewport' content='width=device-width'>"
                "<style>"
                "body { font-family:sans-serif; background:#f0f0f0; color:#333; padding:20px;}"
                "a.btn, button.btn { background:#0066cc; color:#fff; padding:5px 10px; border-radius:5px; text-decoration:none; margin:5px; display:inline-block;}"
                "a.btn:hover, button.btn:hover { background:#005bb5; }"
                "select { margin:5px; }"
                "</style>"
                "<title>Live Stream</title></head><body>";
  html += "<h1>Прямой эфир</h1>";
  if (!cameraInitialized) {
    html += "<p style='color:red;'>Камера не инициализирована!</p>";
  } else {
    html += "<img src='/stream' style='width:100%; max-width:600px; display:block; margin-bottom:20px; border:2px solid #333;'>";
    html += "<h2>Управление камерой</h2>";
    html += "<p><b>Вспышка:</b> ";
    html += "<a href='/control?led=on&return=live' class='btn'>Включить</a> ";
    html += "<a href='/control?led=off&return=live' class='btn'>Выключить</a></p>";
    html += "<p><b>Разрешение:</b> ";
    html += "<a class='btn' href='/control?res=qqvga&return=live'>QQVGA</a>";
    html += "<a class='btn' href='/control?res=qvga&return=live'>QVGA</a>";
    html += "<a class='btn' href='/control?res=vga&return=live'>VGA</a>";
    html += "<a class='btn' href='/control?res=svga&return=live'>SVGA</a>";
    html += "<a class='btn' href='/control?res=sxga&return=live'>SXGA</a>";
    html += "<a class='btn' href='/control?res=uxga&return=live'>UXGA</a></p>";
    html += "<p><b>Качество (JPEG):</b> ";
    html += "<a class='btn' href='/control?quality=5&return=live'>Q=5</a>";
    html += "<a class='btn' href='/control?quality=10&return=live'>Q=10</a>";
    html += "<a class='btn' href='/control?quality=15&return=live'>Q=15</a>";
    html += "<a class='btn' href='/control?quality=30&return=live'>Q=30</a></p>";
    html += "<p><b>Формат пикселей:</b> ";
    html += "<a class='btn' href='/control?format=jpeg&return=live'>JPEG</a></p>";
  }
  html += "<p><a href='/' class='btn'>Главная</a> <a href='/settings' class='btn'>Настройки</a></p>";
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void handleCapture() {
  if (!cameraInitialized) {
    server.send(500, "text/plain", "ЖОПА опять Камера не инициализирована");
    return;
  }
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) {
    server.send(500, "text/plain", "Не удалось получить кадр");
    return;
  }
  server.sendHeader("Content-Type", "image/jpeg");
  server.sendHeader("Content-Length", String(fb->len));
  server.send(200);
  server.client().write((const char*)fb->buf, fb->len);
  esp_camera_fb_return(fb);
}

void handleStream() {
  if (!cameraInitialized) {
    server.send(500, "text/plain", "Камера не инициализирована");
    return;
  }
  WiFiClient client = server.client();
  String response = "HTTP/1.1 200 OK\r\n"
                    "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n"
                    "Connection: keep-alive\r\n"
                    "Access-Control-Allow-Origin: *\r\n"
                    "\r\n";
  client.print(response);
  while (client.connected()) {
    camera_fb_t * fb = esp_camera_fb_get();
    if (!fb) {
      delay(10);
      continue;
    }
    client.print("--frame\r\n");
    client.print("Content-Type: image/jpeg\r\n");
    client.print("Content-Length: " + String(fb->len) + "\r\n\r\n");
    client.write(fb->buf, fb->len);
    client.print("\r\n");
    esp_camera_fb_return(fb);
    delay(30);
  }
}

String getJSONValue(const String &json, const String &key) {
  String searchKey = "\"" + key + "\":";
  int keyPos = json.indexOf(searchKey);
  if (keyPos < 0) return "";
  int start = keyPos + searchKey.length();
  while (start < (int)json.length() && (json[start] == ' ' || json[start] == '\"')) start++;
  int end = start;
  while (end < (int)json.length() && json[end] != '\"' && json[end] != ',' && json[end] != '}') end++;
  return json.substring(start, end);
}

String listFilesJSON(const String &dirname) {
  File dir = SD.open(dirname);
  if (!dir || !dir.isDirectory()) {
    return "[]";
  }
  String output = "[";
  bool first = true;
  while (true) {
    File entry = dir.openNextFile();
    if (!entry) break;
    if (!first) output += ",";
    output += "{\"name\":\"" + String(entry.name()) + "\",\"size\":" + String(entry.size()) + (entry.isDirectory() ? ",\"type\":\"dir\"}" : ",\"type\":\"file\"}");
    first = false;
    entry.close();
  }
  output += "]";
  dir.close();
  return output;
}

void handleFiles() {
  if (!sdInitialized) {
    server.send(500, "text/plain", "SD не инициализирован");
    return;
  }
  String path = server.hasArg("dir") ? server.arg("dir") : "/";
  String fileList = listFilesJSON(path);
  String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'>"
                "<style>"
                "body { font-family:sans-serif; background:#f0f0f0; color:#333; padding:20px;}"
                "a {color:#0066cc;}"
                "ul {list-style:none; padding:0;}"
                "li {margin:5px 0;}"
                "a.btn { background:#0066cc; color:#fff; padding:5px 10px; border-radius:5px; text-decoration:none; margin:5px; }"
                "a.btn:hover { background:#005bb5; }"
                "</style>"
                "<title>File Browser</title></head><body>";
  html += "<h1>Содержимое SD (" + path + ")</h1>";
  html += "<p><a href='?dir=/'>Корень</a></p><ul>";
  int startIndex = 0;
  while (true) {
    int openBrace = fileList.indexOf('{', startIndex);
    if (openBrace < 0) break;
    int closeBrace = fileList.indexOf('}', openBrace);
    if (closeBrace < 0) break;
    String fileObj = fileList.substring(openBrace, closeBrace + 1);
    startIndex = closeBrace + 1;
    String name = getJSONValue(fileObj, "name");
    String type = getJSONValue(fileObj, "type");
    if (type == "dir") {
      html += "<li>[DIR] <a href='?dir=" + name + "'>" + name + "</a></li>";
    } else {
      html += "<li>[FILE] " + name + " ("
              "<a href='/download?file=" + name + "' class='btn'>Скачать</a> "
              "<a href='/delete?file=" + name + "' class='btn' onclick='return confirm(\"Удалить файл?\");'>Удалить</a>)</li>";
    }
  }
  html += "</ul><p><a href='/' class='btn'>На главную</a></p></body></html>";
  server.send(200, "text/html", html);
}

void handleDownload() {
  if (!sdInitialized) {
    server.send(500, "text/plain", "SD не инициализирован");
    return;
  }
  if (!server.hasArg("file")) {
    server.send(400, "text/plain", "Файл не указан");
    return;
  }
  String filename = server.arg("file");
  File file = SD.open(filename, FILE_READ);
  if (!file) {
    server.send(404, "text/plain", "Файл не найден");
    return;
  }
  server.streamFile(file, "application/octet-stream");
  file.close();
}

void handleDelete() {
  if (!sdInitialized) {
    server.send(500, "text/plain", "SD не инициализирован");
    return;
  }
  if (!server.hasArg("file")) {
    server.send(400, "text/plain", "Файл не указан");
    return;
  }
  String filename = server.arg("file");
  if (!SD.exists(filename)) {
    server.send(404, "text/plain", "Файл не найден");
    return;
  }
  SD.remove(filename);
  server.sendHeader("Location", "/files");
  server.send(303);
}

void handleControl() {
  if (!cameraInitialized) {
    server.send(500, "text/plain", "Камера не инициализирована");
    return;
  }
  String retPage = server.hasArg("return") ? server.arg("return") : "";
  String redirect = "/";
  if (retPage == "live") redirect = "/live";
  if (server.hasArg("led")) {
    String val = server.arg("led");
    if (val == "on") setLED(true); else setLED(false);
  }
  if (server.hasArg("res")) {
    framesize_t newSize = framesizeFromString(server.arg("res"));
    setResolution(newSize);
  }
  if (server.hasArg("quality")) {
    int q = server.arg("quality").toInt();
    if (q >= 5 && q <= 63) {
      setQuality(q);
    }
  }
  if (server.hasArg("format")) {
    String f = server.arg("format");
    if (f == "jpeg") setPixelFormat(PIXFORMAT_JPEG);
  }
  server.sendHeader("Location", redirect);
  server.send(303);
}

void handleSettings() {
  String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'>"
                "<style>"
                "body { font-family:sans-serif; background:#f0f0f0; color:#333; padding:20px;}"
                "label {display:block; margin-bottom:10px;}"
                "input[type=checkbox] { margin-right:10px; }"
                "button { background:#0066cc; color:#fff; padding:5px 10px; border:none; border-radius:5px; }"
                "button:hover { background:#005bb5; }"
                "a.btn { background:#0066cc; color:#fff; padding:5px 10px; border-radius:5px; text-decoration:none; }"
                "a.btn:hover { background:#005bb5; }"
                "</style>"
                "<title>Настройки</title></head><body>";
  html += "<h1>Настройки</h1>";
  html += "<form method='POST' action='/settings'>";
  html += "<label><input type='checkbox' name='autodelete' " + String(autoDeleteIfFull ? "checked" : "") + ">Авто-удаление старых записей, если заполненность >97%</label>";
  html += "<button type='submit'>Сохранить</button>";
  html += "</form>";
  html += "<p><a href='/' class='btn'>На главную</a> <a href='/live' class='btn'>Прямой эфир</a></p>";
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void handleSettingsPost() {
  if (server.hasArg("autodelete")) {
    autoDeleteIfFull = true;
  } else {
    autoDeleteIfFull = false;
  }
  server.sendHeader("Location", "/settings");
  server.send(303);
}

void setup() {
  Serial.begin(115200);
  cameraInitialized = initCamera();
  if (!cameraInitialized) {
    Serial.println("Ошибка инициализации камеры");
  }
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  SPI.begin(SD_SCLK, SD_MISO, SD_MOSI, SD_CS);
  sdInitialized = SD.begin(SD_CS);
  if (!sdInitialized) {
    Serial.println("Ошибка инициализации SD :/");
  } else {
    createNewVideoFolder();
    videoStartTime = millis();
  }
  WiFi.softAP(AP_SSID, AP_PASS);
  Serial.print("AP SSID: ");
  Serial.println(AP_SSID);
  Serial.print("IP: ");
  Serial.println(WiFi.softAPIP());
  server.on("/", handleRoot);
  server.on("/live", handleLive);
  server.on("/stream", handleStream);
  server.on("/files", handleFiles);
  server.on("/download", handleDownload);
  server.on("/delete", handleDelete);
  server.on("/capture", handleCapture);
  server.on("/control", handleControl);
  server.on("/settings", HTTP_GET, handleSettings);
  server.on("/settings", HTTP_POST, handleSettingsPost);
  server.begin();
  Serial.println("сервер запущен");
}

void loop() {
  server.handleClient();
  checkDailyRollOver();
  recordFrame();
  cleanupIfNeeded();
}
1740776299087.png

Вот такую красоту мы получаем по итогу :smile10:

А еще я когда разбирал зарядку от купертиновцев чтобы глянуть выйдет ли туда вставить камеру я столкнулся с забавной штукой:
1740775796717.png
1740775842324.png

Почти 30 грамм этой зарядки от Apple это бесполезный утяжелитель когда сама платка весит 13 грамм =)


Надеюсь, вам понравилась моя статейка =)

На любые вопросы с радостью отвечу, замечания приму к сведению – всё же в этом разделе я впервые что-то выкладываю, так как в основном обитаю в разделе Malware.
 

Вложения

  • 1740777311141.png
    1740777311141.png
    568.5 КБ · Просмотры: 70
  • 1740777334866.png
    1740777334866.png
    568.5 КБ · Просмотры: 65
Линк на скачивание кода в .ino =)
 
А еще я когда разбирал зарядку от купертиновцев чтобы глянуть выйдет ли туда вставить камеру я столкнулся с забавной штукой:
:oops: это точно не китайская подделка?

есть фото готовой зарядки с камерой внутри?
 
:oops: это точно не китайская подделка?

есть фото готовой зарядки с камерой внутри?
Неа не подделка :)Сам удивился когда такое веселье увидел
Фоточку кину как буду дома готовой зарядки
 
: smile10: Всем привет

У меня это первая статья для этого раздела, и решил я сделать безделушку, при помощи которой вы сможете подсмотреть что-то, что вам нужно, и подслушать, что вам нужно.

Идея такова: чтобы сделать скрытую камеру с возможностью накатывать свою прошивку, какие-то свои изменения добавлять и чтобы она была скрытной. С этим есть небольшие проблемы, но я дам вам пару идей, как это можно сделать.

Для этого нам понадобиться установить Arduino IDE

Посмотреть вложение 104544

— в моем случае это версия 2.3.2 — и установить туда поддержку плат ESP.

Что потребуется из компонентов?



Собственно, сама ESP-32 Cam. Я буду лично использовать Ai Thinker ESP-32 Cam, более подробно по ней можете почитать тут –

https://github.com/prusa3d/Prusa-Firmware-ESP32-Cam/blob/master/doc/AI_Thinker-ESP32-cam/README.md

Посмотреть вложение 104543

Также, в необязательных компонентах — какая-то платка зарядки по типу 03962A (как в моем примере) / TP4065 и аккумулятор для неё, компактный Li-On (в примере у меня Li-Po, так как ничего компактного, кроме как его, не нашел), и если будете скрывать камеру, как я в статейке, то — какой-то дешевый блок питания для телефона, который будет не жалко.
Приступим к самому разбору того, что нам нужно сделать =)

Начну для начала с того, какие идеи о том, где это можно скрыть, ко мне приходили, и почему я пришел к такому выбору.

В первую очередь нужно понимать, что наша цель — это скрыть нашу камеру, но в тот же момент нужно понимать, что она долго не проживёт на своём аккумуляторе, ну, например, в какой-то изолированной банке с дыркой для камеры. Поэтому нам нужен какой-то источник электричества, который будет постоянно питать наше чудо и
давать возможность вести наблюдение. Из тех вариантов, что мне пришли в голову, — это использовать зарядку для смартфона, так как то, что она в розетке, постоянно никого не смутит. Ну, и ещё были такие варианты, как различные другие приборы, например, кофемашинка (у меня такая модель, куда можно было бы впихнуть легко камеру, но не думаю, что вариант такой уж универсальный).

Решил не выдумывать велосипед с кофемашиной и остановиться на варианте с зарядкой.

Напряжение у зарядки должно быть 5V, но если такой зарядки, подходящей по форм-фактору, у вас нет, то можете взять что-то побольше и просто достать плату из такой китайской зарядки и внедрить её куда-то еще в другой корпус. В моем случае это адаптер на 12V 3A,
что будет многовато для нашей платы зарядки, поэтому выкорчевываем мозги из этого адаптера и подпаиваемся контактами платы зарядки с 220V на 5V к площадке более большого блока.

Когда этот шаг выполнен, то крайний левый контакт и крайний правый контакт с USB нашей зарядки на 5V подпаиваем к + и – нашей платы зарядки 03962A (проверьте мультиметром, перед тем как подпаивать, где у вас +, а где –; хотя даже такое обычно пишут на платках зарядки).

Теперь, когда вы подпаялись к нашей плате зарядки, вы должны подпаять внутренние контакты ± к контактам аккумулятора 3.7V Li-On/Li-Po (зачем это нужно, если у нас зарядка постоянно в розетке? Чтобы, если её вытянули, то хоть какое-то время она продолжала работать; но это ход не обязательный,
и вы можете, вместо того чтобы играться с этим, сразу подпаять контакты зарядки 5V к –/GND, +/+5V на ESP32).

Затем, как вы подпаяли контакты к аккумулятору, нужно подпаять внутренние контакты платы зарядки с маркировкой OUTPUT ± к самой ESP-32 Cam. Вы найдёте на ней много пинов, в том числе VCC, 3V3, 5V, и тут внимательно: если вы пропустили шаг с аккумулятором и платой зарядки аккумулятора и подпаялись сразу к плате зарядки телефона, то вам нужно подключить
5V от зарядки к –/GND, +/+5V на ESP32, а если вы выполнили шаги с платой зарядки аккумулятора, то –/GND, +/+3V3 на ESP32.

Схему того, как это должно выглядеть, приложу. (Надеюсь стало как-то понятнее немного)
Посмотреть вложение 104540

Теперь, когда у нас готово с этой частью, мы переходим к коду и логике работы этого дела:

Как мы будем получать данные?

ESP-32 создаёт точку доступа с непримечательным названием; я, например, выбрал название случайного роутера от TP-Link с сайта DNS — TP-Link TL-WR841N, но вы можете использовать, само собой, что угодно.
Посмотреть вложение 104542
(Ну и пароль для подключения к точке тоже можете видеть : zns6:)



Вам нужно подключиться к точке доступа и перейти на её локальный адрес 192.168.4.1, и если вы всё сделаете правильно, то увидите своё личико с камеры =)

(Также код сохраняет данные на SD-карту)

https://randomnerdtutorials.com/installing-esp32-arduino-ide-2-0/

Выбираем вашу плату среди подключённых устройств к COM — в моем случае это COM6 — и заливаем туда код ниже, который мне уже банально лень объяснять.

Посмотреть вложение 104541

C-подобный:
#include <WiFi.h>
#include <WebServer.h>
#include <esp_camera.h>
#include <FS.h>
#include <SPI.h>
#include <SD.h>

const char* AP_SSID = "TP-Link TL-WR841N";
const char* AP_PASS = "1234567891";

#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

#define SD_CS 13
#define SD_MISO 2
#define SD_MOSI 15
#define SD_SCLK 14

#define LED_PIN 4

WebServer server(80);

unsigned long lastFrameTime = 0;
unsigned long videoStartTime = 0;
unsigned long dayLength = 24UL * 3600UL * 1000UL;
String currentFolder = "";
int dayCounter = 0;
unsigned long frameInterval = 2000;

bool cameraInitialized = false;
bool sdInitialized = false;
bool autoDeleteIfFull = false;

sensor_t * s = NULL;

bool initCamera() {
  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 = 20000000;
  config.pixel_format = PIXFORMAT_JPEG;
  config.frame_size = FRAMESIZE_VGA;
  config.jpeg_quality = 10;
  config.fb_count = 2;

  esp_err_t err = esp_camera_init(&config);
  if (err == ESP_OK) {
    s = esp_camera_sensor_get();
    return true;
  } else {
    return false;
  }
}

uint64_t sumDirectorySize(File dir) {
  uint64_t total = 0;
  while (true) {
    File entry = dir.openNextFile();
    if (!entry) break;
    if (entry.isDirectory()) {
      total += sumDirectorySize(entry);
    } else {
      total += entry.size();
    }
    entry.close();
  }
  return total;
}

float getSDUsage() {
  if (!sdInitialized) return 0;
  uint64_t cardSize = SD.cardSize();
  if (cardSize == 0) return 0;
  File root = SD.open("/");
  uint64_t usedSize = sumDirectorySize(root);
  root.close();
  return ((float)usedSize / (float)cardSize) * 100.0;
}

void createNewVideoFolder() {
  currentFolder = "/video_day_" + String(dayCounter);
  if (!SD.exists(currentFolder)) {
    SD.mkdir(currentFolder);
  }
}

void checkDailyRollOver() {
  unsigned long now = millis();
  if (now - videoStartTime >= dayLength) {
    dayCounter++;
    createNewVideoFolder();
    videoStartTime = now;
  }
}

void recordFrame() {
  if (!cameraInitialized || !sdInitialized) return;
  unsigned long now = millis();
  if (now - lastFrameTime >= frameInterval) {
    camera_fb_t * fb = esp_camera_fb_get();
    if (fb) {
      String filename = currentFolder + "/" + String(millis()) + ".jpg";
      File f = SD.open(filename, FILE_WRITE);
      if (f) {
        f.write(fb->buf, fb->len);
        f.close();
      }
      esp_camera_fb_return(fb);
    }
    lastFrameTime = now;
  }
}

void setResolution(framesize_t size) {
  if (s) s->set_framesize(s, size);
}

void setQuality(int quality) {
  if (s) s->set_quality(s, quality);
}

void setPixelFormat(pixformat_t format) {
  if (format == s->pixformat) return;
  esp_camera_deinit();
  camera_config_t cconfig;
  cconfig.ledc_channel = LEDC_CHANNEL_0;
  cconfig.ledc_timer = LEDC_TIMER_0;
  cconfig.pin_d0 = Y2_GPIO_NUM;
  cconfig.pin_d1 = Y3_GPIO_NUM;
  cconfig.pin_d2 = Y4_GPIO_NUM;
  cconfig.pin_d3 = Y5_GPIO_NUM;
  cconfig.pin_d4 = Y6_GPIO_NUM;
  cconfig.pin_d5 = Y7_GPIO_NUM;
  cconfig.pin_d6 = Y8_GPIO_NUM;
  cconfig.pin_d7 = Y9_GPIO_NUM;
  cconfig.pin_xclk = XCLK_GPIO_NUM;
  cconfig.pin_pclk = PCLK_GPIO_NUM;
  cconfig.pin_vsync = VSYNC_GPIO_NUM;
  cconfig.pin_href = HREF_GPIO_NUM;
  cconfig.pin_sscb_sda = SIOD_GPIO_NUM;
  cconfig.pin_sscb_scl = SIOC_GPIO_NUM;
  cconfig.pin_pwdn = PWDN_GPIO_NUM;
  cconfig.pin_reset = RESET_GPIO_NUM;
  cconfig.xclk_freq_hz = 20000000;
  cconfig.pixel_format = format;
  cconfig.frame_size = s->status.framesize;
  cconfig.jpeg_quality = s->status.quality;
  cconfig.fb_count = 2;
  if (esp_camera_init(&cconfig) == ESP_OK) {
    s = esp_camera_sensor_get();
  } else {
    esp_camera_deinit();
    initCamera();
  }
}

void setLED(bool on) {
  digitalWrite(LED_PIN, on ? HIGH : LOW);
}

void cleanupIfNeeded() {
  if (!autoDeleteIfFull || !sdInitialized) return;
  float usage = getSDUsage();
  if (usage < 97.0) return;
  int oldest = 0;
  while (true) {
    String folder = "/video_day_" + String(oldest);
    if (SD.exists(folder)) {
      File dir = SD.open(folder);
      while (true) {
        File entry = dir.openNextFile();
        if (!entry) break;
        String fname = String(entry.name());
        entry.close();
        SD.remove(fname);
      }
      dir.close();
      SD.rmdir(folder);
      usage = getSDUsage();
      if (usage < 97.0) break;
    } else {
      break;
    }
    oldest++;
  }
}

framesize_t framesizeFromString(const String &val) {
  if (val == "qqvga") return FRAMESIZE_QQVGA;
  if (val == "qvga") return FRAMESIZE_QVGA;
  if (val == "vga") return FRAMESIZE_VGA;
  if (val == "svga") return FRAMESIZE_SVGA;
  if (val == "sxga") return FRAMESIZE_SXGA;
  if (val == "uxga") return FRAMESIZE_UXGA;
  return FRAMESIZE_VGA;
}

void handleRoot() {
  String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'>"
                "<meta name='viewport' content='width=device-width, initial-scale=1.0'>"
                "<style>"
                "body { font-family: sans-serif; background: #f0f0f0; color: #333; margin: 20px; }"
                "a.btn { background:#0066cc; color:#fff; padding:10px; border-radius:5px; text-decoration:none; margin:5px; display:inline-block;}"
                "a.btn:hover { background:#005bb5; }"
                "</style>"
                "<title>ESP32-CAM</title></head><body>";
  html += "<h1>ESP32-CAM Наблюдение</h1>";
  if (!cameraInitialized) html += "<p style='color:red;'>Камера не инициализирована!</p>";
  if (!sdInitialized) html += "<p style='color:red;'>SD-карта не инициализирована!</p>";
  html += "<p><a href='/live' class='btn'>Прямой эфир и управление</a></p>";
  html += "<p><a href='/files' class='btn'>Просмотр файлов SD</a></p>";
  html += "<p><a href='/settings' class='btn'>Настройки</a></p>";
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void handleLive() {
  String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'>"
                "<meta name='viewport' content='width=device-width'>"
                "<style>"
                "body { font-family:sans-serif; background:#f0f0f0; color:#333; padding:20px;}"
                "a.btn, button.btn { background:#0066cc; color:#fff; padding:5px 10px; border-radius:5px; text-decoration:none; margin:5px; display:inline-block;}"
                "a.btn:hover, button.btn:hover { background:#005bb5; }"
                "select { margin:5px; }"
                "</style>"
                "<title>Live Stream</title></head><body>";
  html += "<h1>Прямой эфир</h1>";
  if (!cameraInitialized) {
    html += "<p style='color:red;'>Камера не инициализирована!</p>";
  } else {
    html += "<img src='/stream' style='width:100%; max-width:600px; display:block; margin-bottom:20px; border:2px solid #333;'>";
    html += "<h2>Управление камерой</h2>";
    html += "<p><b>Вспышка:</b> ";
    html += "<a href='/control?led=on&return=live' class='btn'>Включить</a> ";
    html += "<a href='/control?led=off&return=live' class='btn'>Выключить</a></p>";
    html += "<p><b>Разрешение:</b> ";
    html += "<a class='btn' href='/control?res=qqvga&return=live'>QQVGA</a>";
    html += "<a class='btn' href='/control?res=qvga&return=live'>QVGA</a>";
    html += "<a class='btn' href='/control?res=vga&return=live'>VGA</a>";
    html += "<a class='btn' href='/control?res=svga&return=live'>SVGA</a>";
    html += "<a class='btn' href='/control?res=sxga&return=live'>SXGA</a>";
    html += "<a class='btn' href='/control?res=uxga&return=live'>UXGA</a></p>";
    html += "<p><b>Качество (JPEG):</b> ";
    html += "<a class='btn' href='/control?quality=5&return=live'>Q=5</a>";
    html += "<a class='btn' href='/control?quality=10&return=live'>Q=10</a>";
    html += "<a class='btn' href='/control?quality=15&return=live'>Q=15</a>";
    html += "<a class='btn' href='/control?quality=30&return=live'>Q=30</a></p>";
    html += "<p><b>Формат пикселей:</b> ";
    html += "<a class='btn' href='/control?format=jpeg&return=live'>JPEG</a></p>";
  }
  html += "<p><a href='/' class='btn'>Главная</a> <a href='/settings' class='btn'>Настройки</a></p>";
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void handleCapture() {
  if (!cameraInitialized) {
    server.send(500, "text/plain", "ЖОПА опять Камера не инициализирована");
    return;
  }
  camera_fb_t * fb = esp_camera_fb_get();
  if (!fb) {
    server.send(500, "text/plain", "Не удалось получить кадр");
    return;
  }
  server.sendHeader("Content-Type", "image/jpeg");
  server.sendHeader("Content-Length", String(fb->len));
  server.send(200);
  server.client().write((const char*)fb->buf, fb->len);
  esp_camera_fb_return(fb);
}

void handleStream() {
  if (!cameraInitialized) {
    server.send(500, "text/plain", "Камера не инициализирована");
    return;
  }
  WiFiClient client = server.client();
  String response = "HTTP/1.1 200 OK\r\n"
                    "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n"
                    "Connection: keep-alive\r\n"
                    "Access-Control-Allow-Origin: *\r\n"
                    "\r\n";
  client.print(response);
  while (client.connected()) {
    camera_fb_t * fb = esp_camera_fb_get();
    if (!fb) {
      delay(10);
      continue;
    }
    client.print("--frame\r\n");
    client.print("Content-Type: image/jpeg\r\n");
    client.print("Content-Length: " + String(fb->len) + "\r\n\r\n");
    client.write(fb->buf, fb->len);
    client.print("\r\n");
    esp_camera_fb_return(fb);
    delay(30);
  }
}

String getJSONValue(const String &json, const String &key) {
  String searchKey = "\"" + key + "\":";
  int keyPos = json.indexOf(searchKey);
  if (keyPos < 0) return "";
  int start = keyPos + searchKey.length();
  while (start < (int)json.length() && (json[start] == ' ' || json[start] == '\"')) start++;
  int end = start;
  while (end < (int)json.length() && json[end] != '\"' && json[end] != ',' && json[end] != '}') end++;
  return json.substring(start, end);
}

String listFilesJSON(const String &dirname) {
  File dir = SD.open(dirname);
  if (!dir || !dir.isDirectory()) {
    return "[]";
  }
  String output = "[";
  bool first = true;
  while (true) {
    File entry = dir.openNextFile();
    if (!entry) break;
    if (!first) output += ",";
    output += "{\"name\":\"" + String(entry.name()) + "\",\"size\":" + String(entry.size()) + (entry.isDirectory() ? ",\"type\":\"dir\"}" : ",\"type\":\"file\"}");
    first = false;
    entry.close();
  }
  output += "]";
  dir.close();
  return output;
}

void handleFiles() {
  if (!sdInitialized) {
    server.send(500, "text/plain", "SD не инициализирован");
    return;
  }
  String path = server.hasArg("dir") ? server.arg("dir") : "/";
  String fileList = listFilesJSON(path);
  String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'>"
                "<style>"
                "body { font-family:sans-serif; background:#f0f0f0; color:#333; padding:20px;}"
                "a {color:#0066cc;}"
                "ul {list-style:none; padding:0;}"
                "li {margin:5px 0;}"
                "a.btn { background:#0066cc; color:#fff; padding:5px 10px; border-radius:5px; text-decoration:none; margin:5px; }"
                "a.btn:hover { background:#005bb5; }"
                "</style>"
                "<title>File Browser</title></head><body>";
  html += "<h1>Содержимое SD (" + path + ")</h1>";
  html += "<p><a href='?dir=/'>Корень</a></p><ul>";
  int startIndex = 0;
  while (true) {
    int openBrace = fileList.indexOf('{', startIndex);
    if (openBrace < 0) break;
    int closeBrace = fileList.indexOf('}', openBrace);
    if (closeBrace < 0) break;
    String fileObj = fileList.substring(openBrace, closeBrace + 1);
    startIndex = closeBrace + 1;
    String name = getJSONValue(fileObj, "name");
    String type = getJSONValue(fileObj, "type");
    if (type == "dir") {
      html += "<li>[DIR] <a href='?dir=" + name + "'>" + name + "</a></li>";
    } else {
      html += "<li>[FILE] " + name + " ("
              "<a href='/download?file=" + name + "' class='btn'>Скачать</a> "
              "<a href='/delete?file=" + name + "' class='btn' onclick='return confirm(\"Удалить файл?\");'>Удалить</a>)</li>";
    }
  }
  html += "</ul><p><a href='/' class='btn'>На главную</a></p></body></html>";
  server.send(200, "text/html", html);
}

void handleDownload() {
  if (!sdInitialized) {
    server.send(500, "text/plain", "SD не инициализирован");
    return;
  }
  if (!server.hasArg("file")) {
    server.send(400, "text/plain", "Файл не указан");
    return;
  }
  String filename = server.arg("file");
  File file = SD.open(filename, FILE_READ);
  if (!file) {
    server.send(404, "text/plain", "Файл не найден");
    return;
  }
  server.streamFile(file, "application/octet-stream");
  file.close();
}

void handleDelete() {
  if (!sdInitialized) {
    server.send(500, "text/plain", "SD не инициализирован");
    return;
  }
  if (!server.hasArg("file")) {
    server.send(400, "text/plain", "Файл не указан");
    return;
  }
  String filename = server.arg("file");
  if (!SD.exists(filename)) {
    server.send(404, "text/plain", "Файл не найден");
    return;
  }
  SD.remove(filename);
  server.sendHeader("Location", "/files");
  server.send(303);
}

void handleControl() {
  if (!cameraInitialized) {
    server.send(500, "text/plain", "Камера не инициализирована");
    return;
  }
  String retPage = server.hasArg("return") ? server.arg("return") : "";
  String redirect = "/";
  if (retPage == "live") redirect = "/live";
  if (server.hasArg("led")) {
    String val = server.arg("led");
    if (val == "on") setLED(true); else setLED(false);
  }
  if (server.hasArg("res")) {
    framesize_t newSize = framesizeFromString(server.arg("res"));
    setResolution(newSize);
  }
  if (server.hasArg("quality")) {
    int q = server.arg("quality").toInt();
    if (q >= 5 && q <= 63) {
      setQuality(q);
    }
  }
  if (server.hasArg("format")) {
    String f = server.arg("format");
    if (f == "jpeg") setPixelFormat(PIXFORMAT_JPEG);
  }
  server.sendHeader("Location", redirect);
  server.send(303);
}

void handleSettings() {
  String html = "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'>"
                "<style>"
                "body { font-family:sans-serif; background:#f0f0f0; color:#333; padding:20px;}"
                "label {display:block; margin-bottom:10px;}"
                "input[type=checkbox] { margin-right:10px; }"
                "button { background:#0066cc; color:#fff; padding:5px 10px; border:none; border-radius:5px; }"
                "button:hover { background:#005bb5; }"
                "a.btn { background:#0066cc; color:#fff; padding:5px 10px; border-radius:5px; text-decoration:none; }"
                "a.btn:hover { background:#005bb5; }"
                "</style>"
                "<title>Настройки</title></head><body>";
  html += "<h1>Настройки</h1>";
  html += "<form method='POST' action='/settings'>";
  html += "<label><input type='checkbox' name='autodelete' " + String(autoDeleteIfFull ? "checked" : "") + ">Авто-удаление старых записей, если заполненность >97%</label>";
  html += "<button type='submit'>Сохранить</button>";
  html += "</form>";
  html += "<p><a href='/' class='btn'>На главную</a> <a href='/live' class='btn'>Прямой эфир</a></p>";
  html += "</body></html>";
  server.send(200, "text/html", html);
}

void handleSettingsPost() {
  if (server.hasArg("autodelete")) {
    autoDeleteIfFull = true;
  } else {
    autoDeleteIfFull = false;
  }
  server.sendHeader("Location", "/settings");
  server.send(303);
}

void setup() {
  Serial.begin(115200);
  cameraInitialized = initCamera();
  if (!cameraInitialized) {
    Serial.println("Ошибка инициализации камеры");
  }
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);
  SPI.begin(SD_SCLK, SD_MISO, SD_MOSI, SD_CS);
  sdInitialized = SD.begin(SD_CS);
  if (!sdInitialized) {
    Serial.println("Ошибка инициализации SD :/");
  } else {
    createNewVideoFolder();
    videoStartTime = millis();
  }
  WiFi.softAP(AP_SSID, AP_PASS);
  Serial.print("AP SSID: ");
  Serial.println(AP_SSID);
  Serial.print ("IP:");
  Serial.println (WiFi.softAPIP ());
  server.on ("/", handleRoot);
  server.on ("/ live", handleLive);
  server.on ("/ stream", handleStream);
  server.on ("/ files", handleFiles);
  server.on ("/ download", handleDownload);
  server.on ("/ delete", handleDelete);
  server.on ("/ capture", handleCapture);
  server.on ("/ control", handleControl);
  server.on ("/ settings", HTTP_GET, handleSettings);
  server.on ("/ settings", HTTP_POST, handleSettingsPost);
  server.begin ();
  Serial.println ("server running");
}

void loop () {
  server.handleClient ();
  checkDailyRollOver ();
  recordFrame ();
  cleanupIfNeeded ();
}
 [/ CODE]
[ATTACH type="full" width="578px" alt="1740776299087.png"]104539[/ATTACH]
We get such beauty from the end : smile10:

[SPOILER = "Cupertin genius"]
And when I took apart the charge from the merchants to see if the camera would go there, I came across a funny thing:
[ATTACH type="full" width="631px" alt="1740775796717.png"]104535[/ATTACH][ATTACH type="full" width="631px" alt="1740775842324.png"]104536[/ATTACH]
Almost 30 grams of this charge from Apple is a useless weightlifter when the scarf itself weighs 13 grams =)
[/ SPOILER]


[B]I hope you enjoyed my article =)[/B]

I’ll be happy to answer any questions, I’ll take note of – yet in this section for the first time I post something, since I mainly live in the Malware section.
[/QUOTE]
i guess you're also active in Hackaday forum, right?
 
:oops: это точно не китайская подделка?
Это высокотехнологичный нанобрусок
1741693680018.png
 
Это высокотехнологичный нанобрусок
Посмотреть вложение 104974
Походу у епл настолько дела плохи что они стали в официальных магазинах продавать подделки зарядок.
Жадные капиталисты купертиновцы
 
Надеюсь, вам понравилась моя статейка =)
статья интересная. но, не совсем понял, как камера будет вести наблюдение через полностью закрытый корпус? суть то в "камере в зарядке", а не камере на базе зарядки как я понял
 
статья интересная. но, не совсем понял, как камера будет вести наблюдение через полностью закрытый корпус? суть то в "камере в зарядке", а не камере на базе зарядки как я понял
у себя я сделал увеличив прорезь для провода внизу блока питания и вставив камеру туда =} но вообще можно засунуть много куда это так для примера что было
 
сама идея поселения в БП хорошая. т.е. встроеная батарея которая периодически запитывается от сети.
Можно хоть в дырку розетки=} Фантазия и наличие питания для камеры
 
Осветительные приборы- люстры, торшеры и тп, да хоть чайники.. И при сииильном желании даже холодильник сгодицо под такое безобразие))
It's insane bro, you may have looked at articles in Hack@day website, they have manipulated all kinds of hardware and devices!
 
Кулибин спаял хороший жучок, интересная статья!)
По целям для инсталяции больше подойдут - электронные настенные часы, крупная бытовая техника (идеально кондиционер), датчики движения от сигнализации (часто есть в каждой комнате).
А так же, интересно было бы почитать, может будет интересно тебе такое реализовать:
Снаружи дома/из соседней кв. находится проводка в стене, меряется точная толщина стены, сверлится маленькое отверстие и помещается жук с прослушкой, выход в помещение идет оооочень маленьким но мощным динамиком, паз для которого сверлится оооочень маленьким сверлом) а энергию черпает напрямую с проводки, в жуке и сбор и передача.
Такие штуки от 800-1500$ в омерике, а у нас их частники заберут как спец средства и за большую сумму.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх