Fyzikální kabinet FyzKAB
TechHobby ESP32 + MicroPython ESP32: Čidla a senzory v MicroPythonu Barometrické čidlo BMP180 v MicroPythonu

ESP32: Barometrické čidlo BMP180 v MicroPythonu

25. díl „volného seriálu“ článků

Máme po čase další díl našeho „volného“ seriálu článků věnovaných MicroPythonu na modulu ESP32. V několika předchozích článcích jsme se již dotkli toho, jak připojovat různé periferie k modulu ESP32, a v jednom případě už dokonce i přes I2C sběrnici. Tentokrát se na sběrnici I²C však zaměříme více, protože bude hrát klíčovou roli při komunikaci s barometrickým senzorem BMP180. Pokusíme se nejen připojit toto čidlo k modulu ESP32, ale hlavně si ukážeme, jak v MicroPythonu vyřešit problém načítání měřených veličin.

Pochopitelně na závěr zkusíme vytvořit i nějaké komplexnější „praktické“ řešení. Tento rozsáhlejší projekt, který by měl využít mnoho z toho, co všechno jsme se během celého našeho seriálu naučili. A vězte, že jsme se toho během těch dosavadních pětadvaceti díl už o MicroPythonu na modulu ESP32 docela dost naučili!

Barometrické a teplotní čidlo BMP180

Čidlo BMP180 je malý a levný barometrický senzor, který kromě atmosférického tlaku dokáže měřit i teplotu. Jeho přesnost asi nedosahuje úrovně profesionálních meteorologických stanic, ale pro běžné amatérské projekty – například domácí meteostanici, měření nadmořské výšky nebo řízení větrání – je svou přesností více jak dostačující! Hlavní výhodou pro naše hrátky je především jeho dostupnost, malé rozměry, nízká cena, a díky I²C rozhraní i snadná komunikace.

cidlo BMP180 - rub a lic

Sběrnice I²C

O sběrnici I²C jsme se již v jednom z předešlých článků zmínili, ale protože to dnes bude především jen o čidlu pracujícím na sběrnici I²C, bude lepší si některé základní informace zopakovat.

I²C (Inter-Integrated Circuit) je dvoudrátová sériová komunikační sběrnice navržená pro jednoduché propojení mikrokontrolérů s periferními zařízeními, jako jsou senzory, displeje nebo paměti EEPROM. Pro přenos dat využívá dvě linky:

  • SDA (Serial Data) – datová linka
  • SCL (Serial Clock) – hodinová linka

Obě linky jsou typu open-drain a vyžadují tedy externí pull-up rezistory (typicky 4,7–10 kΩ) připojené na napájecí napětí sběrnice (obvykle 3,3 V nebo 5 V).

I²C využívá sběrnicovou topologii, kde jedno zařízení vystupuje jako master (řídící jednotka, např. modul ESP32), a jedno nebo více zařízení jako slave (podřízené periferie, zpravidla nějaké senzory). Každý slave má unikátní 7bitovou nebo 10bitovou adresu, kterou určuje výrobce nebo ji lze částečně nastavit hardwarově (např. pomocí propojek nebo pájecích plošek). Výhodou I²C je jednoduchá topologie (sběrnice), která poskytuje možnost připojení více zařízení současně na stejné vodiče. Další výhodou je široká podpora této sběrnice u různých čidel a mikrokontrolérů. Pro kratší vzdálenosti a nízké až střední rychlosti je sběrnice I²C velmi efektivní řešení.

Modul ESP32 podporuje více I²C rozhraní současně (I2C(0) a I2C(1)), přičemž defaultní piny jsou:

Modul ESP32
I2C(0) I2C(1)
SDA GPIO21 GPIO33
SCL GPIO22 GPIO32

MicroPython umožňuje použít jak hardwarové I²C (modul I2C), tak i softwarovou emulaci (modul SoftI2C), která dovoluje provoz sběrnice na libovolných digitálních pinech.

Rychlosti a provozní režimy:
  • Standard Mode – až 100 kbps
  • Fast Mode – až 400 kbps
  • Fast Mode Plus – až 1 Mbps (ESP32 může podporovat)
  • High-Speed Mode – až 3,4 Mbps (méně běžné, ne všechny čipy podporují)
Výhody:
Jednoduché vedení sběrnice (2 vodiče pro všechna zařízení)
Podpora více zařízení na jedné sběrnici
Snadná implementace a široká dostupnost čidel
Omezení:
Omezená délka sběrnice (typicky do 1 metru, závisí na rychlosti a kapacitě vedení)
Omezená přenosová rychlost (v porovnání s např. SPI)
Náchylnost na rušení při delším vedení nebo při chybějících pull-up rezistorech

Připojení čidla BMP180 k modulu ESP32

Senzor BMP180, jak bylo naznačeno, komunikuje přes sběrnici I²C. Modul ESP32 má naštěstí několik hardwarově podporovaných I²C pinů, pro I2C(0) to jsou obvykle GPIO21 (SDA) a GPIO22 (SCL). Pokud tedy nemáme zvláštní důvod, doporučujeme začít právě s těmito piny.

Schéma zapojení bude tedy jednoduché (viz obrázek a tabulka propojení)

zapojeni BMP180-ESP32
BMP180 ESP32
VCC 3V3
GND GND
SCL GPIO22
SDA GPIO21
Poznámka:
Některé moduly čidla BMP180 mají na desce i pull-up rezistory mezi Ucc a piny SCL a SDA, pokud bychom měli modul, který tyto rezistory nemá, můžeme je doplnit externě (např. 4.7 kΩ mezi SDA/SCL a 3.3V). Pro první pokusy to ale často funguje i bez nich. 😊

Modul ESP32 i jeho MicroPython nám dává i možnost – použít tzv. softwarovou I²C sběrnici pomocí třídy SoftI2C. Budeme si pamatovat, že, pokud bychom potřebovali použít i piny bez hardwarové podpory I²C, můžeme použít modul I2CSoft. Pomocí SoftI2C pak lze libovolně zvolit, které piny použijeme pro SDA a SCL – stačí to nastavit při inicializaci softwarové I²C sběrnice.

To se hodí v případě, že potřebujeme klasické piny uvolnit nebo už na nich máme připojeného něco jiného. I když, jak už o sběrnici I²C víme, senzory se na ni dají připojovat „neomezeně“, protože komunikace se řídí pomocí adresace. Problém by však nastal při připojení dvou čidel BMP180 na stejnou I²C sběrnici, neboť adresa obou čidel BMP180 je pevně stanovena výrobcem (hodnota 119).

Načítání dat z BMP180 v MicroPythonu

Máme-li po elektrické stránce vše připojeno, můžeme začít programovat. Nejdříve se pustíme do jednoduchého programu, který nám ukáže, jak ze senzoru BMP180 načíst hodnoty teploty a tlaku. Senzory komunikující na jakékoliv sběrnici, a ani I²C není výjimkou, je dobré řídit pomocí již existující knihovny. Zkusíme se tedy podívat po internetových komunitách a najít pro čidlo BMP180 již hotovou knihovnu. Knihovnu pojmenovanou bmp180.py najdeme hned v několika článcích různých autorů. Řada z citovaných knihoven je běžně dostupná na GitHubu nebo v komunitních zdrojích MicroPythonu. Ale jako už jsme viděli kupříkladu u knihovny věnované I²C ovladači serva, ne vždy je knihovna přímo použitelná pro modul ESP32 a ne vždy je použitelná pro aktuální verzi firmwaru MicroPythonu na modulu ESP32.

Bohužel tato situace nastává i v případě knihovny pro modul BMP180. Knihovna bmp180.py, kterou jsme našli hned v několika různých verzích (a z několika různých zdrojů), vždy vyvolaly chybu programu. A to, i když jsme ji použili přímo s jejím ukázkovým programem daného autora! Nakonec se ukázalo, že ve všech případech jde o problém s inicializací I²C sběrnice. Skoro se nám zdá, že asi v některém z posledních firmwarů MicroPythonu pro ESP32 došlo k nějaké změně v modulu I2C, čímž se tyto knihovny staly s dnešním MicroPythonem modulu ESP32 nekompatibilní. Ale je to jen naše spekulace!

Řešením vzniklé situace, podobně jako u knihovny pro ovládání serva pomocí I²C, byla naše úprava nefungující knihovny. Dále tedy uvádíme upravenou (opravenou?) knihovnu, která nyní (zatím?) funguje. Doufejme, že za nějaký čas se díky nějaké podobně nemilé události, naše knihovna nestane jen dalším nefunkčním kódem, jako tomu je u knihoven bmp180.py, na které jsme při psaní tohoto článku narazili. 😒

Upravený modul bmp180_ext.py

Když už jsme museli zasáhnout do kódu původní knihovny bmp180.py, abychom ji vůbec zprovoznili, zkusili jsme ji upravit trochu více. Do původní knihovny jsme přidali funkci, které se jistě hodí všem, kteří chtějí měřit tlak v pevně umístěné meteorologické stanici s jasně známou nadmořskou výškou. V takové situaci pak dává smysl přepočet tlaku na nadmořskou hladinu, co ale původní knihovna neumožňovala. V původní knihovně bylo možné jen ze zadané výchozí hodnoty tlaku na hladině moře (konstanta baseline) naopak dopočítat nadmořskou výšku.

Druhou změnou byla úprava výpočtu oné nadmořské výšky. Ne nadarmo bychom byli Fyzikální kabinet, abychom někde „nevyškrábli“ přesnější vzoreček! 😊

Funkce sea_level_pressure()

Kromě změny metody určení nadmořské výšky z atmosférického tlaku jsme do knihovny přidali funkci sea_level_pressure(), která přepočítává tlak na hladinu moře. Když měříme atmosférický tlak, měříme ho ve výšce, kde se právě nacházíme. Ale pokud chceme tuto hodnotu porovnávat s jinými měřeními hodnotami (například s údaji z jiné meteostanice v našem městě), musíme se shodnout na společné referenci – a tou je tlak přepočtený na hladinu moře. Proto když máme pouze „surovou“ hodnotu tlaku v místě měření, moc nám toho sama o sobě neřekne. Abychom zjistili, jaká je skutečná meteorologická situace (například blíží-li se tlaková níže), musíme tuto hodnotu přepočítat tak, jako bychom ji měřili u moře. K přepočtu na tuto hodnotu nám slouží funkce sea_level_pressure().

Funkce využívá naměřenou hodnotu tlaku, teploty a zadanou nadmořskou výšku. Je-li funkce volána s parametrem nadmořské výšky (v metech) vrátí přepočet na hladinu moře dle této zadané nadmořské výšky. Pokud žádný parametr do funkce nezadáme, funkce si za nadmořskou výšku dosadí hodnotu, kterou si sama spočítá pomocí vlastnosti altitude z výchozí zadané referenční hodnoty tlaku. (viz dále popis metod a atributů upravené knihovny).

Metody a vlastnosti knihovny BMP180_ext.py

Tím, že jsme původní knihovnu nejen upravili, ale i rozšířili, nechceme ji dále prezentovat pod stejným názvem jako předešlé knihovny bmp180.py. Aby se zbytečně nepletla s těmi v tuto chvíli nefungujícími verzemi, přejmenovali jsme ji na bmp180_ext.py.

Ještě než uvedeme zdrojový kód knihovny bmp180_ext.py, představíme si její metody a vlastnosti.

Knihovna bmp180_ext.py obsahuje základní třídu BMP180. Tato třída umožňuje:

  • číst teplotu (°C)
  • číst tlak (Pa)
  • spočítat nadmořskou výšku (m)
  • vypočítat z nadmořské výšky tlak na hladině moře
  • nastavovat režim přesnosti (oversampling) načítání čidla BMP180
  • nastavit výchozí tlak na hladině moře (využito pro výpočet nadmořské výšky)

Knihovna se naimportuje do programu a pro přístup k čidlo BMP180 je třeba vytvořit objekt třídy BMP180. Zadaným parametrem je u tohoto objektu platný objekt I²C sběrnice (buď hardwarové, nebo softwarové). Přestože při volbě hardwarového řadiče sběrnice I²C by měly být v MicroPythonu již nastaveny výchozí piny linek SDA a SCL, je potřeba je v definici objektu sběrnice I²C uvést – viz následující ukázka kódu.

from machine import I2C, Pin
from bmp180_ext import BMP180

i2c_bus = I2C(0, scl=Pin(22), sda=Pin(21))
sensor = BMP180(i2c_bus)

Objekt sensor, pak získá pro nás využitelné následující metodu a atributy:

.altitude
Vypočítá a vrátí nadmořskou výšku podle barometrické rovnice, hodnota je uvedena v metrech. Využívá metody pro aktuální teplotu (temperature), tlak (pressure) a nastavenou výchozí hodnotu tlaku na hladině moře baseline.
.baseline
Nastavení atmosférického tlaku na hladině moře (v Pa) – využíváno např. pro výpočet nadmořské výšky.
Výchozí hodnota: 101325 (standardní tlak na hladině moře)
.blocking_read()
Spustí měření a počká, až budou data k dispozici.
Data je třeba načítat v rychlých smyčkách, pokud funkci pressure() spustíme jednou a pak znovu za 10 sekund, vrátí 10 sekund starou hodnotu. V takovém případě je třeba před měřením použít metodu blocking_read().
.oversample_sett
Nastavuje přesnost měření. Vyšší číslo = vyšší přesnost měření, ale delší odezva (a vyšší spotřeba) senzoru.
Možné hodnoty: 0, 1, 2, 3 (výchozí hodnota 3)
Hodnota 0 – načtení: 1 vzorek – čas měření: 4,5 ms
Hodnota 1 – načtení: 2 vzorky – čas měření: 7,5 ms
Hodnota 2 – načtení: 4 vzorky – čas měření: 13,5 ms
Hodnota 3 – načtení: 4 vzorky – čas měření: 25,5 ms
.pressure
Načte a vrátí atmosférický tlak, hodnota je uvedena v Pa (hodnota float), využívá aktuální teplotu a interní kalibraci.
.sea_level_pressure(altitude)
Načte aktuální atmosférický tlak a přepočítá jej pomocí barometrické rovnice na tlak na hladině moře. Využívá metody pro aktuální teplotu (temperature), tlak (pressure) a vstupní hodnotu nadmořské výšky (v metrech) zadanou ve vstupním parametru altitude. Není-li vstupní parametr zadán, dosadí se do něj výsledek z altitude.
.temperature
Načte čidlo a vrátí aktuální teplotu, hodnota je ve °C (hodnota float).

Kromě výše uvedených vlastností jsou k dispozici i některé další, ale ty pro běžné hrátky s čidlem již nejsou potřeba, např.:

.compvaldump()
Vrací seznam všech kalibračních konstant ze senzoru. Senzor BMP180 má ve své vnitřní EEPROM paměti uloženy kalibrační konstanty, které jsou jedinečné pro každý kus senzoru. Výrobce je tam uloží při kalibraci během výroby. Funkce vrací pole kalibračních hodnot. (Používá se pro ladění)

Následující programový kód je zdrojový výpis knihovny bmp180_ext.py. Kód je potřeba překopírovat do prostředí Thonny IDE a uložit do složky lib na modulu ESP32. Pro uložení knihovny do modulu ESP32 lze využít v menu Soubor volbu Uložit jako… nebo Uložit kopii… a zvolit volbu Micropython zařízení (viz obrázek).

ulozeni knihovny
Kód knihovny BMP180_ext.py (s ohledem na rozsah kódu jej uvádíme bez obarvení syntaxe)

# https://github.com/micropython-IMU/micropython-bmp180 (UPRAVENO!!!)
# Modifications: FyzKAB, 2025
# Modified to work on the ESP32 module (MicroPython firmware v1.25.0),
# adjusted altitude calculation,
# and added a method for sea-level pressure estimation.
'''
bmp180 is a micropython module for the Bosch BMP180 sensor. It measures
temperature as well as pressure, with a high enough resolution to calculate
altitude.
Breakoutboard: http://www.adafruit.com/products/1603
data-sheet: http://ae-bst.resource.bosch.com/media/products/dokumente/
bmp180/BST-BMP180-DS000-09.pdf

The MIT License (MIT)
Copyright (c) 2014 Sebastian Plamauer, oeplse@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
'''

from ustruct import unpack as unp
from machine import I2C, Pin
import math
import time

# BMP180 class
class BMP180():
    '''
    Module for the BMP180 pressure sensor.
    '''

    _bmp_addr = 119     # adress of BMP180 is hardcoded on the sensor

    # init
    def __init__(self, i2c_bus):

        # create i2c obect
        _bmp_addr = self._bmp_addr
        self._bmp_i2c = i2c_bus
        self.chip_id = self._bmp_i2c.readfrom_mem(_bmp_addr, 0xD0, 2)
        # read calibration data from EEPROM
        self._AC1 = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xAA, 2))[0]
        self._AC2 = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xAC, 2))[0]
        self._AC3 = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xAE, 2))[0]
        self._AC4 = unp('>H', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xB0, 2))[0]
        self._AC5 = unp('>H', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xB2, 2))[0]
        self._AC6 = unp('>H', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xB4, 2))[0]
        self._B1 = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xB6, 2))[0]
        self._B2 = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xB8, 2))[0]
        self._MB = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xBA, 2))[0]
        self._MC = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xBC, 2))[0]
        self._MD = unp('>h', self._bmp_i2c.readfrom_mem(_bmp_addr, 0xBE, 2))[0]

        # settings to be adjusted by user
        self.oversample_setting = 3
        self.baseline = 101325.0

        # output raw
        self.UT_raw = None
        self.B5_raw = None
        self.MSB_raw = None
        self.LSB_raw = None
        self.XLSB_raw = None
        self.gauge = self.makegauge()   # Generator instance
        for _ in range(128):
            next(self.gauge)
            time.sleep_ms(1)

    def compvaldump(self):
        '''
        Returns a list of all compensation values
        '''
        return [self._AC1, self._AC2, self._AC3, self._AC4, self._AC5, self._AC6,
                self._B1, self._B2, self._MB, self._MC, self._MD, self.oversample_setting]

    # gauge raw
    def makegauge(self):
        '''
        Generator refreshing the raw measurments.
        '''
        delays = (5, 8, 14, 25)
        while True:
            self._bmp_i2c.writeto_mem(self._bmp_addr, 0xF4, bytearray([0x2E]))
            t_start = time.ticks_ms()
            while (time.ticks_ms() - t_start) <= 5: # 5mS delay
                yield None
            try:
                self.UT_raw = self._bmp_i2c.readfrom_mem(self._bmp_addr, 0xF6, 2)
            except:
                yield None
            self._bmp_i2c.writeto_mem(self._bmp_addr, 0xF4, bytearray([0x34+(self.oversample_setting << 6)]))
            t_pressure_ready = delays[self.oversample_setting]
            t_start = time.ticks_ms()
            while (time.ticks_ms() - t_start) <= t_pressure_ready:
                yield None
            try:
                self.MSB_raw = self._bmp_i2c.readfrom_mem(self._bmp_addr, 0xF6, 1)
                self.LSB_raw = self._bmp_i2c.readfrom_mem(self._bmp_addr, 0xF7, 1)
                self.XLSB_raw = self._bmp_i2c.readfrom_mem(self._bmp_addr, 0xF8, 1)
            except:
                yield None
            yield True

    def blocking_read(self):
        if next(self.gauge) is not None: # Discard old data
            pass
        while next(self.gauge) is None:
            pass

    @property
    def oversample_sett(self):
        return self.oversample_setting

    @oversample_sett.setter
    def oversample_sett(self, value):
        if value in range(4):
            self.oversample_setting = value
        else:
            print('oversample_sett can only be 0, 1, 2 or 3, using 3 instead')
            self.oversample_setting = 3

    @property
    def temperature(self):
        '''
        Temperature in degree C.
        '''
        next(self.gauge)
        try:
            UT = unp('>H', self.UT_raw)[0]
        except:
            return 0.0
        X1 = (UT-self._AC6)*self._AC5/2**15
        X2 = self._MC*2**11/(X1+self._MD)
        self.B5_raw = X1+X2
        return (((X1+X2)+8)/2**4)/10

    @property
    def pressure(self):
        '''
        Pressure in mbar.
        '''
        next(self.gauge)
        self.temperature   # Populate self.B5_raw
        try:
            MSB = unp('B', self.MSB_raw)[0]
            LSB = unp('B', self.LSB_raw)[0]
            XLSB = unp('B', self.XLSB_raw)[0]
        except:
            return 0.0
        UP = ((MSB << 16)+(LSB << 8)+XLSB) >> (8-self.oversample_setting)
        B6 = self.B5_raw-4000
        X1 = (self._B2*(B6**2/2**12))/2**11
        X2 = self._AC2*B6/2**11
        X3 = X1+X2
        B3 = ((int((self._AC1*4+X3)) << self.oversample_setting)+2)/4
        X1 = self._AC3*B6/2**13
        X2 = (self._B1*(B6**2/2**12))/2**16
        X3 = ((X1+X2)+2)/2**2
        B4 = abs(self._AC4)*(X3+32768)/2**15
        B7 = (abs(UP)-B3) * (50000 >> self.oversample_setting)
        if B7 < 0x80000000:
            pressure = (B7*2)/B4
        else:
            pressure = (B7/B4)*2
        X1 = (pressure/2**8)**2
        X1 = (X1*3038)/2**16
        X2 = (-7357*pressure)/2**16
        return pressure+(X1+X2+3791)/2**4

    # Fyzikální konstanty – interní, sdílené všemi instancemi
    _G = 9.80665     # tíhové zrychlení (m/s2)
    _M = 0.0289644   # molární hmotnost vzduchu (kg/mol)
    _R = 8.31447     # univerzální plynová konstanta (J/(mol·K))

    @property
    def altitude(self):
        '''
        Výpočet nadmořské výšky (v metrech) podle přesné barometrické rovnice.
        Používá aktuální teplotu ze senzoru.
        '''
        try:
            # Získání aktuální teploty a převod na Kelviny
            temp_c = self.temperature
            T = temp_c + 273.15

            # Získání tlaku
            p = self.pressure
            p0 = self.baseline

            # Výpočet výšky
            h = (self._R * T) / (self._G * self._M) * math.log(p0 / p)
        except Exception as e:
            print(f"Chyba ve výpočtu výšky: {e}")
            h = 0.0

        return h

    def sea_level_pressure(self, altitude=None):
        '''
        Vypočítá tlak přepočtený na hladinu moře (QNH) v Pa,
        na základě aktuálního tlaku, teploty a nadmořské výšky.
        '''
        try:
            L = 0.0065 # teplotní gradient (K/m)

            temp_c = self.temperature
            pressure = self.pressure
            if altitude is None:
                altitude = self.altitude
            T = temp_c + 273.15

            p0 = pressure * (1 - (L * altitude) / T) ** (-self._G * self._M / (self._R * L))

        except Exception as e:
            print("Chyba při výpočtu sea-level tlaku:", e)
            p0 = 0.0

        return p0

Testovací program čidla BMP180

Pokud jsme uložili knihovnu bmp180_ext.py do našeho modulu ESP32, vytvoříme následující program, který nejen otestuje funkčnost knihovny, ale zároveň nám ukáže, jak načíst hodnoty z modulu BMP180.

Tradičně nejdříve uvedeme celý program, pak si vysvětlíme jeho jednotlivé části.

Kód programu:

from bmp180_ext import BMP180
from machine import Pin, lightsleep, I2C   # SoftI2C
import time

#i2c_bus = SoftI2C(scl=Pin(22), sda=Pin(21), freq=100000)
i2c_bus =  I2C(0, scl=Pin(22), sda=Pin(21), freq=100000)
bmp180 = BMP180(i2c_bus)
bmp180.oversample_sett = 2
bmp180.baseline = 101325

while True:

    bmp180.blocking_read()   # zajistí čerstvá data

    temp = round(bmp180.temperature, 1)
    p = round(bmp180.pressure)/100
    phm = round(bmp180.sea_level_pressure())/100
    altitude = round(bmp180.altitude)

    t = time.localtime(time.time())
    print(f"Aktualni cas: {t[3]:02d}:{t[4]:02d}:{t[5]:02d}, {t[2]:02d}.{t[1]:02d}.{t[0]:04d}")

    print(f"Teplota: {temp:.1f} °C")
    print(f"Tlak  (STP): {p:.2f} hPa")
    print(f"Tlak (MSLP): {phm:.2f} hPa")
    print(f"Vyska: {altitude:.0f} m")
    time.sleep(0.01)

    lightsleep(60000)

    print()

Program pochopitelně začíná importem potřebných obslužných modulů.

from bmp180_ext import BMP180
from machine import Pin, lightsleep, I2C   # SoftI2C
import time

Pro činnost čidla BMP180 potřebujeme použít I²C sběrnici, tu můžeme buď ovládat pomocí modulu I2C (hardwarový řadič) nebo modulem SoftI2C (softwarový řadič). Podle potřeby je nutné odkomentovat, či zakomentovat příslušné části kódu.

Z modulu bmp180_ext importujeme třídu BMP180, kterou použijeme pro deklaraci objektu sensor, který nám umožňuje přístup k čidlu BMP180. Zároveň hned čidlu nastavíme přesnost měření a stanovíme výchozí hladinu atmosférického tlaku na hladině moře pomocí vlastnosti baseline.

#i2c_bus = SoftI2C(scl=Pin(22), sda=Pin(21), freq=100000)
i2c_bus =  I2C(0, scl=Pin(22), sda=Pin(21), freq=100000)
bmp180 = BMP180(i2c_bus)
bmp180.oversample_sett = 2
bmp180.baseline = 101325

V nekonečné hlavní smyčce pravidelně načítáme hodnoty z čidla, které dle potřeby zaokrouhlujeme na potřebný počet desetinných míst, tlak převádíme na hektopascaly.

bmp180.blocking_read()   # zajistí čerstvá data

temp = round(bmp180.temperature, 1)
p = round(bmp180.pressure)/100

Hodnoty atmosférického tlaku na úrovni mořské hladiny a odvislou hodnoty nadmořské výšky čidla dopočítáme pomocí k tomu určených funkcí.

phm = round(bmp180.sea_level_pressure())/100
altitude = round(bmp180.altitude)

Jelikož načítáme hodnoty každých 10 minut, vypisujeme k naměřeným hodnotám i aktuální časovou značku. Deklarujeme objekt pro práci s aktuálním časem. Objekt má strukturu pole, kde jednotlivé položky pole mají význam časových parametrů (hodina, minuta… den, měsíc, rok…).

t = time.localtime(time.time())
print(f"Aktualni cas: {t[3]:02d}:{t[4]:02d}:{t[5]:02d}, {t[2]:02d}.{t[1]:02d}.{t[0]:04d}")

Doba čekání na další měření (1 minuta) není kdovíjak dlouhá, ale i tak je asi škoda, aby se modul ESP32 v plném provozním režimu (a s odpovídající spotřebou) po tuto dobu jen „točil“ v nějaké čekací smyčce. Využijeme tedy znalosti z předchozího článku a uvedeme modul ESP32 do režimu spánku. Jelikož v článku věnovanému režimům spánku jsme spíše preferovali režim hlubokého spánku (deep sleep), zde tedy pro změnu použijeme režim lehkého spánku (light sleep). Nespornou výhodu je v tomto případě chování modulu ESP32 po probuzení z lehkého spánku, kdy normálně pokračuje v běhu programu. Celý program se tedy chová, jako bychom použili klasický příkaz sleep(), ale můžeme mít dobrý pocit, že jsme použili zase nějakou tu mazáckou vychytávku. 😊

Modul ESP32 uvedeme do lehkého spánku na 1 minutu (tj. 60000 milisekund).

lightsleep(60000)
BMP180 - ESP32

Následující obrázek nám ukazuje získaný výstup po spuštění programu:

BMP180-TEST - vystup

Komplexnější příklad použití čidla BMP180

V předchozí části dnešního článku jsme si ukázali, jak pracovat s čidlem BMP180 – jak ho připojit, jak načíst teplotu a tlak… ale pojďme to posunout dál. Nechceme zůstat u stylu: „importuj knihovnu, zavolej metodu a hotovo“. Teď si ukážeme, jak ze stejného čidla vytěžit víc – pustíme se do mini aplikace, která se už blíží reálnému nasazení.

Pochopitelně půjde jen o ukázkový „cvičný“ příklad, tedy o nic přehnaně funkčně složitého. Tento projekt má především ukázat, že i relativně jednoduchý senzor, jako je BMP180, může posloužit jako výchozí bod pro poměrně komplexní aplikaci. V rámci jednoho programu propojíme práci se senzorem, připojení k internetu, zpracování dat z externího API, synchronizaci času, výpočet nadmořské výšky a nastavení úsporného režimu provozu mikrokontroléru ESP32. I když, jak bylo naznačeno, výsledná aplikace není určena k přímému nasazení jako plnohodnotná meteostanice, demonstruje řadu technik, které se dají dále rozvíjet. Důležitější než samotný výstup je ale pro nás zkušenost, kterou si při tvorbě takového programu osvojíme – a právě v tom spočívá skutečná hodnota tohoto typu projektů.

Věříme, že tento příklad ukáže, jak zde dříve prezentované části mohou do sebe zapadat a jak z nich lze postupně stavět složitější a užitečnější systémy.

Naše požadavky na program

Cílem našeho snažení je postavit jednoduchý systém, který:

  1. měří teplotu a tlak pomocí čidla BMP180,
  2. mezi měřeními šetří energii hlubokým spánkem (deep sleep),
  3. pravidelně synchronizuje čas přes internet z NTP serveru,
  4. zohledňuje letní/zimní čas (ano, i tohle může být šikovné),
  5. načte si referenční hodnotu aktuálního tlaku na hladině moře z API meteowebu,
  6. vypočítá nadmořskou výšku podle místního a „standardního“ tlaku,
  7. a jako bonus: může být kdykoli ručně probuzen tlačítkem, které zároveň „ukončí“ běh aplikace.
vetsi projket s cidlem BMP180

Řešení jednotlivých částí

Postupně si projdeme jednotlivé úkoly našeho programu a ukážeme si jejich řešení. Pokud někdo bude mít zájem o hlubší seznámení s danou problematikou, které se to bude aktuálně týkat, uvedeme mu odkaz na dřívější článek, ve kterém jsme se tím zabývali.

Načtení teploty a tlaku z čidla BMP180

Základním krokem je samozřejmě získání dat z čidla BMP180. Zde se v kontentu tohoto článku neodehraje nic dramatického. Stačí připojit čidlo přes I²C a použít příslušnou knihovnu. Knihovnu bmp180_ext.py jsme si představili dříve, teď ji prostě použijeme.

from bmp180_ext import BMP180
from machine import I2C, Pin

#I2C pro BMP180
I2C_SCL_PIN = 22
I2C_SDA_PIN = 21

print("\n--- Mereni ---")
i2c_bus = I2C(1, scl=I2C_SCL_PIN, sda=I2C_SDA_PIN, freq=100000)
bmp180 = BMP180(i2c_bus)
bmp180.oversample_sett = 2
bmp180.baseline = 101325   # vychozi, bude prepsano internetem

tepl = round(bmp180.temperature, 1)
tlak = round(bmp180.pressure)/100

print(f"Teplota: {tepl:.1f} °C",)
print(f"Tlak (STP) : {tlak:.2f} hPa")

Zjištění tlaku na hladině moře

Abychom získali aktuální nadmořskou výšku, potřebuje hodnotu atmosférického tlaku korigovanou na hladinu moře. Běžně bychom asi použili těch všude zmiňovaných 101325 Pa, ale tato hodnota se ve skutečnosti mění v čase, neboť kupříkladu závisí na počasí.

Řešení se nabízí elegantní: Připojíme modul ESP32 k internetu a zjistíme si hodnotu atmosférického tlaku přepočtenou na hladinu moře z některé z profesionálních stanic v našem okolí.

Tuto hodnotu můžeme použít buď jako referenci pro výpočet nadmořské výšky, nebo jako určitou korekci při přepočtu lokálního tlaku na standardizovaný údaj.

Připojení k internetu

Abychom mohli stáhnout aktuální atmosférický tlak (nebo přesný čas), musí se náš modul ESP32 připojit k internetu.

Předpokládáme, že náš modul ESP32 je v dosahu Wi-Fi sítě, která umožňuje přístup k internetu. Modul ESP32 připojíme k této síti jako tzv. stanici (STA). Na modulu ESP32 vytvoříme jednoduchý webový klient, který odešle dotaz na API některého z meteorologických webů a vyžádá si relevantní data.

import network

# pripojeni k Wi-Fi
WIFI_SSID = "Jmeno-WIFI-site"
WIFI_PASS = "heslo-k-WIFI"

def connect_wifi():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        wlan.connect(WIFI_SSID, WIFI_PASS)
        print("Pripojuji k Wi-Fi")
        for _ in range(30):
            if wlan.isconnected():
                break
            print(".", end="")
            time.sleep(1)
        print()
    return wlan.isconnected()

if connect_wifi():
    print("Wi-Fi pripojeno.")
    atd.
else:
    print("Nepodarilo se pripojit k Wi-Fi.")
Načtení dat a API meteorologického webu

Jakmile máme modul ESP32 připojený k internetu, můžeme odeslat dotaz na některý z meteoserverů. Data se z API takových webů se (zpravidla) vracejí v podobě JSON dat, musíme je tedy správně načíst a zpracovat.

Jelikož chceme získat data co nejrychleji a pokud možno „bez zbytečné práce“, použijeme tedy pro vytvoření webového klienta modul urequests. Výhoda tohoto modulu je, že má vyřešené problémy s načítáním dat, které jsme si ve výše uvedeném článku ukazovali.

Pro zpracování dat ve formátu JSON použijeme modul ujson (vysvětlení, co je JSON také najdete rovněž v dříve uvedeném článku).

Následující ukázka ilustruje část kódu, která odesílá dotaz na meteoweb. Pro tento dotaz potřebujeme GPS souřadnice místa, ke kterému požadujeme hodnotu tlaku, a URL pro dotaz na API (obsahuje GPS souřadnice a někdy i API klíč). My využíváme web open-meteo.com, který nevyžaduje žádnou registraci, takže nepotřebujeme zadávat do URL parametr API klíče.

Z načtené odezvy webu, která má podobu strukturovaných JSON dat, vyseparujeme parametr pressure_msl, konkrétně jeho aktuální (tj. current) hodnotu.

Jelikož se během připojení k serveru a i při zpracování odezvy může cokoliv přihodit (server je nedostupný, změní se struktura JSON dat) je celý tento úsek programu vsazen do struktury try/except, jejímž účelem je zachycení a zpracování případné chyby.

import urequests
try:
    import ujson as json
except:
    import json

# GPS souřadnice
LOCATION_LAT = 49.3951948   # Např. kasna na namesti v KT
LOCATION_LON = 13.2934678

# Požadavek na API Open-Meteo
OPEN_METEO_URL = (
    f"https://api.open-meteo.com/v1/forecast?latitude={LOCATION_LAT}&longitude={LOCATION_LON}&current=pressure_msl"
)

def get_sea_level_pressure():
    try:
        response = urequests.get(OPEN_METEO_URL)
        data = response.json()
        response.close()
        return float(data["current"]["pressure_msl"])
    except:
        print("Chyba pri nacteni dat z open-meteo.com")
        return None

sea_level_pressure = get_sea_level_pressure()   # nacteni tlaku z meteowebu
if sea_level_pressure is not None:
    bmp180.baseline = sea_level_pressure * 100
else:
    print("Chyba pri ziskavani kalibrace tlaku.")

print(f"Tlak (internet): {sea_level_pressure:.2f} hPa")

Synchronizace času

Když už jsme úspěšně připojeni k Wi-Fi s přístupem k internetu, využijeme možnosti získání přesného času z některého z NTP serverů. NTP server je specializovaný server poskytující údaje o přesném času.

Dále uvedená část kódu programu ukazuje načtení času do modulu ESP32. Čas je pak dostupný v objektu ntp.time(), který jako parametr předáme do příkazu localtime(). Z předchozího programu již víme, že objekt (v našem kódu označen t) má strukturu pole, kde jednotlivé položky pole mají význam časových parametrů (hodina, minuta… den, měsíc, rok…).

Za zmínku v následující ukázce ještě stojí přičtení hodnoty 3600 k hodnotě získané z metody time(). Tato metoda vrací čas v podobě UTC a my potřebujeme přičíst 1 hodinu (tj. 3600 vteřin), abychom získali čas středoevropský.

import ntptime
import time

def sync_time(ntp_server='ntp.nic.cz'):
    ntptime.host = ntp_server
    try:
        ntptime.settime()
        print("Cas synchronizovan.")
    except:
        print("Chyba pri synchronizaci casu.")

t = time.localtime(time.time() + 3600 * 1)
print(f"\nAktualni cas: {t[3]:02d}:{t[4]:02d}:{t[5]:02d}, {t[2]:02d}.{t[1]:02d}.{t[0]:04d}")

Jelikož modul ESP32 má svůj RTC obvod, který se stará o běh hodin, je asi zbytečné neustále načítat přesný čas z NTP serveru. Čas se tedy synchronizuje jen při prvním spuštění obvodu (je třeba nastavit čas v modulu ESP32), pak při každém desátém probuzení (konstanta NTP_SYNC_INTERVAL).

NTP_SYNC_INTERVAL = 10

if boot_count == 1 or boot_count % NTP_SYNC_INTERVAL == 0:
    print("Synchronizuji cas z NTP... ", end="")
    sync_time()
Čítač probuzení

V předešlé ukázce se používá proměnná boot_count, která obsahuje počet probuzení obvodu ESP32 z režimu hlubokého spánku, ve kterém modul většinu času setrvává. Jelikož používáme režim hlubokého spánku, ze kterého se probuzení podobá resetu, je potřeba počet probuzení uchovávat v paměti modulu RTC (součást procesoru modulu ESP32). /p>

Následující ukázka ukazuje, jak získávat počty probuzení, tedy nastavovat proměnnou boot_count.

import machine

# ==== ČÍTAČ BOOTŮ V RTC ====
def load_wake_count():
    rtc = machine.RTC()
    try:
        # Zkus načíst počet z RTC paměti
        wake_count = rtc.memory()
        return int(wake_count.decode()) if wake_count else 0
    except Exception as e:
        return 0

def save_wake_count(count):
    rtc = machine.RTC()
    rtc.memory(str(count).encode())   # Uložení počtu do RTC paměti

if machine.wake_reason() != machine.EXT0_WAKE:
    boot_count = load_wake_count()
    print(f"\n>>> Probuzeni č. {boot_count} (Timer)")
    boot_count += 1   # Inkrementace počtu probuzení
    save_wake_count(boot_count)
Ošetření letního času

Načteme-li čas z NTP serveru a upravíme jej na středoevropský čas, bylo by dobré ještě zohlednit i možnost nastavení letního a zimního času. O hledem na předešlé řešení načtení času z NTP serveru si vytvoříme funkci summer(), která bude vracet hodnotu 0 při zimním času a 1 při letním času. Tak jako jsme posunuli čas o 1 hodinu při korekci na středoevropský čas, tak stejně budeme, či nebudeme, přidávat funkcí summer další hodinu kvůli letnímu času.

Následující část kódu ukazuje kód funkce summer. Letní čas je stanoven od poslední neděle v březnu daného roku (2 hodiny ráno) a končí poslední nedělí v říjnu (3 hodiny ráno). Hlavní částí funkce je tedy určení poslední neděle v daném měsíci. 😊

def summer():
    # Získání aktuálního UTC času
    t = time.localtime()
    year, month, day, hour, minute, second, weekday, yearday = t

    # Najdi poslední neděli v březnu
    if month == 3:
        for d in range(31, 24, -1):
            if time.localtime(time.mktime((year, 3, d, 0, 0, 0, 0, 0)))[6] == 6:
                last_sunday_march = d
                break
        if day > last_sunday_march or (day == last_sunday_march and hour >= 2):
            return 1
        else:
            return 0

    # Duben až září = letní čas
    elif 4 <= month <= 9:
        return 1

    # Najdi poslední neděli v říjnu
    elif month == 10:
        for d in range(31, 24, -1):
            if time.localtime(time.mktime((year, 10, d, 0, 0, 0, 0, 0)))[6] == 6:
                last_sunday_october = d
                break
        if day < last_sunday_october or (day == last_sunday_october and hour < 3):
            return 1
        else:
            return 0

    # Leden, únor, listopad, prosinec = zimní čas
    return 0

Použití funkce summer pak bude následující:

t = time.localtime(time.time() + 3600 * (1+summer()))

print(f"\nAktualni cas: {t[3]:02d}:{t[4]:02d}:{t[5]:02d}, {t[2]:02d}.{t[1]:02d}.{t[0]:04d}")

Správa režimů spánku

Máme-li všechny důležití části programu připravené, začneme je postupně skládat do hlavní části programu. Poslední částí, která se však již týká hlavního fungování celého programu, jsou nastavení okolo režimu spánku – respektive probuzení z něj.

Probuzení pomocí tlačítka

První částí, na kterou se zaměříme, je probuzení pomocí tlačítka. Poté, co modul ESP32 usne je možnost jeho odezvy omezená. Chceme-li v tokovém případě program zastavit, máme jen omezené možnosti. Využijeme tedy toho, že lze modul z režimu spánku probudit událostí na některém z RTC pinů.

Pro probuzení modulu ESP32 a určité „ukončení“ programu využijeme vestavěného tlačítka, které je zapojeno na pinu GPIO0 (který je naštěstí jedním z pinů rodiny RTC, které mohou modul ESP32 probouzet z režimu spánku.)

Proč „ukončení a ne ukončení?

Když uložíme program do modulu ESP32 jako hlavní program (soubor main.py), zjistíme, že pokud program skončí nebo se resetuje, spustí se soubor main.py znova. Program tedy vlastně nelze ukončit. Představa našeho „ukončení“ programu tedy bude taková, že program necháme běžet v nekonečné smyčce, ve které nic nedělá. Tento stav maximálně budeme signalizovat blikající vestavěnou LED (GPIO2), abychom věděli, že program již nevykonává svou hlavní část.

Nejdříve se podíváme, jak nastavit probuzení modulu ESP32 pomocí stisknutí vestavěného tlačítka, pak si ukážeme výše popsané ukončení programu.

Nastavení probuzení jedním pinem (ext0), který má být stisknutý (tj. míst hodnotu LOW) zařídí následující řádky:

# ==== NASTAVENI BUZENI PINEM ====
button = Pin(BUTTON_PIN, mode=Pin.IN, pull=Pin.PULL_UP)
esp32.wake_on_ext0(pin=button, level=esp32.WAKEUP_ALL_LOW)

Jakmile se modul ESP32 probudí z hlubokého článku (což se podobá resetování), měli bychom obecně podchytit dva důležité stavy:

  • Vyhodnotit, že došlo k resetu z důvodu probuzení modulu z režimu spánku.
  • Vyhodnotit, že důvodem probuzení bylo stisknuté tlačítko a nikoliv časovač.

Naštěstí po startovním úvodním resetu potřebujeme, aby program běžel stejně jako po probuzení, nemusíme řešit rozdělení programu dle typu resetu, postačí nám tedy jen následující podmínka, která reaguje pouze na důvod probuzení, tedy zda nastalo probuzení pinem (ext0) nebo časovačem:

if machine.wake_reason() != machine.EXT0_WAKE:

    #tady je kod pro probuzeni casovacem

else:
    print("Probuzeni pomoci tlacitka – program konci.")
    while True:   # bezi navzdy, coz ma byt konec programu
        led.value(0)
        time.sleep(0.2)
        led.value(1)
        time.sleep(0.2)
Uspání na určenou dobu

Na samý závěr programu už nezbývá než uspat modul ESP32 na předem stanovenou dobu. V tomto ukázkovém případě se jedná jen o dobu deseti minut (600 s). V reálném nasazení by to spíše byla hodina, neboť měření 1× za hodinu by asi mohlo u meteorologické stanice.

SLEEP_TIME_SEC = 600

# ==== USPANI ====
print(f"\nUspani na {SLEEP_TIME_SEC} sekund...")
time.sleep(0.01)
machine.deepsleep(SLEEP_TIME_SEC * 1000)

Hotový program

Celý kód jsme si postupně vysvětlili, nezbývá nám než jej spojit v jeden celek. Následující výpis programu je jednou z možných variant. Jen opět připomeňme, že je nutné program spustit v podobě souboru main.py přímo v modulu ESP32 a nikoliv jen zeleným tlačítkem v prostředí Thonny IDE. To by program proběhl právě jednou, pak usnul na deset minut a skončil. Rozhodně též nesmíme zapomenout do modulu ESP32 (do složky lib) uložit všechny externí knihovny – v tomto případě jen knihovnu bmp180_ext.py, neboť ostatní knihovny jsou součástí aktuálního firmwaru MicroPythonu.

Kód programu:

from bmp180_ext import BMP180
from machine import I2C, Pin, deepsleep
import machine
import network
import ntptime
import urequests
try:
    import ujson as json
except:
    import json
import esp32
import time

# ==== KONSTANTY ====
# pripojeni k Wi-Fi
WIFI_SSID = "Nazev-WiFi-site"
WIFI_PASS = "heslo-k-WiFi-siti"

# casovani buzeni a NTP synchronizace
SLEEP_TIME_SEC = 600
NTP_SYNC_INTERVAL = 10

# GPS souřadnice
LOCATION_LAT = 49.3951948   # Např. kasna na namesti v KT
LOCATION_LON = 13.2934678

# Požadavek na API Open-Meteo
OPEN_METEO_URL = (
    f"https://api.open-meteo.com/v1/forecast?latitude={LOCATION_LAT}&longitude={LOCATION_LON}&current=pressure_msl"
)

#I2C pro BMP180
I2C_SCL_PIN = 22
I2C_SDA_PIN = 21

# interni LED a stop tlacitko
LED_PIN = 2
BUTTON_PIN = 0
led = Pin(LED_PIN, Pin.OUT)
led.value(1)   # LED rozsvítit po probuzení

# ==== FUNKCE ====
def connect_wifi():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        wlan.connect(WIFI_SSID, WIFI_PASS)
        print("Pripojuji k Wi-Fi")
        for _ in range(30):
            if wlan.isconnected():
                break
            print(".", end="")
            time.sleep(1)
        print()
    return wlan.isconnected()

def sync_time(ntp_server='ntp.nic.cz'):
    ntptime.host = ntp_server
    try:
        ntptime.settime()
        print("Cas synchronizovan.")
    except:
        print("Chyba pri synchronizaci casu.")

def get_sea_level_pressure():
    try:
        response = urequests.get(OPEN_METEO_URL)
        data = response.json()
        response.close()
        return float(data["current"]["pressure_msl"])
    except:
        print("Chyba pri nacteni dat z open-meteo.com")
        return None

def summer():
    # Získání aktuálního UTC času
    t = time.localtime()
    year, month, day, hour, minute, second, weekday, yearday = t

    # Najdi poslední neděli v březnu
    if month == 3:
        for d in range(31, 24, -1):
            if time.localtime(time.mktime((year, 3, d, 0, 0, 0, 0, 0)))[6] == 6:
                last_sunday_march = d
                break
        if day > last_sunday_march or (day == last_sunday_march and hour >= 2):
            return 1
        else:
            return 0

    # Duben až září = letní čas
    elif 4 <= month <= 9:
        return 1

    # Najdi poslední neděli v říjnu
    elif month == 10:
        for d in range(31, 24, -1):
            if time.localtime(time.mktime((year, 10, d, 0, 0, 0, 0, 0)))[6] == 6:
                last_sunday_october = d
                break
        if day < last_sunday_october or (day == last_sunday_october and hour < 3):
            return 1
        else:
            return 0

    # Leden, únor, listopad, prosinec = zimní čas
    return 0

# ==== ČÍTAČ BOOTŮ V RTC ====
def load_wake_count():
    rtc = machine.RTC()
    try:
        # Zkuste načíst počet z RTC paměti
        wake_count = rtc.memory()
        return int(wake_count.decode()) if wake_count else 0
    except Exception as e:
        return 0

def save_wake_count(count):
    rtc = machine.RTC()
    rtc.memory(str(count).encode())    # Uložení počtu do RTC paměti

# ==== HLAVNÍ ČÁST ====
# ==== ZJIŠTĚNÍ DŮVODU PROBUZENÍ ====
if machine.wake_reason() != machine.EXT0_WAKE:
    boot_count = load_wake_count()
    print(f"\n>>> Probuzeni č. {boot_count} (Timer)")
    boot_count += 1   # Inkrementace počtu probuzení
    save_wake_count(boot_count)

    print("\n--- Mereni ---")
    bus = I2C(1, scl=I2C_SCL_PIN, sda=I2C_SDA_PIN, freq=100000)   # on esp8266
    bmp180 = BMP180(bus)
    bmp180.oversample_sett = 2
    bmp180.baseline = 101325

    if connect_wifi():
        print("Wi-Fi pripojeno.")

        if boot_count == 1 or boot_count % NTP_SYNC_INTERVAL == 0:
            print("Synchronizuji cas z NTP... ", end="")
            sync_time()

        sea_level_pressure = get_sea_level_pressure()   # nacteni tlaku z meteowebu
        if sea_level_pressure is not None:
            bmp180.baseline = sea_level_pressure * 100
        else:
            print("Chyba pri ziskavani kalibrace tlaku.")
    else:
        print("Nepodarilo se pripojit k Wi-Fi.")

    tepl = round(bmp180.temperature, 1)
    tlak = round(bmp180.pressure)/100
    vyska = round(bmp180.altitude)

    t = time.localtime(time.time() + 3600 * (1+summer()))
    print(f"\nAktualni cas: {t[3]:02d}:{t[4]:02d}:{t[5]:02d}, {t[2]:02d}.{t[1]:02d}.{t[0]:04d}")
    print(f"Teplota: {tepl:.1f} °C",)
    print(f"Tlak: {tlak:.2f} hPa")
    print(f"Tlak (hladina moře): {sea_level_pressure:.2f} hPa")
    print(f"Vyska: {vyska:.0f} m n. m.")

    # ==== NASTAVENI BUZENI PINEM ====
    button = Pin(BUTTON_PIN, mode=Pin.IN, pull=Pin.PULL_UP)
    esp32.wake_on_ext0(pin=button, level=esp32.WAKEUP_ALL_LOW)

    # ==== USPANI ====
    print(f"\nUspani na {SLEEP_TIME_SEC} sekund...")
    time.sleep(0.01)
    machine.deepsleep(SLEEP_TIME_SEC * 1000)

else:
    print("Probuzeni pomoci tlacitka - program konci.")
    while True:   # bezi navzdy, coz ma byt konec programu
        led.value(0)
        time.sleep(0.2)
        led.value(1)
        time.sleep(0.2)

Na následujícím obrázku vidíme výstupní okno prostředí Thonny IDE, které nyní slouží jako určitý sériový monitor výstupu modulu ESP32. Takto vypadá výstup po spuštění programu. Všimněme si, že při prvním spuštění došlo pro připojení k Wi-Fi k synchronizaci hodit z NTP, zatímco při první probuzení je tato část vynechána (hodiny se budou synchronizovat zase až při desátém probuzení).

BMP180 - velky program

Program běží, máme tedy hotovo!

Je program přesně podle našich představ? Modul ESP32 většinu času spí a šetří baterii. Jednou za čas se probudí, načte aktuální hodnoty tlaku a teploty. Pochopitelně se i připojí k Wi-Fi a (v zadaných okamžicích) sesynchronizuje čas s internetem a stáhne referenční atmosférický tlak z profesionální meteorologické stanice. Na závěr vypočítá nadmořskou výšku a vše potřebné vytiskne na výstup. Nesmíme zapomenout na to, že ho lze ručně probudit z hlubokého spánku – pak se rozbliká a tím do klidového režimu.

A teď upřímně… není takový ve své podstatě docela „nelehký“ program vlastně brnkačka? 😊

Závěr (Závěr?)

Přestože se zprvu asi zdálo, že článek o čidle BMP180 bude naprosto nudným článkem, který nám akorát představí jen další „zvířátko ze zoologické zahrady“ čidel připojitelných k modulu ESP32, ukázalo se, že ve světě bastlení nikdy nic není nic nudného!

Kdyby nic jiného, tak snad nám tento článek poskytne ke stažení knihovnu bmp180_ext.py, kterou (dokud bude fungovat!) můžete používat do svých projektů. A pokud jste k těmto projektům našli inspiraci v našich článcích, nebyl záchvat naší grafomanie zbytečný!

Dnešním článkem se pomalu blížíme k závěru celého našeho seriálu věnovanému MicroPythonu na modulu ESP32. Během těch pětadvaceti článků jsme prošli především to hlavní (kromě Bluetooth, to nesnášíme!), co bychom od modulu ESP32 vlastně mohli požadovat a co by naopak on od nás potřeboval. Jistě, ještě nám tu pár věcí zbývá, takže to (možná) na dalších pár článků vydá… Ale upřímně, skoro se zdá, že již se vlastně nemáme co nového učit. Hlavní vlastnosti modulu ESP32 jsme poznali:

  • Víme jak načítat piny, tak pomocí nich řídit různá čidla.
  • Zvládli jsme zachytávat události a to jak na úrovni přerušení, tak třeba jako události pro probuzení z režimu spánku.
  • Poznali jsme pár zajímavých čidel (někdy i s jejich vrtochami)
  • A co legrace jsme si užili s připojením modulu ESP32 k internetu!

Zkrátka, ať si kdokoliv říká o MicroPythonu na modulu ESP32, co chce. My víme, že už víme (a teď už i umíme) své!

UPOZORNĚNÍ:
Nesouhlasíme s vyřazením Newtonových zákonů, Ohmova zákona a zákona zachování energie z učiva fyziky základních škol v České republice!