ESP32 - TSL2561 - BME280 - OLED

09.03.2024 Arduino #esp32 #tls2661 #bme280 #oled #senzor #teplota #vlhkost #tlak #světlo

Ukázka jak pracovat se senzory, světla, teploty, vlhkosti, tlaku a zobrazení dat na OLED displeji. Pokud se využijí komponenty od LaskaKitu, tak je to velmi snadné. LaskaKit mě baví, fakt skvělý koncept.


Propojení komponent od LaskaKitu pomocí konektoru uSup je pro jednoduché odzkoušení, ale i praktické využití fakt skvělý koncept. Žádný breadboardy, dráty, studeňáky. Zapíchá se pár konektorů a programujeme. V tomto příspěvku je ukázka programování třech senzorů zapojených s ESP32:

  1. Senzor jasu TSL2561.
  2. Senzor teploty, tlaku, vlhkosti a výšky BME280
  3. OLED display pro zobrazení hodnot měřených veličin.

img_3142 

Samotné zapojení je tedy jednoduché.

OLED a senzory zapojené pomocí I2C

OLED technologie je relativně nová a nabízí lepší kvalitu než stará technologie LCD. OLED moduly jsou dostupné v široké škále velikostí a funkcí. Protože všechny moduly podporují protokol I2C jako prostředek pro komunikaci, kód a zapojení všech z nich je naprosto stejné. Jediný rozdíl je v tom, že se musí zvážit velikost displeje, aby se obsah, který chceme zobrazit, na něj správně vešel.

Všechny další senzorické moduly podporují také protokol I2C.

Interintegrovaný obvod (IIC), který se běžně nazývá I2C (I na druhou C) vyvinutý společností Philips v 80. letech jako sběrnice pro výměnu dat používanou k přenosu dat mezi centrální procesorovou jednotkou (CPU) nebo mikrokontrolérovou jednotkou (MCU) zařízení a periferní čipy. V podstatě to bylo zaměřeno na televizní aplikace. Pro svou jednoduchost se stala tak populární, že se po čase stala jedním z primárních mechanismů přenosu dat pro CPU a MCU a periferní zařízení, která nejsou nezbytnou součástí stejné desky plošných spojů a jsou k ní připojena drátem (např. zobrazovací moduly atd.).

I2C se skládá z komunikační sběrnice tvořené dvěma vodiči, která podporuje obousměrný přenos dat mezi nadřízeným a několika podřízenými zařízeními. Typicky má hlavní uzel na starosti řízení sběrnice – což se ve skutečnosti provádí generováním synchronizačního signálu na sériové lince hodin (SCL). Je to signál, který by master během přenosu neustále posílal a všechny ostatní uzly připojené ke sběrnici jej použijí k synchronizaci své komunikace a detekci rychlosti sběrnice. Data jsou přenášena mezi master a slave přes sériovou datovou linku (SDA). Přenosová rychlost může být až 3,4 Mbps. Všechna zařízení, která chtějí přenášet data přes I2C, by měla mít jedinečnou adresu a mohou fungovat buď jako vysílač nebo přijímač v závislosti na funkci zařízení. Například OLED zobrazovací modul je přijímač, který přijímá některá data a zobrazuje je, zatímco teplotní senzor je transceiver, který odesílá zachycenou teplotu přes I2C sběrnici. Normálně je hlavní zařízení zařízení, které zahajuje přenos dat na sběrnici a generuje hodinové signály, které přenos umožňují. Během tohoto přenosu je každé zařízení adresované tímto masterem považováno za slave a čte tato data.

Když chce uzel poslat nějaká data, úplně první byte dat by měla být adresa přijímače a teprve potom přijdou skutečná data. To znamená, že abychom mohli odeslat data do výstupního zařízení pomocí I2C (např. I2C OLED zobrazovací modul), měli bychom nejprve najít jeho I2C adresu a to je to, co uděláme jako první v dalších krocích.

Pokud máte zájem dozvědět se více o detailech a teoriích navštivte stránku o I2C sběrnici

Důležitá poznámka o zařízeních s podporou I2C je, že způsob, jakým bysme je měli připojit k ESP32, jsou všechny stejné. Je to proto, že ESP32 provozuje svou I2C komunikaci pouze na konkrétních pinech. V tomto tutoriálu se využívá připojení pomocí uSip konektoru, ale ten je opět propojen s pinem 22 jako SCK a 21 jako SDA.

Jako první krok k připojení k zařízení s podporou I2C potřebujeme znát adresu modulu. Chceme-li tak učinit, po připojení modulu k ESP32 si můžeme nahrát kód, který provede skenování připojených I2C zařízení. Tento kód obsahuje knihovnu Wire, což je knihovna obsažená v VSC PlatformIO IDE, která zpracovává I2C komunikaci. Snaží se prohledat připojená I2C zařízení a odeslat jejich adresu přes sériový port do vašeho počítače. K jeho výstupu tedy můžete přistupovat pomocí nástroje Serial Monitor.

Adresy I2C zařízení jsou omezeny od 1 do 126. Tento kód se jednoduše pokusí připojit ke každému zařízení v pořadí (bez přenosu jakýchkoli dat) a poté zkontrolovat, zda nedošlo k nějaké chybě hlášené základní knihovnou při připojení k poskytnuté adrese. Pokud nedojde k žádné chybě, vytiskne adresu jako dostupný modul pro připojení. Také je třeba poznamenat, že prvních 15 adres je rezervovaných, takže je přeskočí a vytiskne pouze ty nad tímto rozsahem. Pamatujme, že adresy těchto I2C modulů jsou pevně zakódovány na zařízení a nelze je změnit. Bylo by tedy dobré si je poznamenat.

Knihovna Wire zvládne nízkoúrovňovou komunikaci se zařízeními I2C. Pokud se chcete připojit ke konkrétnímu zařízení, abyste z něj/do něj mohli číst/zapisovat data, normálně byste použili knihovnu poskytovanou společností, která tento modul původně postavila. Tato knihovna zpracovává všechny detaily I2C komunikace s daným modulem a umožňuje nám soustředit se více na naše podnikání, které v tomto případě zobrazuje data tak, jak chceme.

Společnost Adafruit, která vyrábí původní verzi OLED zobrazovacích modulů, poskytuje knihovny s názvem Adafruit SH110X pro zobrazení dat na těchto monochromatických displejích. Než tedy začneme kódovat, musíme tuto knihovnu nainstalovat. Existuje také další knihovna s názvem Adafruit GFX Library, která zpracovává více grafických věcí na nízké úrovni a je interně používána Adafruit SSD1306. Musíme mít obě knihovny nainstalované.

Senzor svítivosti TSL2561

Snímač TSL2561 je osazen na LaskaKit modulu standardní velikosti a rozměrů, kde naleznete dva uŠup konektory a také místo pro připájení standardního 2,54mm rozteče samec/female. Tento modul lze pohodlně propojit s dalšími moduly.

Napájecí napětí modulu a tedy i senzoru je od 2,7V do 3,6V. Adresa snímače je 0x29, 0x39 (výchozí) nebo 0x49 - záleží na pájecím můstku umístěném na zadní straně modulu. To umožní změnit adresu senzoru a připojit tři moduly TSL2561 současně, každý s jinou nastavenou adresou I2C. Takže u jednoho snímače lze ponechat výchozí nastavení, u druhého snímače se připojí středová ploška pájecího můstku na jednu stranu, u třetího snímače se připojí středová ploška pájecího můstku na druhou stranu.

Další pájecí můstek určuje, zda budou pull-up rezistory připojeny na I2C sběrnici nebo ne. Standardně jsou rezistory zapojeny. Pokud je chcete odstranit, stačí proříznout cestu mezi pájecími můstky.

Práce se senzorem BME280 je uvedena v předchozím článku.

Programování

Programový kód je omezen pouze na nejnutnější části. Celá aplikace umožňuje zobrazení mřených veličin na OLED displeji, ale také je přístupné webové rozhraní přes server na ESP32.

main.cpp

#include <WiFi.h>
#include <Wire.h>
#include <math.h>
#include <Adafruit_GFX.h>     
#include <Adafruit_SH110X.h> 
#include <Adafruit_BME280.h>
#include <Adafruit_TSL2561_U.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <PubSubClient.h>
#include <htmlui.h>

#define i2c_Address 0x3c

// Light sensor
Adafruit_TSL2561_Unified tsl = Adafruit_TSL2561_Unified(TSL2561_ADDR_FLOAT, 12345); 
// BME 280
Adafruit_BME280 bme;
// Display
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire, -1);

#define SEALEVELPRESSURE_HPA (1013.25)
#define CORRECT_TEMP (0.40)

String temp;
String hum;
String press;
String alt;

const char* ssid = "WeatherData";
const char* password = "12345678";

AsyncWebServer server(80);

float getLight(){
  sensors_event_t event;
  tsl.getEvent(&event);
  float actLight;
 
  /* Display the results (light is measured in lux) */
  if (event.light){
    // Serial.print(event.light); Serial.println(" lux");
    actLight = event.light;
  }else{
    /* If event.light = 0 lux the sensor is probably saturated
       and no reliable data could be generated! */
    //Serial.println("Sensor overload");
    actLight = 0;
  }
  return actLight;
}

String getData(){
  temp=String(bme.readTemperature()-CORRECT_TEMP);
  hum=String(bme.readHumidity());
  press=String(bme.readPressure()/100.0F);
  alt=String(bme.readAltitude(SEALEVELPRESSURE_HPA));
  /*
  Serial.print("Temperature: ");
  Serial.println(temp);
  Serial.print("Humidity: ");
  Serial.println(hum);
  Serial.print("Pressure: ");
  Serial.println(press);
  Serial.print("Altitude: ");
  Serial.println(alt);
  */

  String result = temp+";"+hum+";"+press+";"+alt+";"+getLight();
  return result;
}
void viewDataOnDisplay(String data){
  char *splString;
  const char *delm = (char*)";";
  splString = strtok(strdup(data.c_str()),delm);

  char *temp = splString;
  char *hum = strtok(NULL, delm);
  char *press = strtok(NULL, delm);
  char *alt = strtok(NULL, delm);
  char *valLight = strtok(NULL, delm);

  display.setCursor(0,0);
  display.println((String)"Temp: " + temp + " C");
  display.setCursor(0,10);
  display.println((String)"Hum: " + hum + " %");
  display.setCursor(0,20);
  display.println((String)"Press: " + press + " hPa");
  display.setCursor(0,30);
  display.println((String)"Alt: " + alt + " m");
  display.setCursor(0,40);
  display.println((String)"Light: " + valLight + " Lux");
  display.display();  
  delay(500);
  display.clearDisplay();
}

void setup() {
  Serial.begin(115200);

  // Server
  WiFi.softAP(ssid, password);

  IPAddress IP = WiFi.softAPIP();
  Serial.print("AP IP address: ");
  Serial.println(IP);

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html);
  });

  server.on("/getdata", HTTP_GET, [] (AsyncWebServerRequest *request) {
    request->send(200, "text/plain", getData().c_str());
  });

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

  bool status = bme.begin(0x77);  // 0x77 OR 0x76 for left pad on board
  if (!status) {
     Serial.println("Could not find a valid BME280 sensor, check wiring, address, sensor ID!");
     Serial.print("      SensorID was: 0x"); Serial.println(bme.sensorID(),16);
     Serial.print("         ID of 0xFF probably means a bad address, a BMP 180 or BMP 085\n");
     Serial.print("         ID of 0x56-0x58 represents a BMP 280,\n");
     Serial.print("         ID of 0x60 represents a BME 280.\n");
     Serial.print("         ID of 0x61 represents a BME 680.\n");
    while (1);
  }

  display.begin(i2c_Address, true); 
  display.clearDisplay(); 
  display.setTextColor(SH110X_WHITE); 
  display.setTextSize(1); 

}

void loop() {
  temp=String(bme.readTemperature()-CORRECT_TEMP);
  hum=String(bme.readHumidity());
  press=String(bme.readPressure()/100.0F);
  alt=String(bme.readAltitude(SEALEVELPRESSURE_HPA));

  viewDataOnDisplay(getData());

}

htmlui.h

const char index_html[] PROGMEM = R"rawliteral(
 <!DOCTYPE HTML><html>
    <head>
      <title>ESP Web Server</title>
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <style>

            html {
                box-sizing: border-box;
                -ms-overflow-style: scrollbar;
            }

            *,
            *::before,
            *::after {
                box-sizing: inherit;
            }
            .container{ width: 100%;padding-right: 15px;padding-left: 15px;margin-right: auto;margin-left: auto;}

            .row { display: -ms-flexbox; display: flex; -ms-flex-wrap: wrap; flex-wrap: wrap; margin-right: -15px;margin-left: -15px;}

            .no-gutters {margin-right: 0;margin-left: 0;}

            .no-gutters > .col,
            .no-gutters > [class*="col-"] { padding-right: 0;padding-left: 0;}

            .col-1, .col-2, .col-3, .col-4, .col-5, .col-6, .col-7, .col-8, .col-9, .col-10, .col-11, .col-12, .col,
            .col-auto { position: relative; width: 100%; padding-right: 15px;padding-left: 15px; }

            .col {-ms-flex-preferred-size: 0; flex-basis: 0;-ms-flex-positive: 1;flex-grow: 1;max-width: 100%;}
            .col-auto {-ms-flex: 0 0 auto; flex: 0 0 auto;width: auto;max-width: 100%;}
            .col-1 {-ms-flex: 0 0 8.333333%;flex: 0 0 8.333333%; max-width: 8.333333%;}
            .col-2 {-ms-flex: 0 0 16.666667%;flex: 0 0 16.666667%;max-width: 16.666667%;}
            .col-3 {-ms-flex: 0 0 25%;flex: 0 0 25%; max-width: 25%;}
            .col-4 { -ms-flex: 0 0 33.333333%;flex: 0 0 33.333333%; max-width: 33.333333%;}
            .col-5 {-ms-flex: 0 0 41.666667%;flex: 0 0 41.666667%;max-width: 41.666667%; }
            .col-6 {-ms-flex: 0 0 50%;flex: 0 0 50%; max-width: 50%;}
            .col-7 {-ms-flex: 0 0 58.333333%;flex: 0 0 58.333333%;max-width: 58.333333%;}
            .col-8 {-ms-flex: 0 0 66.666667%;flex: 0 0 66.666667%;max-width: 66.666667%;}
            .col-9 { -ms-flex: 0 0 75%;flex: 0 0 75%; max-width: 75%; }
            .col-10 {-ms-flex: 0 0 83.333333%;flex: 0 0 83.333333%;max-width: 83.333333%;}
            .col-11 {-ms-flex: 0 0 91.666667%;flex: 0 0 91.666667%;max-width: 91.666667%;}
            .col-12 {-ms-flex: 0 0 100%;flex: 0 0 100%; max-width: 100%; }

            .m-0 {margin: 0 !important;}
            .mt-0,.my-0 {margin-top: 0 !important; }
            .mr-0,.mx-0 {margin-right: 0 !important;}
            .mb-0,.my-0 {margin-bottom: 0 !important;}
            .ml-0,.mx-0 {margin-left: 0 !important;}
            .m-1 { margin: 0.25rem !important;}
            .mt-1, .my-1 { margin-top: 0.25rem !important; }
            .mr-1,.mx-1 { margin-right: 0.25rem !important;}
            .mb-1, .my-1 {margin-bottom: 0.25rem !important; }
            .ml-1,.mx-1 {margin-left: 0.25rem !important; }
            .m-2 {margin: 0.5rem !important;}
            .mt-2,.my-2 {margin-top: 0.5rem !important;}
            .mr-2,.mx-2 { margin-right: 0.5rem !important; }
            .mb-2,.my-2 {margin-bottom: 0.5rem !important;}
            .ml-2,.mx-2 {margin-left: 0.5rem !important;}
            .m-3 { margin: 1rem !important;}
            .mt-3,.my-3 {margin-top: 1rem !important;}
            .mr-3,.mx-3 {margin-right: 1rem !important;}
            .mb-3,.my-3 {margin-bottom: 1rem !important;}
            .ml-3, .mx-3 {margin-left: 1rem !important; }
            .m-4 { margin: 1.5rem !important; }
            .mt-4, .my-4 {margin-top: 1.5rem !important;}
            .mr-4,.mx-4 { margin-right: 1.5rem !important;}
            .mb-4, .my-4 {margin-bottom: 1.5rem !important; }
            .ml-4,.mx-4 {margin-left: 1.5rem !important;}
            .m-5 {margin: 3rem !important; }
            .mt-5,.my-5 {margin-top: 3rem !important;}
            .mr-5,.mx-5 {margin-right: 3rem !important;}
            .mb-5, .my-5 { margin-bottom: 3rem !important;}
            .ml-5,.mx-5 { margin-left: 3rem !important; }
            .p-0 { padding: 0 !important;}
            .pt-0, .py-0 {padding-top: 0 !important;}
            .pr-0,.px-0 {padding-right: 0 !important;}
            .pb-0,.py-0 {padding-bottom: 0 !important;}
            .pl-0,.px-0 {padding-left: 0 !important;}
            .p-1 { padding: 0.25rem !important;}
            .pt-1, .py-1 {padding-top: 0.25rem !important;}
            .pr-1, .px-1 {padding-right: 0.25rem !important;}
            .pb-1,.py-1 { padding-bottom: 0.25rem !important;}
            .pl-1,.px-1 {padding-left: 0.25rem !important;}
            .p-2 {padding: 0.5rem !important;}
            .pt-2,.py-2 {padding-top: 0.5rem !important;}
            .pr-2, .px-2 {padding-right: 0.5rem !important;}
            .pb-2, .py-2 {padding-bottom: 0.5rem !important;}
            .pl-2,.px-2 {padding-left: 0.5rem !important;}
            .p-3 {padding: 1rem !important;}
            .pt-3, .py-3 {padding-top: 1rem !important;}
            .pr-3,.px-3 {padding-right: 1rem !important;}
            .pb-3, .py-3 {padding-bottom: 1rem !important; }
            .pl-3,.px-3 { padding-left: 1rem !important;}
            .p-4 {padding: 1.5rem !important; }
            .pt-4, .py-4 {padding-top: 1.5rem !important;}
            .pr-4,.px-4 {padding-right: 1.5rem !important;}
            .pb-4,.py-4 {padding-bottom: 1.5rem !important;}
            .pl-4,.px-4 {padding-left: 1.5rem !important; }
            .p-5 { padding: 3rem !important; }
            .pt-5, .py-5 {padding-top: 3rem !important;}
            .pr-5,.px-5 {padding-right: 3rem !important;}
            .pb-5,.py-5 {padding-bottom: 3rem !important;}
            .pl-5,.px-5 {padding-left: 3rem !important;}
            body{
                font-family: Arial, Helvetica, sans-serif;
            }
            .btn {
                display: inline-block;
                font-weight: 400;
                color: #212529;
                text-align: center;
                vertical-align: middle;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
                user-select: none;
                background-color: transparent;
                border: 1px solid transparent;
                padding: 0.375rem 0.75rem;
                font-size: 1rem;
                line-height: 1.5;
                border-radius: 0.25rem;
                transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
            }
            .btn:hover {
                color: #212529;
                text-decoration: none;
            }
            .btn-primary {color: #fff;background-color: #007bff; border-color: #007bff;}
            .btn-primary:hover {color: #fff;background-color: #0069d9;border-color: #0062cc;}
            .btn-success {color: #fff;background-color: #28a745;border-color: #28a745;}
            .btn-success:hover { color: #fff;background-color: #218838;border-color: #1e7e34;}
            .btn-danger {color: #fff;background-color: #d9534f;border-color: #d43f3a;}
            .btn-danger:hover {color: #fff;background-color: #c9302c;border-color: #ac2925;}

            input,button,select,optgroup,textarea {  margin: 0;font-family: inherit;font-size: inherit;line-height: inherit;}
            textarea {overflow: auto;resize: vertical;}
            textarea.form-control {height: auto;}

            .toggle {position: relative; display: inline-block; width: 48px; height:26px; float:right; margin-top:20px;} 
            .toggle input {display: none;}
            .shade {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; }
            .shade:before {position: absolute; content: ""; height: 18px; width: 18px; left:4px; bottom: 4px; background-color: #fff; -webkit-transition: .4s; transition: .4s;}
            input:checked+.shade {background-color: #2196F3}
            input:checked+.shade:before {-webkit-transform: translateX(22px); -ms-transform: translateX(22px); transform: translateX(22px)}
      </style>
    </head>
    <body onload="iniPage()">
        <div class="container">
          <div id="content">
              <div class="row">
                <div class="col-12">
                  <h2>Weather</h2>
                </div>
              </div>
              <div class="row mt-4">
                <label class="col-4 ">Temp:</label>
                <div id="temp">%temp%</div>
              </div>
              <div class="row mt-4">
                <label class="col-4 ">Hum:</label>
                <div id="hum">%hum%</div>
              </div>
              <div class="row mt-4">
                <label class="col-4 ">Press:</label>
                <div id="press">%press%</div>
              </div>
              <div class="row mt-4">
                <label class="col-4 ">Alt:</label>
                <div id="alt">%alt%</div>
              </div>   
              <div class="row mt-4">
                <label class="col-4 ">Light:</label>
                <div id="light">%light%</div>
              </div>

          </div>
        </div>
    <script>

    function iniPage(){
      var xhttp = new XMLHttpRequest();
      xhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
          var stateArr = this.responseText.split(";");
          document.getElementById("temp").innerHTML = stateArr[0];
          document.getElementById("hum").innerHTML = stateArr[1];
          document.getElementById("press").innerHTML = stateArr[2];
          document.getElementById("alt").innerHTML = stateArr[3];
          document.getElementById("light").innerHTML = stateArr[4];
        }
      };
      xhttp.open("GET", "/getdata", true);
      xhttp.send();     
    }
    
    setInterval(iniPage, 5000);

    </script>
    </body>
    </html>
)rawliteral";

platformio.ini

[env:upesy_wroom]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
build_type = debug
lib_deps = 
	adafruit/Adafruit Unified Sensor@^1.1.6
	adafruit/Adafruit BME280 Library@^2.2.2
	adafruit/Adafruit TSL2561@^1.1.2
	sparkfun/SparkFun AS3935 Lightning Detector Arduino Library@^1.4.8
         adafruit/Adafruit GFX Library@^1.11.9
	adafruit/Adafruit SH110X@^2.1.10
        esphome/ESPAsyncWebServer-esphome@^3.1.0
        knolleary/PubSubClient@^2.8

Literatura:

https://github.com/LaskaKit/OLED-1_3inch-I2C

https://github.com/LaskaKit/TSL2561-Luminosity-Sensor/tree/main