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.