ESP32-Cam PTZ Control, WiFi Config dan Video Streaming March 30, 2025 by

ESP32-Cam yang dapat Mengontrol 2Servo (PTZ atau Pan & Tilt), konfigurasi WiFi via webserver dan Streaming Video via web

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <ESP32Servo.h>
#include <EEPROM.h>
#include "esp_camera.h"
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "fb_gfx.h"
#include "soc/soc.h" //disable brownout problems
#include "soc/rtc_cntl_reg.h"  //disable brownout problems
#include "esp_http_server.h"

// Pin definitions for ESP32-CAM
#define FLASH_GPIO_NUM 4
#define PAN_PIN 12    // Changed to GPIO12
#define TILT_PIN 2    // Changed to GPIO2

// EEPROM configuration
#define EEPROM_SIZE 512
#define EEPROM_START 0

// Stream content type
#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";

// Structure to store WiFi settings
typedef struct {
  char sta_ssid[32];
  char sta_password[64];
  char ap_ssid[32];
  char ap_password[64];
  uint32_t sta_ip;
  uint32_t sta_gateway;
  uint32_t sta_subnet;
  uint32_t ap_ip;
  uint32_t ap_gateway;
  uint32_t ap_subnet;
  uint8_t mode; // 0 = STA, 1 = AP, 2 = STA+AP
} WiFiConfig;

// Camera configuration
#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

// Global variables
WebServer server(80);
WiFiConfig wifiConfig;
Servo panServo;
Servo tiltServo;
int panAngle = 90;
int tiltAngle = 90;
bool vertical_flip = false;
bool horizontal_flip = false;
httpd_handle_t stream_httpd = NULL;

// HTML content (same as in your original enhanced code)
const char* wifiConfigPage = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>ESP32-CAM WiFi Config</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.container { max-width: 600px; margin: 0 auto; }
.tab { overflow: hidden; border: 1px solid #ccc; background-color: #f1f1f1; }
.tab button { background-color: inherit; float: left; border: none; outline: none; cursor: pointer; padding: 10px 16px; transition: 0.3s; }
.tab button:hover { background-color: #ddd; }
.tab button.active { background-color: #ccc; }
.tabcontent { display: none; padding: 12px; border: 1px solid #ccc; border-top: none; }
input, select { width: 100%; padding: 8px; margin: 5px 0 15px 0; display: inline-block; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
button { background-color: #4CAF50; color: white; padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background-color: #45a049; }
.status { margin-top: 20px; padding: 10px; background-color: #f8f8f8; border: 1px solid #ddd; }
#scanStatus { margin-left: 10px; color: #666; font-style: italic; }
</style>
</head>
<body>
<div class="container">
  <h1>ESP32-CAM WiFi Configuration</h1>

  <div class="status">
    <h3>Current Status</h3>
    %STATUS%
  </div>

  <div class="tab">
    <button class="tablinks active" onclick="openTab(event, 'sta')">STA Mode</button>
    <button class="tablinks" onclick="openTab(event, 'ap')">AP Mode</button>
    <button class="tablinks" onclick="openTab(event, 'reboot')">System</button>
  </div>

  <div id="sta" class="tabcontent" style="display:block;">
    <h2>STA (Client) Configuration</h2>
    <form action="/save" method="post">
      <input type="hidden" name="mode" value="0">

      <label for="sta_ssid">WiFi Network:</label>
      <input type="text" id="sta_ssid" name="sta_ssid" placeholder="Your ssid"><br>

      <label for="sta_password">Password:</label>
      <input type="password" id="sta_password" name="sta_password" placeholder="Leave empty if no password"><br>

      <h3>Custom IP Settings (Optional)</h3>
      <label for="sta_ip">Static IP:</label>
      <input type="text" id="sta_ip" name="sta_ip" placeholder="e.g., 192.168.1.100"><br>

      <label for="sta_gateway">Gateway:</label>
      <input type="text" id="sta_gateway" name="sta_gateway" placeholder="e.g., 192.168.1.1"><br>

      <label for="sta_subnet">Subnet Mask:</label>
      <input type="text" id="sta_subnet" name="sta_subnet" placeholder="e.g., 255.255.255.0"><br>

      <button type="submit">Save STA Configuration</button>
    </form>
  </div>

  <div id="ap" class="tabcontent">
    <h2>AP (Access Point) Configuration</h2>
    <form action="/save" method="post">
      <input type="hidden" name="mode" value="1">

      <label for="ap_ssid">SSID:</label>
      <input type="text" id="ap_ssid" name="ap_ssid" value="%AP_SSID%" required><br>

      <label for="ap_password">Password (min 8 chars):</label>
      <input type="password" id="ap_password" name="ap_password" value="%AP_PASSWORD%" minlength="8"><br>

      <h3>IP Settings</h3>
      <label for="ap_ip">IP Address:</label>
      <input type="text" id="ap_ip" name="ap_ip" value="%AP_IP%" required><br>

      <label for="ap_gateway">Gateway:</label>
      <input type="text" id="ap_gateway" name="ap_gateway" value="%AP_GATEWAY%" required><br>

      <label for="ap_subnet">Subnet Mask:</label>
      <input type="text" id="ap_subnet" name="ap_subnet" value="%AP_SUBNET%" required><br>

      <button type="submit">Save AP Configuration</button>
    </form>
  </div>

  <div id="reboot" class="tabcontent">
    <h2>System Operations</h2>
    <form action="/reboot" method="post">
      <button type="submit">Reboot Device</button>
    </form>
  </div>
</div>

<script>
function openTab(evt, tabName) {
  var i, tabcontent, tablinks;
  tabcontent = document.getElementsByClassName("tabcontent");
  for (i = 0; i < tabcontent.length; i++) {
    tabcontent[i].style.display = "none";
  }
  tablinks = document.getElementsByClassName("tablinks");
  for (i = 0; i < tablinks.length; i++) {
    tablinks[i].className = tablinks[i].className.replace(" active", "");
  }
  document.getElementById(tabName).style.display = "block";
  evt.currentTarget.className += " active";
}

function scanNetworks() {
  const select = document.getElementById("sta_ssid");
  const scanButton = document.getElementById("scanButton");
  const scanStatus = document.getElementById("scanStatus");
  
  select.innerHTML = '<option value="">Scanning...</option>';
  scanButton.disabled = true;
  scanStatus.textContent = "Scanning networks...";
  
  fetch('/scan')
    .then(response => {
      if (!response.ok) throw new Error('Network response was not ok');
      return response.json();
    })
    .then(data => {
      select.innerHTML = '<option value="">-- Select Network --</option>';
      
      if (data.error) {
        select.innerHTML = `<option value="">${data.error}</option>`;
        scanStatus.textContent = data.error;
        return;
      }
      
      if (data.length === 0) {
        select.innerHTML = '<option value="">No networks found</option>';
        scanStatus.textContent = "No networks found";
        return;
      }
      
      data.forEach(network => {
        const option = document.createElement("option");
        option.value = network.ssid;
        option.text = `${network.ssid} (${network.rssi} dBm${network.encryption ? ", Secured" : ", Open"})`;
        if (network.ssid == "%STA_SSID%") option.selected = true;
        select.appendChild(option);
      });
      
      scanStatus.textContent = `Found ${data.length} networks`;
    })
    .catch(error => {
      console.error('Error:', error);
      select.innerHTML = '<option value="">Scan failed</option>';
      scanStatus.textContent = "Scan failed";
    })
    .finally(() => {
      scanButton.disabled = false;
    });
}

// Initial scan when page loads if on STA tab
window.onload = function() {
  if (document.getElementById('sta').style.display === 'block') {
    scanNetworks();
  }
};
</script>
</body>
</html>
)rawliteral";

const char* mainPageContent = R"rawliteral(
<!DOCTYPE HTML>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: Arial; text-align: center; margin: 0 auto; padding: 20px; }
.control { margin: 20px; }
.slider { width: 80%; margin: 10px; }
.button {
  padding: 10px 20px;
  font-size: 16px;
  margin: 5px;
  cursor: pointer;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 5px;
}
.button:hover { background-color: #45a049; }
.preset-btn { background-color: #008CBA; }
.preset-btn:hover { background-color: #0077a3; }
.video-container { margin: 20px 0; }
#video { max-width: 100%; height: auto; }
.tabs { overflow: hidden; border: 1px solid #ccc; background-color: #f1f1f1; }
.tablinks { background-color: inherit; float: left; border: none; outline: none; cursor: pointer; 
            padding: 10px 16px; transition: 0.3s; }
.tablinks:hover { background-color: #ddd; }
.tablinks.active { background-color: #ccc; }
.tabcontent { display: none; padding: 12px; border: 1px solid #ccc; border-top: none; }
</style>
</head>
<body>
<h1>ESP32-CAM PTZ Control</h1>

<div class="tabs">
  <button class="tablinks active" onclick="openTab(event, 'ptz')">PTZ Control</button>
  <button class="tablinks" onclick="openTab(event, 'video')">Video Stream</button>
  <button class="tablinks" onclick="openTab(event, 'wifi')">WiFi Setup</button>
</div>

<div id="ptz" class="tabcontent" style="display:block;">
  <div class="control">
    <h2>Pan Control (Horizontal)</h2>
    <input type="range" min="0" max="180" value="%PAN%" class="slider" id="panSlider" onchange="updatePan(this.value)">
    <p>Angle: <span id="panValue">%PAN%</span>°</p>
    <button class="button" onclick="movePan(-10)">-10°</button>
    <button class="button" onclick="movePan(-5)">-5°</button>
    <button class="button" onclick="movePan(5)">+5°</button>
    <button class="button" onclick="movePan(10)">+10°</button>
    <button class="button" onclick="centerPan()">Center (90°)</button>
  </div>

  <div class="control">
    <h2>Tilt Control (Vertical)</h2>
    <input type="range" min="0" max="180" value="%TILT%" class="slider" id="tiltSlider" onchange="updateTilt(this.value)">
    <p>Angle: <span id="tiltValue">%TILT%</span>°</p>
    <button class="button" onclick="moveTilt(-10)">-10°</button>
    <button class="button" onclick="moveTilt(-5)">-5°</button>
    <button class="button" onclick="moveTilt(5)">+5°</button>
    <button class="button" onclick="moveTilt(10)">+10°</button>
    <button class="button" onclick="centerTilt()">Center (90°)</button>
  </div>

  <div class="control">
    <h2>Preset Positions</h2>
    <button class="button preset-btn" onclick="setPreset(90, 90)">Center</button>
    <button class="button preset-btn" onclick="setPreset(0, 45)">Left Down</button>
    <button class="button preset-btn" onclick="setPreset(180, 45)">Right Down</button>
    <button class="button preset-btn" onclick="setPreset(0, 135)">Left Up</button>
    <button class="button preset-btn" onclick="setPreset(180, 135)">Right Up</button>
  </div>
</div>

<div id="video" class="tabcontent">
  <div class="video-container">
    <img src= "/stream" id="videoStream" width="640" height="480">
  </div>
  <div class="controls">
    <button class="button" onclick="rotateVertical()">Rotasi Vertikal</button>
    <button class="button" onclick="rotateHorizontal()">Rotasi Horizontal</button>
    <button class="button" onclick="resetRotation()">Reset Rotasi</button>
  </div>
</div>

<div id="wifi" class="tabcontent">
  <h2>WiFi Configuration</h2>
  <p>Click the button below to configure WiFi settings</p>
  <button class="button" onclick="window.location.href='/setup'">Go to WiFi Setup</button>
</div>

<script>
function updatePan(angle) {
  document.getElementById("panValue").innerHTML = angle;
  fetch('/pan?value=' + angle);
}

function updateTilt(angle) {
  document.getElementById("tiltValue").innerHTML = angle;
  fetch('/tilt?value=' + angle);
}

function movePan(offset) {
  const slider = document.getElementById("panSlider");
  let newAngle = parseInt(slider.value) + offset;
  if (newAngle < 0) newAngle = 0;
  if (newAngle > 180) newAngle = 180;
  slider.value = newAngle;
  updatePan(newAngle);
}

function moveTilt(offset) {
  const slider = document.getElementById("tiltSlider");
  let newAngle = parseInt(slider.value) + offset;
  if (newAngle < 0) newAngle = 0;
  if (newAngle > 180) newAngle = 180;
  slider.value = newAngle;
  updateTilt(newAngle);
}

function centerPan() {
  document.getElementById("panSlider").value = 90;
  updatePan(90);
}

function centerTilt() {
  document.getElementById("tiltSlider").value = 90;
  updateTilt(90);
}

function setPreset(pan, tilt) {
  document.getElementById("panSlider").value = pan;
  document.getElementById("tiltSlider").value = tilt;
  updatePan(pan);
  updateTilt(tilt);
}

function rotateVertical() {
  fetch('/control?cmd=vertical')
    .then(response => { console.log('Vertical flip toggled'); });
}

function rotateHorizontal() {
  fetch('/control?cmd=horizontal')
    .then(response => { console.log('Horizontal flip toggled'); });
}

function resetRotation() {
  fetch('/control?cmd=reset')
    .then(response => { console.log('Rotation reset'); });
}

function openTab(evt, tabName) {
  var i, tabcontent, tablinks;
  tabcontent = document.getElementsByClassName("tabcontent");
  for (i = 0; i < tabcontent.length; i++) {
    tabcontent[i].style.display = "none";
  }
  tablinks = document.getElementsByClassName("tablinks");
  for (i = 0; i < tablinks.length; i++) {
    tablinks[i].className = tablinks[i].className.replace(" active", "");
  }
  document.getElementById(tabName).style.display = "block";
  evt.currentTarget.className += " active";
  
  // Auto-refresh stream when video tab is opened
  if (tabName === 'video') {
    var img = document.getElementById("videoStream");
    function updateImage() {
      img.src = '/stream?' + new Date().getTime();
    }
    setInterval(updateImage, 0);
  }
  
  // Auto-scan when STA tab is opened
  if (tabName === 'sta') {
    setTimeout(scanNetworks, 500);
  }
}
</script>
</body>
</html>
)rawliteral";

String processor(const String& var) {
  if (var == "PAN") return String(panAngle);
  else if (var == "TILT") return String(tiltAngle);
  else if (var == "STATUS") {
    String status;
    if (WiFi.getMode() & WIFI_STA) {
      if (WiFi.status() == WL_CONNECTED) {
        status += "<p>STA Mode: Connected to " + String(wifiConfig.sta_ssid) + "</p>";
        status += "<p>IP Address: " + WiFi.localIP().toString() + "</p>";
      } else {
        status += "<p>STA Mode: Not connected (" + String(wifiConfig.sta_ssid) + ")</p>";
      }
    }
    if (WiFi.getMode() & WIFI_AP) {
      status += "<p>AP Mode: " + String(wifiConfig.ap_ssid) + "</p>";
      status += "<p>AP IP: " + WiFi.softAPIP().toString() + "</p>";
    }
    return status;
  }
  else if (var == "STA_SSID") return String(wifiConfig.sta_ssid);
  else if (var == "AP_SSID") return String(wifiConfig.ap_ssid);
  else if (var == "AP_PASSWORD") return String(wifiConfig.ap_password);
  else if (var == "AP_IP") return IPAddress(wifiConfig.ap_ip).toString();
  else if (var == "AP_GATEWAY") return IPAddress(wifiConfig.ap_gateway).toString();
  else if (var == "AP_SUBNET") return IPAddress(wifiConfig.ap_subnet).toString();
  return String();
}

static esp_err_t stream_handler(httpd_req_t *req){
  camera_fb_t * fb = NULL;
  esp_err_t res = ESP_OK;
  size_t _jpg_buf_len = 0;
  uint8_t * _jpg_buf = NULL;
  char * part_buf[64];

  res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
  if(res != ESP_OK){
    return res;
  }

  while(true){
    fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Camera capture failed");
      res = ESP_FAIL;
    } else {
      if(fb->width > 400){
        if(vertical_flip || horizontal_flip) {
          uint8_t *buf = fb->buf;
          int width = fb->width;
          int height = fb->height;
          int bytes_per_pixel = fb->len / (width * height);
          
          uint8_t *flipped_buf = (uint8_t *)malloc(fb->len);
          
          for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
              int src_x = horizontal_flip ? (width - 1 - x) : x;
              int src_y = vertical_flip ? (height - 1 - y) : y;
              
              for (int b = 0; b < bytes_per_pixel; b++) {
                flipped_buf[(y * width + x) * bytes_per_pixel + b] =
                  buf[(src_y * width + src_x) * bytes_per_pixel + b];
              }
            }
          }
          
          _jpg_buf = flipped_buf;
          _jpg_buf_len = fb->len;
        } else {
          if(fb->format != PIXFORMAT_JPEG){
            bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
            esp_camera_fb_return(fb);
            fb = NULL;
            if(!jpeg_converted){
              Serial.println("JPEG compression failed");
              res = ESP_FAIL;
            }
          } else {
            _jpg_buf_len = fb->len;
            _jpg_buf = fb->buf;
          }
        }
      }
      
      if(res == ESP_OK){
        size_t hlen = snprintf((char *)part_buf, 64, _STREAM_PART, _jpg_buf_len);
        res = httpd_resp_send_chunk(req, (const char *)part_buf, hlen);
      }
      if(res == ESP_OK){
        res = httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
      }
      if(res == ESP_OK){
        res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
      }
      
      if(fb){
        esp_camera_fb_return(fb);
        fb = NULL;
      } else if(_jpg_buf && (vertical_flip || horizontal_flip)){
        free(_jpg_buf);
        _jpg_buf = NULL;
      }
      
      if(res != ESP_OK){
        break;
      }
    }
  }
  return res;
}

static esp_err_t cmd_handler(httpd_req_t *req) {
  char* buf;
  size_t buf_len;
  char variable[32] = {0,};
 
  buf_len = httpd_req_get_url_query_len(req) + 1;
  if (buf_len > 1) {
    buf = (char*)malloc(buf_len);
    if(!buf){
      httpd_resp_send_500(req);
      return ESP_FAIL;
    }
    if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) {
      if (httpd_query_key_value(buf, "cmd", variable, sizeof(variable)) == ESP_OK) {
        if (strcmp(variable, "vertical") == 0) {
          vertical_flip = !vertical_flip;
          Serial.println("Vertical flip toggled");
        } else if (strcmp(variable, "horizontal") == 0) {
          horizontal_flip = !horizontal_flip;
          Serial.println("Horizontal flip toggled");
        } else if (strcmp(variable, "reset") == 0) {
          vertical_flip = false;
          horizontal_flip = false;
          Serial.println("Rotation reset");
        }
      }
    }
    free(buf);
  }
  httpd_resp_send(req, NULL, 0);
  return ESP_OK;
}

void startCameraServer() {
  httpd_config_t config = HTTPD_DEFAULT_CONFIG();
  config.server_port = 80;

  httpd_uri_t stream_uri = {
    .uri       = "/stream",
    .method    = HTTP_GET,
    .handler   = stream_handler,
    .user_ctx  = NULL
  };

  httpd_uri_t cmd_uri = {
    .uri       = "/control",
    .method    = HTTP_GET,
    .handler   = cmd_handler,
    .user_ctx  = NULL
  };

  if (httpd_start(&stream_httpd, &config) == ESP_OK) {
    httpd_register_uri_handler(stream_httpd, &stream_uri);
    httpd_register_uri_handler(stream_httpd, &cmd_uri);
  }
}

void setupCamera() {
  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;
 
  if(psramFound()){
    config.frame_size = FRAMESIZE_UXGA;
    config.jpeg_quality = 10;
    config.fb_count = 2;
  } else {
    config.frame_size = FRAMESIZE_SVGA;
    config.jpeg_quality = 12;
    config.fb_count = 1;
  }

  // Disable brownout detector
  WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
}

void loadConfig() {
  EEPROM.get(EEPROM_START, wifiConfig);

  if (wifiConfig.mode > 2) {
    memset(&wifiConfig, 0, sizeof(wifiConfig));
    strcpy(wifiConfig.ap_ssid, "ESP32-CAM");
    strcpy(wifiConfig.ap_password, "password123");
    wifiConfig.ap_ip = (uint32_t)IPAddress(192, 168, 4, 1);
    wifiConfig.ap_gateway = (uint32_t)IPAddress(192, 168, 4, 1);
    wifiConfig.ap_subnet = (uint32_t)IPAddress(255, 255, 255, 0);
    wifiConfig.mode = 1;
  }
}

void saveConfig() {
  EEPROM.put(EEPROM_START, wifiConfig);
  EEPROM.commit();
}

void startWiFi() {
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
  delay(100);

  if (wifiConfig.mode == 0 || wifiConfig.mode == 2) {
    IPAddress staticIP(wifiConfig.sta_ip);
    IPAddress gateway(wifiConfig.sta_gateway);
    IPAddress subnet(wifiConfig.sta_subnet);

    if (wifiConfig.sta_ip != 0) {
      WiFi.config(staticIP, gateway, subnet);
    }

    WiFi.begin(wifiConfig.sta_ssid, wifiConfig.sta_password);

    Serial.print("Connecting to WiFi");
    int attempts = 0;
    while (WiFi.status() != WL_CONNECTED && attempts < 20) {
      delay(500);
      Serial.print(".");
      attempts++;
    }

    if (WiFi.status() == WL_CONNECTED) {
      Serial.println("\nConnected to WiFi");
      Serial.print("IP Address: ");
      Serial.println(WiFi.localIP());
    } else {
      Serial.println("\nFailed to connect to WiFi");
    }
  }

  if (wifiConfig.mode == 1 || wifiConfig.mode == 2) {
    IPAddress apIP(wifiConfig.ap_ip);
    IPAddress gateway(wifiConfig.ap_gateway);
    IPAddress subnet(wifiConfig.ap_subnet);

    WiFi.softAPConfig(apIP, gateway, subnet);
    WiFi.softAP(wifiConfig.ap_ssid, wifiConfig.ap_password);

    Serial.println("AP Mode Enabled");
    Serial.print("AP SSID: ");
    Serial.println(wifiConfig.ap_ssid);
    Serial.print("AP IP Address: ");
    Serial.println(apIP);
  }
}

void handleScan() {
  WiFiMode_t currentMode = WiFi.getMode();
  WiFi.mode(WIFI_STA);
  WiFi.disconnect();
  delay(100);

  int n = WiFi.scanNetworks();
  String json = "[";
  
  if (n == -1) {
    json += "{\"error\":\"Scan failed\"}";
  } else if (n == 0) {
    json += "{\"error\":\"No networks found\"}";
  } else {
    for (int i = 0; i < n; ++i) {
      if (i) json += ",";
      json += "{";
      json += "\"ssid\":\"" + WiFi.SSID(i) + "\",";
      json += "\"rssi\":" + String(WiFi.RSSI(i)) + ",";
      json += "\"encryption\":" + String(WiFi.encryptionType(i) != WIFI_AUTH_OPEN);
      json += "}";
      delay(10);
    }
  }
  
  json += "]";
  
  WiFi.mode(currentMode);
  if (currentMode & WIFI_AP) {
    WiFi.softAP(wifiConfig.ap_ssid, wifiConfig.ap_password);
  }
  
  server.send(200, "application/json", json);
  WiFi.scanDelete();
}

void handleSave() {
  if (server.method() != HTTP_POST) {
    server.send(405, "text/plain", "Method Not Allowed");
    return;
  }

  String mode = server.arg("mode");

  if (mode == "0") {
    String ssid = server.arg("sta_ssid");
    String password = server.arg("sta_password");
    String ip = server.arg("sta_ip");
    String gateway = server.arg("sta_gateway");
    String subnet = server.arg("sta_subnet");

    if (ssid.length() > 0) {
      ssid.toCharArray(wifiConfig.sta_ssid, sizeof(wifiConfig.sta_ssid));
      password.toCharArray(wifiConfig.sta_password, sizeof(wifiConfig.sta_password));

      if (ip.length() > 0) {
        wifiConfig.sta_ip = IPAddress().fromString(ip);
      } else {
        wifiConfig.sta_ip = 0;
      }
      
      if (gateway.length() > 0) {
        wifiConfig.sta_gateway = IPAddress().fromString(gateway);
      } else {
        wifiConfig.sta_gateway = 0;
      }
      
      if (subnet.length() > 0) {
        wifiConfig.sta_subnet = IPAddress().fromString(subnet);
      } else {
        wifiConfig.sta_subnet = 0;
      }

      wifiConfig.mode = 0;
      saveConfig();

      server.send(200, "text/html", "<script>alert('STA Configuration Saved! Device will reboot.'); setTimeout(function(){ window.location.href = '/reboot'; }, 1000);</script>");
      return;
    }
  } else if (mode == "1") {
    String ssid = server.arg("ap_ssid");
    String password = server.arg("ap_password");
    String ip = server.arg("ap_ip");
    String gateway = server.arg("ap_gateway");
    String subnet = server.arg("ap_subnet");

    if (ssid.length() > 0 && ip.length() > 0) {
      ssid.toCharArray(wifiConfig.ap_ssid, sizeof(wifiConfig.ap_ssid));
      password.toCharArray(wifiConfig.ap_password, sizeof(wifiConfig.ap_password));

      wifiConfig.ap_ip = IPAddress().fromString(ip);
      wifiConfig.ap_gateway = IPAddress().fromString(gateway);
      wifiConfig.ap_subnet = IPAddress().fromString(subnet);

      wifiConfig.mode = 1;
      saveConfig();

      server.send(200, "text/html", "<script>alert('AP Configuration Saved! Device will reboot.'); setTimeout(function(){ window.location.href = '/reboot'; }, 1000);</script>");
      return;
    }
  }

  server.send(400, "text/plain", "Invalid Parameters");
}

void handleReboot() {
  server.send(200, "text/html", "<!DOCTYPE html><html><head><meta http-equiv=\"refresh\" content=\"5;url=/setup\"></head><body><h1>Rebooting...</h1><p>Device will restart in 5 seconds.</p></body></html>");
  delay(1000);
  ESP.restart();
}

void handleNotFound() {
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";

  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }

  server.send(404, "text/plain", message);
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  pinMode(FLASH_GPIO_NUM, OUTPUT);
  digitalWrite(FLASH_GPIO_NUM, LOW);

  EEPROM.begin(EEPROM_SIZE);
  loadConfig();

  panServo.attach(PAN_PIN);
  tiltServo.attach(TILT_PIN);
  panServo.write(panAngle);
  tiltServo.write(tiltAngle);

  startWiFi();
  setupCamera();
  startCameraServer();

  server.on("/", HTTP_GET, []() {
    String html = mainPageContent;
    html.replace("%PAN%", String(panAngle));
    html.replace("%TILT%", String(tiltAngle));
    server.send(200, "text/html", html);
  });

  server.on("/setup", HTTP_GET, []() {
    String html = wifiConfigPage;
    html.replace("%STATUS%", processor("STATUS"));
    html.replace("%STA_SSID%", processor("STA_SSID"));
    html.replace("%AP_SSID%", processor("AP_SSID"));
    html.replace("%AP_PASSWORD%", processor("AP_PASSWORD"));
    html.replace("%AP_IP%", processor("AP_IP"));
    html.replace("%AP_GATEWAY%", processor("AP_GATEWAY"));
    html.replace("%AP_SUBNET%", processor("AP_SUBNET"));
    server.send(200, "text/html", html);
  });

  server.on("/pan", HTTP_GET, []() {
    if (server.hasArg("value")) {
      panAngle = server.arg("value").toInt();
      panServo.write(panAngle);
    }
    server.send(200, "text/plain", "OK");
  });

  server.on("/tilt", HTTP_GET, []() {
    if (server.hasArg("value")) {
      tiltAngle = server.arg("value").toInt();
      tiltServo.write(tiltAngle);
    }
    server.send(200, "text/plain", "OK");
  });

  server.on("/scan", HTTP_GET, handleScan);
  server.on("/save", HTTP_POST, handleSave);
  server.on("/reboot", HTTP_POST, handleReboot);
  server.onNotFound(handleNotFound);

  server.begin();
  Serial.println("HTTP server started");
}

void loop() {
  server.handleClient();
  delay(2);
}

Atau untuk versi lebih mudahnya ini dia versi binary :

yang dapat di-upload melalui ini oleh spacehuhn

Leave a Reply

Your email address will not be published. Required fields are marked *