ESP8266 WEB SERVER İLE ADXL345 İVME SENSÖRÜ GRAFİK ARAYÜZÜ

Blog sitesindeki ilk teknik yayınım ESP8266_01 ile bir WEB Server oluşturulması üzerineydi. O yayına bu link’i tıklayarak ulaşabilirsiniz. Bir aradan sonra yine ESP8266_01 tabanlı bir projemden söz edeceğim. WEB sunucumuz ADXL ivme ölçerden okunan verileri bir grafik halinde yayınlıyor.
Başlangıçta ESP programlarını Arduino IDE platformu üzerinde geliştirip yüklüyordum. Son zamanlarda Arduino IDE yi terkederek Visual Studio/Platform IO kullanmaya başladım. Bu yayında bu yeni platformun kullanılışını anlatmayacağım. VS Platform_IO kullanımının bilindiği varsayımı ile devam ediyorum.

WEB Grafikleri için Highcharts kütüphanesini kullanıyorum. NTP zaman bilgileri için de ezTime kütüphanesini kullandım.

Bu yayında ivme ve gyro okumalarını derece’ye çevirmiyorum. Açı hesaplamalarının yapılışını anlattığım bir başka yayınım var:

İvme ve gyro değerlerinden hareketle eğim açılarının hesaplanması.

ESP8266 belirlenen aralıklarla yeni bir okuma yaparak flash belleğe littleFS kütüphanesinden yararlanarak kaydediyor. Bir istemci ile bağlantı kurulduğunda kaydedilmiş olan verileri istemciye aktarıp, bağlantı kesilmediği sürece yeni okumalar ile Asenkron WEB protokolu ile güncelleme yapıyor.

KAYNAKLAR

Her yeni konuya girdiğimde kaçınılmaz olarak internet üzerinde yaptığım taramalar ile bir şeyler öğrenmeye, örnekler bulmaya çalışıyorum.

Bu projede de ESP konusundaki ilk başvuru kaynağımı oluşturan RandomNerdTutorials (RNT) sitesinden yararlandım.

Kendi yaptığım ekleme ve değişikliklerim şöyle:

  • RNT blogundaki örnekler NTPClient kütüphanesini kullanıyor. Bu kütüphane Espressif ESP8266 nin yeni sürümleri ile çalışmıyor. Ben bu nedenle ezTime kütüphanesini kullandım.
  • ezTime kütüphanesi geliştiricisinin vermiş olduğu örnekte olduğu gibi kullanılırsa Static IP ile çalışmıyor, NTP server timeout oluyor. Bu nedenle Wifi yapılandırma ayarları ile epeyi uğraşmam gerekti.
  • RNT örneklerinde kullanılanlar ile ezTime kütüphanesi nesne isimleri arasında çatışmalar oluyordu, bunlar çözümlendi.
  • ADXL345 için bulunan örnekler genelde hazır kütüphane kullanımına yönelik. RNT Blogunda da ADXL345 e yönelik bir örnek uygulama yok. İşin bu tarafında kendi geliştirmelerime epeyi iş düştü.
  • RNT Blogundaki örnekler genelde sıcaklık ve ısı sensörlerine yönelik olarak verilmiş. ADXL verilerinin yayınlanması için index.html ve script.js dosyaları üzerinde epeyi değişiklik yapmam gerekti. Grafik ekranlarına Delete ve RawData butonları eklendi.
  • Bu çalışmalardaki deneyimlerimi ve sonuçları RNT Blog sitesini yöneten arkadaşlar ile de paylaşıyorum.

KULLANDIĞIM DONANIM ve DEVRE ŞEMASI

Kullanılan MODÜLLER

Buraya elimizin altında bulunması yararlı olacak bilgileri koyuyorum.

ADXL345 MODÜLÜ

ADXL345 için piyasada yaygın olarak iki tipte geliştirme modülü bulunuyor. Bunlardan birisinde ADXL erişim ayakları PCB nin tek tarafında bulunuyor. Benim kullandığım diğerinde ise sağlı sollu iki kenara paylaştırılmış durumda.

ESP8266_01 MODÜLÜ

ESP8266 nin farklı tiplerde geliştirme modülleri bulunuyor. Benim kullandığım ESP8266_01 in 1M flash bellekli olanı. Bunların 512KB flash lı olanları da var, görünümleri aynı, Çin’li tedarikçilerden satın alındığında dikkatli olmak gerekiyor, genelde bu ayrıntıyı belirtmiyorlar. 512K Flash boyutu bu ve benzeri projeler için yetersiz kalıyor.

512KB/1M olduğunu sağdaki 8 ayaklı EEPROM’un üzerindeki etiketi okuyarak anlayabilirsiniz. 25Q40 olanlar 4MBit yani 512KB, 25Q80 olanlar ise 8Mbit yani 1MB lık olanlar. Aşağıdaki fotoda görülen de 512KB Flash bellekli, ayak fonksiyonları güzel gösterildiği için kullandığım bir görsel.

ESP8266_01 ayak fonksiyonlarını özetleyen bu tablo da gözümüzün önünde bulunsun.

FTDI232 USB-TTL UART ÇEVİRİCİ MODÜLÜ

ESP8266 yı programlamak ve devamında hata ayıklamak için bir USB/UART TTL çeviriciye gereksinim var. Program geliştirilip yüklendikten sonra normal çalışma sırasında bu modüle gereksinim kalmıyor.

DEVRE ŞEMASI

Devreye 5V beslemeyi iki pinli bir konnektör üzerinden veriyorum.

ADXL345 modülü üzerinde bir 3.3V gerilim regülatörü var, ADXL345 i bu besliyor. İstenirse dahili regülatör devre dışı bırakılıp 3V3 ayağına dışarıdan 3V3 besleme verilebiliyor.

ESP8266 da 3.3V ile çalışıyor, devreye hem ADXL hem de ESP yi beslemek üzere bir 3V3 gerilim regülatörü koydum.

İki tane de butonumuz var. Bunlardan SW1, SPDT tipi olanı ESP8266 ya program yüklerken GPIO0 ayağını “0” V a çekmek için kullanılıyor. SW2 de RESET ayağını “0” seviyesine çekerek ESP8266 yı resetlemekte kullanılıyor.

Devrede istendiğinde kullanılabilecek bir I2C ekran konnektörü de var ama bu yayına ona ilişkin bir kod koymadım.

BASKILI DEVRE

Tek taraflı bir PCB tasarladım, CNC de kazıyarak üretmek üzere.

CİHAZIN BİTMİŞ HALİ (EKRAN VE PROGRAMLAMA BAĞLANTILARI HARİÇ)
CİHAZ OLED EKRAN VE PROGRAMLAMA BAĞLANTILARI İLE

PROGRAM

Program geliştirmesinde Visual Studio IDE yi PlatformIO eklentisi ile birlikte kullanıyorum. Kullanılan geliştirme ortamı VS PlatformIO olsa da bu çalışma tamamiyle Arduino framework üzerinde yürütülüyor. Bu arada Arduino platformu kullanımı hakkındaki düşüncelerimi burada belirtmek istiyorum:

Arduino platformu hobby amaçlı geliştiriciler ve öğrenciler arasında çok popüler ve sevilen bir araç.
Ben Arduino'yu sevmiyorum, bu nedenle sadece ESP serisi işlemciler ile kullanıyorum.
Öğrencilere, yani bu işlere gömülü sistemleri öğrenmek için girenlere de tavsiye etmiyorum. Arduino ile çok kısa sürede çalışan bir ürün ortaya konulabilse de mikro denetleyicilerin iç yapıları, yapılandırma ayarları hakkında hiç bir şey öğrenmiyorlar. Kütüphanesini bulamadıkları bir uygulama söz konusu olduğunda bir şey yapamıyorlar.
Çünkü muazzam büyüklükteki bir hazır kütüphaneler koleksiyonunu kullanarak çalışıyorlar.
Bir başka deyişle araba yapmayı değil, sürmeyi öğreniyorlar. Elbette bu da bir şeydir, harika bir ralli sürücüsü olabilirsiniz bunu da küçümsememek gerek. Ama amacınızın ne olduğunu doğru belirlemek gerek. Araç sahibi gelip "in arabadan artık vermiyorum, kendi arabanı yap" dediğinde ortada kalıverirsiniz. Arabayı sürmek başka şey, arabayı yapmak başka şey.

KULLANILAN KÜTÜPHANELER

Gereken kütüphanelere PIO Home sayfasındaki Libraries seçeceği üzerinde ulaşılıp yüklenebiliyor.

Bunun için bu ekrandaki Search libraries kutucuğuna aradığınız kütüphanenin birkaç kelimesini yazıp sorgulana yapıyorsunuz, karşınıza aramanıza uygun düşen bir liste çıkıyor. O listeden istediğiniz kütüphaneyi seçerek yüklüyorsunuz.

KULLANILAN KÜTÜPHANELER

Burada görülen kütüphaneleri yukarıda sözünü ettiğim gibi projenize eklemeniz gerekiyor.

PLATFORMIO.ini DOSYASI

Platformio.ini dosyasının içeriği aşağıdaki gibi olmalı. lib_deps bölümü kütüphaneler projeye eklendiğince otomatik olarak oluşturuluyor. Eksik kalmış satırlar oldursa aşağıdaki gibi tamamlamak gerekiyor.

KODLAR

ADXL345 FONKSİYONU

ADXL345 için kullanılabilecek hazır kütüphaneler var. Bunlar çok farklı seçeneklere olanak veren kapsamlı, flash bellekte de bu ölçüde hatırı sayılır yer kaplayan kodlar. Ben hiç olmazsa ADXL345 ile ilgili fonksiyon için kütüphane kullanmamayı tercih ettim.

Bu fonksiyon yerine hazır kütüphane kullanılırsa aynı işi birkaç satır kod ile yapmanız mümkün. Ama ADXL ile iletişim ve kontrol konusunda hiç bir şey öğrenmemiş olursunuz.

Bu fonksiyon ADXL345 den ivme değerlerini ADC çıkışlarında alınan haliyle veriyor. X ve Y eksenlerinde 0 ila 311 arasında değerler alınıyor. Bu değerlerin ivme ya da açı değerlerine çevrilmesi gerekiyor ama bunu bir sonraki aşamaya bıraktım.

Bu fonksiyon ADXL verilerini ve okumanın yapıldığı tarih-zaman bilgilerini readings adlı bir JSON değişkenine yüklüyor, ayrıca bir String’e çevirerek geri gönderiyor. JSON readings değişkeninin bildirimi şöyle:

JSONVar readings;

String measureAcc()
{
  unsigned int data[6];

  // Start I2C Transmission
  Wire.beginTransmission(Addr);
  // Select bandwidth rate register
  Wire.write(0x2C);
  // Normal mode, Output data rate = 100 Hz
  Wire.write(0x0A);
  // Stop I2C transmission
  Wire.endTransmission();

  // Start I2C Transmission
  Wire.beginTransmission(Addr);
  // Select power control register
  Wire.write(0x2D);
  // Auto-sleep disable
  Wire.write(0x08);
  // Stop I2C transmission
  Wire.endTransmission();

  // Start I2C Transmission
  Wire.beginTransmission(Addr);
  // Select data format register
  Wire.write(0x31);
  // Self test disabled, 4-wire interface, Full resolution, Range = +/-2g
  Wire.write(0x08);
  // Stop I2C transmission
  Wire.endTransmission();
  delay(300);

  for (int i = 0; i < 6; i++)
  {
    // Start I2C Transmission
    Wire.beginTransmission(Addr);
    // Select data register
    Wire.write((50 + i));
    // Stop I2C transmission
    Wire.endTransmission();

    // Request 1 byte of data
    Wire.requestFrom(Addr, 1);

    // Read 6 bytes of data
    // xAccl lsb, xAccl msb, yAccl lsb, yAccl msb, zAccl lsb, zAccl msb
    if (Wire.available() == 1)
    {
      data[i] = Wire.read();
    }
  }

  // Convert the data to 10-bits
  xAccl = (((data[1] & 0x03) * 256) + data[0]);
  if (xAccl > 511)
  {
    xAccl -= 1024;
  }
  yAccl = (((data[3] & 0x03) * 256) + data[2]);
  if (yAccl > 511)
  {
    yAccl -= 1024;
  }
  zAccl = (((data[5] & 0x03) * 256) + data[4]);
  if (zAccl > 511)
  {
    zAccl -= 1024;
  }
  readings["time"] = String(getTime());
  readings["xAccl"] = String(xAccl);
  readings["yAccl"] = String(yAccl);
  readings["zAccl"] = String(zAccl);
  String jsonString = JSON.stringify(readings);
  
  // Output data to serial monitor
  Serial.println(readings);
  return jsonString;
}

SETUP ve ANA DÖNGÜ

void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);
   // Initialise I2C communication as MASTER
  Wire.begin(SDA, SCL);
   // Initialise SPIFSS
  initFS();

  // Connect to Wi-Fi
  init_WIFI();

  Serial.println("WAITING FOR SYNC ");
  setServer("pool.ntp.org");
  setDebug(INFO);

  waitForSync();
  Turkey.setLocation("TR");

  // Create a data.txt file
  bool fileexists = LittleFS.exists(dataPath);
  Serial.print(fileexists);
  if(!fileexists) {
    Serial.println("File doesn't exist");
    Serial.println("Creating file...");
    // Prepare readings to add to the file
    String message = measureAcc() + ",";
    // Apend data to file to create it
    appendFile(LittleFS, dataPath, message.c_str());
   }
  else {
    Serial.println("File already exists"); 
  }

   // Web Server Root URL
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/index.html", "text/html");
  });

  server.serveStatic("/", LittleFS, "/");
  
  // Request for sensor reading data
  server.on("/readings", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/data.txt", "text/txt");
  });

  // Request for raw data view
  server.on("/view-data", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(LittleFS, "/data.txt", "text/txt");
  });

  // Request for delete file
  server.on("/delete-data", HTTP_GET, [](AsyncWebServerRequest *request){
    deleteFile(LittleFS, dataPath);
    request->send(200, "text/plain", "data.txt has been deleted.");
  });

  Events.onConnect([](AsyncEventSourceClient *client){
    if(client->lastId()){
      Serial.printf("Client reconnected! Last message ID that it got is: %u\n", client->lastId());
    }
    // send event with message "hello!", id current millis
    // and set reconnect delay to 1 second
    client->send("hello!", NULL, millis(), 10000);
  });
  server.addHandler(&Events);

  // Start server
  server.begin();
  Events.send(measureAcc().c_str(),"new_readings", millis());
}



void loop(){

  if ((millis() - lastTime) > timerDelay) {
    // Send Events to the client with the Sensor Readings Every 30 seconds
    Events.send("ping",NULL,millis());
    Events.send(measureAcc().c_str(),"new_readings" ,millis());
    String message = measureAcc() + ",";

    if ((getFileSize(LittleFS, dataPath))>= 3400){
      Serial.print("Too many data points, deleting file...");
      // Uncomment the next two lines if you don't want to delete the data file automatically.
      // It won't log more data into the file
      deleteFile(LittleFS, dataPath);
      appendFile(LittleFS, "/data.txt", message.c_str());
    }
    else{
      // Append new readings to the file
      appendFile(LittleFS, "/data.txt", message.c_str());
    }

    lastTime = millis();

    Serial.print(readFile(LittleFS, dataPath));
 }
}

SPIFSS DOSYA SİSTEMİ FONKSİYONLARI

// Read file from LittleFS
String readFile(fs::FS &fs, const char * path){
  Serial.printf("Reading file: %s\r\n", path);

  File file = fs.open(path, "r");
  if(!file || file.isDirectory()){
    Serial.println("- failed to open file for reading");
    return String();
  }
  
  String fileContent;
  while(file.available()){
    fileContent += file.readStringUntil('\n');
    break;     
  }
  file.close();
  return fileContent;
}

// Append data to file in LittleFS
void appendFile(fs::FS &fs, const char * path, const char * message){
  Serial.printf("Appending to file: %s\r\n", path);

  File file = fs.open(path, "a");
  if(!file){
    Serial.println("- failed to open file for appending");
    return;
  }
  if(file.print(message)){
    Serial.println("- message appended");
  } else {
    Serial.println("- append failed");
  }
file.close();
}

// Delete File
void deleteFile(fs::FS &fs, const char * path){
  Serial.printf("Deleting file: %s\r\n", path);
  if(fs.remove(path)){
    Serial.println("- file deleted");
  } else {
    Serial.println("- delete failed");
  }
}

// Get file size
int getFileSize(fs::FS &fs, const char * path){
  File file = fs.open(path, "r");

  if(!file){
    Serial.println("Failed to open file for checking size");
    return 0;
  }
  Serial.print("File size: ");
  Serial.println(file.size());

  return file.size();
}

INDEX.HTML DOSYASI

<!DOCTYPE html>
<html>
  <head>
    <title>ESP IOT DASHBOARD</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/png" href="favicon.png">
    <link rel="stylesheet" type="text/css" href="style.css">
    <script src="https://code.highcharts.com/highcharts.js"></script>
  </head>
  <body>
    <div class="topnav">
      <h1>ADXL345 SENSOR OKUMALARI</h1>
    </div>
    <div class="content">
      <div class="card-grid">
        <div class="card">
          <p class="card-title"> X and Y Axis</p>
          <div id="chart-xyAccl" class="chart-container"></div>
        </div>
      </div>
      <div class="button-grid"></div>
        <div class="card">
          <p>
            <a href="delete-data"><button class="button-delete">Delete Data</button></a>
            <a href="view-data"><button class="button-data">View Raw Data</button></a>
          </p>
        </div>
      </div>
    </div>
  </body>
    <script src="script.js"></script>
</html>

script.js JAVASCRIPT DOSYASI

// Get current sensor readings when the page loads
window.addEventListener('load', getReadings);

// Create Temperature Chart
var chartXY = new Highcharts.Chart({
  chart:{
    renderTo:'chart-xyAccl'
  },
  series: [
    {
      name: 'ADXL345_X',
      type: 'line',
      color: '#101D42',
      marker: {
        symbol: 'circle',
        radius: 3,
        fillColor: '#101D42',
      }
    },
    {
      name: 'ADXL345_Y',
      type: 'line',
      color: '#00A6A6',
      marker: {
        symbol: 'square',
        radius: 3,
        fillColor: '#00A6A6',
      }
    },
  ],
  title: {
    text: undefined
  },
  xAxis: {
    type: 'datetime',
    dateTimeLabelFormats: { second: '%H:%M:%S' }
  },
  yAxis: {
    title: {
      text: 'X/Y eksenleri ivmeleri (ADC verisi)'
    }
  },
  credits: {
    enabled: false
  }
});

//Plot XY Accelerations

function plot_xyAccl(timeValue, value1, value2) {

  console.log(timeValue);
  var x = new Date(timeValue*1000).getTime();
  console.log(x);

var y = Number(value1);
console.log(value1);

if(chartXY.series[0].data.length > 40) {
  chartXY.series[0].addPoint([x, y], true, true, true);
} else {
  chartXY.series[0].addPoint([x, y], true, false, true);
}

y = Number(value2);
console.log(value2);
if(chartXY.series[1].data.length > 40) {
  chartXY.series[1].addPoint([x, y], true, true, true);
} else {
  chartXY.series[1].addPoint([x, y], true, false, true);
}
}

// Function to get current readings on the webpage when it loads for the first time

function getReadings(){
  var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
      if (this.readyState == 4 && this.status == 200) {
        var myObj = JSON.parse("["+this.responseText.slice(0, -1)+"]");
        var len = myObj.length;
        if(len > 40) {
          for(var i = len-40; i<len; i++){
            plot_xyAccl(myObj[i].time, myObj[i].xAccl, myObj[i].yAccl);
          }
        }
        else {
          for(var i = 0; i<len; i++){
            plot_xyAccl(myObj[i].time, myObj[i].xAccl, myObj[i].yAccl);  
          }
        } 
      }
    };
  xhr.open("GET", "/readings", true);
  xhr.send();
}

if (!!window.EventSource) {
  var source = new EventSource('/Events');

  source.addEventListener('open', function(e) {
    console.log("Events Connected");
    }, false);

  source.addEventListener('error', function(e) {
      if (e.target.readyState != EventSource.OPEN) {
        console.log("Events Disconnected");
      }
    }, false);

  source.addEventListener('message', function(e) {
    console.log("message", e.data);
    }, false);

  source.addEventListener('new_readings', function(e) {
    console.log("new_readings", e.data);
    var myObj = JSON.parse(e.data);
    console.log(myObj);
    plot_xyAccl(myObj.time, myObj.xAccl, myObj.yAccl);
    }, false);
}



İSTEMCİ EKRAN GÖRÜNTÜLERİ

SONUÇ

Bu haliyle pek fazla ayrıntı vermemiş olduğumun farkındayım. Yapacağım güncellemeler ile daha faydalı hale getireceğimi umuyorum.
Yapılması gereken bir kaç şey daha var:
– Kayıtların flash bellekte tutulması yerine SD kart üzerinde tutulması daha uygun olacak. Bu haliyle flash bellek ömrü ve kapasitesi açısından çok sağlıklı değil.
– ADXL345 verileri ADC lerden okunduğu haliyle ham olarak kullanılıyor. Bu değerlerin kalibre edilip, ivme ya da eğim açısı gibi daha anlamlı hale getirilerek görüntülenmesi gerekiyor.

BUYAYININ SONU – Selçuk Özbayraktar Temmuz 2021