ESP32 Web Osiloskop Projesi(Kütüphanesiz)

👤 Yazar: ozcan 📅 Tarih: 09.06.2026 02:00 👁️ Görüntüleme: 34

Kütüphanesiz ESP32 Web Osiloskop Projesi (Gömülü esp_http_server ile v3 Uyumlu)

Merhaba yenidir.com takipçileri! Bu yazımızda, harici hiçbir kütüphaneye (ESPAsyncWebServer, AsyncTCP vb.) ihtiyaç duymadan, ESP32'nin kendi çekirdeğinde donanımsal olarak yer alan esp_http_server.h motorunu kullanarak yüksek performanslı bir dijital web osiloskopu yapıyoruz.

Arduino Core v3.0+ güncellemeleriyle gelen kütüphane uyuşmazlıklarını ve MD5 derleme hatalarını tamamen çözen bu kararlı kod, tarayıcınızda HTML5 Canvas altyapısı kullanarak saniyede 30 kareye yakın hızda (FPS) canlı sinyal grafiği çizer. Projede HOLD (Ekran Dondurma), VOLTS/DIV, TIME/DIV, grafik kaydırma (Y-OFFSET) ve yakalanan verileri CSV olarak dışa aktarma gibi gelişmiş laboratuvar özellikleri mevcuttur.

Donanım Bağlantısı ve Güvenlik Önlemleri

ESP32'nin dahili ADC (Analog Dijital Dönüştürücü) pinleri maksimum 3.3V seviyesine kadar güvenli ölçüm yapabilir. İşlemcinizin GPIO pinine zarar vermemek için aşağıdaki adımlara dikkat etmelisiniz:

Projenin Kaynak Kodu

Aşağıdaki kodu doğrudan kopyalayıp hiçbir kütüphane kurmadan Arduino IDE üzerinden ESP32 kartınıza yükleyebilirsiniz. Kodun başındaki ssid ve password alanlarını kendi Wi-Fi ağınıza göre düzenlemeyi unutmayın.

#include <WiFi.h>
#include <esp_http_server.h>

// Wi-Fi Ağ Ayarlarınız
const char* ssid = "YENIDIR_WIFI";
const char* password = "WIFI_SIFRENIZ";

// Osiloskop Ölçüm Pini
const int analogPin = 34; 

// Örnekleme Değişkenleri
#define ORNEK_SAYISI 200
uint16_t voltajVerileri[ORNEK_SAYISI];

// Sabit boyutlu güvenli bellek alanı
char jsonBuffer[1500] = "[]";

httpd_handle_t server = NULL;

// Web Arayüzü HTML (Tüm Gelişmiş Özellikler Eklendi)
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Yenidir.com - Profesyonel Web Osiloskop</title>
    <style>
        body { font-family: 'Segoe UI', Arial, sans-serif; background: #121214; color: #e1e1e6; text-align: center; margin: 0; padding: 10px; }
        h1 { color: #00adb5; margin-bottom: 2px; margin-top: 5px; }
        .container { max-width: 900px; margin: auto; background: #1a1a1e; padding: 15px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }
        .control-panel { display: flex; flex-wrap: wrap; justify-content: center; gap: 15px; background: #22252a; padding: 12px; border-radius: 8px; margin-bottom: 10px; border: 1px solid #333; }
        .control-group { display: flex; align-items: center; gap: 6px; font-size: 13px; }
        label { font-weight: bold; color: #aaa; }
        select, button { background: #121214; color: #00adb5; border: 1px solid #00adb5; padding: 6px 10px; border-radius: 4px; font-weight: bold; cursor: pointer; outline: none; }
        select:hover, button:hover { background: #00adb5; color: #121214; }
        button.hold-active { background: #ff4757; color: #fff; border-color: #ff4757; animation: blink 1s infinite; }
        @keyframes blink { 50% { opacity: 0.6; } }
        input[type=range] { -webkit-appearance: none; background: #121214; border: 1px solid #444; height: 6px; border-radius: 3px; outline: none; width: 100px; }
        input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; background: #00adb5; width: 14px; height: 14px; border-radius: 50%; cursor: pointer; }
        canvas { background: #050505; border: 2px solid #333; border-radius: 6px; display: block; margin: 10px auto; box-shadow: inset 0 0 10px #000; }
        .stats { display: flex; flex-wrap: wrap; justify-content: space-around; font-size: 14px; font-weight: bold; margin-top: 10px; background: #22252a; padding: 10px; border-radius: 6px; border: 1px solid #333; gap: 10px; }
        .val { color: #00adb5; }
    </style>
</head>
<body>
    <div class="container">
        <h1>ESP32 Dijital Osiloskop</h1>
        <p style="color: #888; margin-top: 0; margin-bottom: 10px; font-size: 13px;">yenidir.com Donanım Laboratuvarı</p>
        
        <div class="control-panel">
            <div class="control-group">
                <label>VOLTS/DIV:</label>
                <select id="voltsDiv">
                    <option value="0.2">200 mV</option>
                    <option value="0.5" selected>500 mV</option>
                    <option value="1.0">1.0 V</option>
                    <option value="1.5">1.5 V</option>
                </select>
            </div>
            <div class="control-group">
                <label>TIME/DIV:</label>
                <select id="timeDiv">
                    <option value="50">X0.5</option>
                    <option value="100" selected>X1.0</option>
                    <option value="150">X1.5</option>
                    <option value="200">X2.0</option>
                </select>
            </div>
            <div class="control-group">
                <label>TRIGGER (V):</label>
                <input type="range" id="triggerLevel" min="0" max="3.3" step="0.1" value="1.6">
                <span id="trigVal" class="val">1.6V</span>
            </div>
            <div class="control-group">
                <label>Y-OFFSET:</label>
                <input type="range" id="yOffset" min="-150" max="150" step="5" value="0">
            </div>
            <div class="control-group">
                <button id="holdBtn">HOLD</button>
                <button id="exportBtn">EXPORT CSV</button>
            </div>
        </div>

        <canvas id="scopeCanvas" width="800" height="400"></canvas>
        
        <div class="stats">
            <div>Maks (Vmax): <span id="vmax" class="val">0.00</span> V</div>
            <div>Min (Vmin): <span id="vmin" class="val">0.00</span> V</div>
            <div>Tepe-Tepe (Vpp): <span id="vpp" class="val">0.00</span> V</div>
            <div>Frekans: <span id="freq" class="val">0.00</span> Hz</div>
            <div>Periyot: <span id="period" class="val">0.00</span> ms</div>
        </div>
    </div>

    <script>
        const canvas = document.getElementById('scopeCanvas');
        const ctx = canvas.getContext('2d');
        
        const voltsDivSelect = document.getElementById('voltsDiv');
        const timeDivSelect = document.getElementById('timeDiv');
        const triggerLevelSlider = document.getElementById('triggerLevel');
        const trigValLabel = document.getElementById('trigVal');
        const yOffsetSlider = document.getElementById('yOffset');
        const holdBtn = document.getElementById('holdBtn');
        const exportBtn = document.getElementById('exportBtn');

        let isHold = false;
        let sonGelenVeri = [];
        const SAMPLE_TIME_MS = 0.08; 

        holdBtn.addEventListener('click', () => {
            isHold = !isHold;
            if (isHold) {
                holdBtn.innerText = "FREEZE";
                holdBtn.classList.add('hold-active');
            } else {
                holdBtn.innerText = "HOLD";
                holdBtn.classList.remove('hold-active');
            }
        });

        exportBtn.addEventListener('click', () => {
            if (sonGelenVeri.length === 0) return;
            let csvContent = "data:text/csv;charset=utf-8,Indeks,HamDeger,Voltaj(V)\n";
            sonGelenVeri.forEach((val, idx) => {
                let v = (val * 3.3) / 4095;
                csvContent += `${idx},${val},${v.toFixed(3)}\n`;
            });
            const encodedUri = encodeURI(csvContent);
            const link = document.createElement("a");
            link.setAttribute("href", encodedUri);
            link.setAttribute("download", "yenidir_osiloskop_veri.csv");
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        });

        triggerLevelSlider.addEventListener('input', () => {
            trigValLabel.innerText = parseFloat(triggerLevelSlider.value).toFixed(1) + "V";
        });

        function drawGrid() {
            ctx.strokeStyle = '#1e222b'; ctx.lineWidth = 1;
            for (let x = 0; x < canvas.width; x += 40) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, canvas.height); ctx.stroke(); }
            for (let y = 0; y < canvas.height; y += 40) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke(); }
            
            ctx.strokeStyle = '#444'; ctx.lineWidth = 1.5;
            ctx.beginPath(); ctx.moveTo(0, canvas.height/2); ctx.lineTo(canvas.width, canvas.height/2); ctx.stroke();
            ctx.beginPath(); ctx.moveTo(canvas.width/2, 0); ctx.lineTo(canvas.width/2, canvas.height); ctx.stroke();

            const vDiv = parseFloat(voltsDivSelect.value);
            const trigV = parseFloat(triggerLevelSlider.value);
            const offset = parseInt(yOffsetSlider.value);
            let trigY = (canvas.height / 2) - ((trigV - 1.65) / vDiv) * 40 - offset;
            if (trigY >= 0 && trigY <= canvas.height) {
                ctx.strokeStyle = 'rgba(255, 71, 87, 0.4)';
                ctx.setLineDash([5, 5]);
                ctx.beginPath(); ctx.moveTo(0, trigY); ctx.lineTo(canvas.width, trigY); ctx.stroke();
                ctx.setLineDash([]);
            }
        }

        setInterval(function() {
            if (isHold) return;

            fetch('/data').then(response => response.json()).then(data => {
                if (data.length === 0) return;
                sonGelenVeri = data;

                ctx.clearRect(0, 0, canvas.width, canvas.height);
                drawGrid();

                const vDiv = parseFloat(voltsDivSelect.value);
                const tDivFactor = parseInt(timeDivSelect.value);
                const trigV = parseFloat(triggerLevelSlider.value);
                const offset = parseInt(yOffsetSlider.value);

                let hamMax = Math.max(...data); let hamMin = Math.min(...data);
                let vmax = (hamMax * 3.3) / 4095; let vmin = (hamMin * 3.3) / 4095; let vpp = vmax - vmin;
                document.getElementById('vmax').innerText = vmax.toFixed(2);
                document.getElementById('vmin').innerText = vmin.toFixed(2);
                document.getElementById('vpp').innerText = vpp.toFixed(2);

                let esikVoltajı = (vmax + vmin) / 2;
                let gecisIndeksleri = [];
                for (let i = 0; i < data.length - 1; i++) {
                    let v1 = (data[i] * 3.3) / 4095;
                    let v2 = (data[i+1] * 3.3) / 4095;
                    if (v1 <= esikVoltajı && v2 > esikVoltajı) {
                        gecisIndeksleri.push(i);
                    }
                }
                if (gecisIndeksleri.length >= 2 && vpp > 0.2) {
                    let toplamFark = 0;
                    for(let k=0; k < gecisIndeksleri.length-1; k++) {
                        toplamFark += (gecisIndeksleri[k+1] - gecisIndeksleri[k]);
                    }
                    let ortalamaOrnekPeriyodu = toplamFark / (gecisIndeksleri.length - 1);
                    let periyotMs = ortalamaOrnekPeriyodu * SAMPLE_TIME_MS;
                    let frekansHz = 1000 / periyotMs;
                    
                    document.getElementById('period').innerText = periyotMs.toFixed(2);
                    document.getElementById('freq').innerText = frekansHz.toFixed(0);
                } else {
                    document.getElementById('period').innerText = "0.00";
                    document.getElementById('freq').innerText = "0.00";
                }

                let baslangicIndeksi = 0;
                for (let i = 0; i < data.length - 1; i++) {
                    let v1 = (data[i] * 3.3) / 4095;
                    let v2 = (data[i+1] * 3.3) / 4095;
                    if (v1 <= trigV && v2 > trigV) {
                        baslangicIndeksi = i;
                        break;
                    }
                }

                ctx.strokeStyle = '#00adb5'; ctx.lineWidth = 3; ctx.shadowBlur = 6; ctx.shadowColor = '#00adb5';
                ctx.beginPath();

                let gosterilecekNoktaSayisi = Math.min(data.length - baslangicIndeksi, tDivFactor);
                let stepX = canvas.width / (gosterilecekNoktaSayisi - 1);

                for (let i = 0; i < gosterilecekNoktaSayisi; i++) {
                    let hamDeger = data[baslangicIndeksi + i];
                    let gercekVoltaj = (hamDeger * 3.3) / 4095;
                    let y = (canvas.height / 2) - ((gercekVoltaj - 1.65) / vDiv) * 40 - offset;
                    
                    if (y < 0) y = 0; if (y > canvas.height) y = canvas.height;
                    let x = i * stepX;
                    
                    if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
                }
                ctx.stroke(); ctx.shadowBlur = 0;
            });
        }, 30);
    </script>
</body>
</html>
)rawliteral";

// GÖMÜLÜ SUNUCU URİ TRAFİK KONTROLLERİ
esp_err_t root_get_handler(httpd_req_t *req) {
    httpd_resp_set_type(req, "text/html");
    httpd_resp_set_hdr(req, "Connection", "keep-alive");
    return httpd_resp_send(req, index_html, HTTPD_RESP_USE_STRLEN);
}

esp_err_t data_get_handler(httpd_req_t *req) {
    httpd_resp_set_type(req, "application/json");
    httpd_resp_set_hdr(req, "Connection", "keep-alive");
    httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*"); 
    return httpd_resp_send(req, jsonBuffer, HTTPD_RESP_USE_STRLEN);
}

httpd_uri_t uri_root = { .uri = "/", .method = HTTP_GET, .handler = root_get_handler, .user_ctx = NULL };
httpd_uri_t uri_data = { .uri = "/data", .method = HTTP_GET, .handler = data_get_handler, .user_ctx = NULL };

void start_native_web_server() {
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.stack_size = 8192;
    config.max_open_sockets = 7;
    config.lru_purge_enable = true; 

    if (httpd_start(&server, &config) == ESP_OK) {
        httpd_register_uri_handler(server, &uri_root);
        httpd_register_uri_handler(server, &uri_data);
        Serial.println("[BAŞARILI] Yerleşik Web Sunucu Başlatıldı.");
    }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("\n--- [YENIDIR LAB SCOPE] ESP32 Başlatılıyor ---");
  
  analogReadResolution(12);
  
  WiFi.persistent(false);
  WiFi.disconnect(true);
  delay(200);
  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  
  Serial.print("Wi-Fi'ye bağlanılıyor");
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    yield();
  }
  
  Serial.println("\n[BAŞARILI] Wi-Fi Bağlantısı Sağlandı.");
  Serial.print("IP Adresi: ");
  Serial.println(WiFi.localIP());

  start_native_web_server();
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    delay(10);
    return;
  }

  // 1. Sinyal örneklerini durmaksızın arka planda oku
  for (int i = 0; i < ORNEK_SAYISI; i++) {
    voltajVerileri[i] = analogRead(analogPin);
  }

  // 2. Sıfır gecikmeli JSON dize tamponu oluşturma
  int offset = snprintf(jsonBuffer, sizeof(jsonBuffer), "[");
  for (int i = 0; i < ORNEK_SAYISI; i++) {
    int written = snprintf(jsonBuffer + offset, sizeof(jsonBuffer) - offset, "%d", voltajVerileri[i]);
    offset += written;
    if (i < ORNEK_SAYISI - 1) {
      written = snprintf(jsonBuffer + offset, sizeof(jsonBuffer) - offset, ",");
      offset += written;
    }
  }
  snprintf(jsonBuffer + offset, sizeof(jsonBuffer) - offset, "]");

  delay(10); 
}

Sonuç ve Analiz

Yazılımı yükledikten sonra Arduino Seri Port ekranından ESP32'nizin aldığı yerel IP adresini tarayıcınıza yazarak osiloskop paneline ulaşabilirsiniz. esp_http_server mimarisinin sunduğu hafiflik ve asenkron JSON aktarımı sayesinde web arayüzünüz işlemci döngülerini kilitlemeden akıcı bir şekilde çalışacaktır.

esp32 osiloskop web görüntüsü

Bir sonraki donanım ve siber mühendislik projemizde görüşmek üzere, sorularınızı yorumlar kısmından bizimle paylaşabilirsiniz. Atölyenizde keyifli ölçümler!

💬 Yorumlar

Henüz yorum yapılmamış. İlk yorumu siz yapın!

Bir Yorum Bırakın

Güvenlik Kontrolü:

Yenidir.com
Sayfa Başı
Yenidir CMS V0.1