Pojďme programovat elektroniku | Arduino

Pojďme programovat elektroniku: Postavíme bezpečnostní systém za 30 Kč

  • Před pár týdny jste si mohli v akci koupit Wi-Fi desku za jeden dolar
  • Nám už TTGO T-Display dorazila do redakce
  • Připojíme k ní jazýčkový kontakt a vyrobíme bezpečnostní systém

Bastlíři, kteří nás čtou pravidelně, možná před dvěma týdny zachytili upoutávku na jednu z mnoha nákupních akcí na Aliexpressu. Jeden z prodejců několik dnů nabízel maličkou prototypovací destičku TTGO T-Display s Wi-Fi čipem ESP32 a barevným 1,14“ LCD displejem za pouhý dolar!

To jsem si nemohl nechat ujít ani já, desku rychle objednal a dnes si ji ukážeme v dalším programovacím experimentu našeho seriálu. Postavíme si primitivní bezpečnostní systém, který bude detekovat otevřené dveře nebo třeba okno.

Nechce se vám číst celý článek? Podívejte se na video:

Jakmile k této akci dojde, čip se probudí, zobrazí červené varování na displeji, odešle zprávu do centrály, kterou bude Raspberry Pi Zero W a mohlo by informaci dále zpracovat, načež se přepne do hlubokého spánku, ve kterém bude spalovat jen zlomek elektrické energie.

TTGO T-Display má totiž ještě jednu vychytávku: na jeho maličké destičce se našlo místo i pro nabíjecí obvod jednočlánkové (3,7/4,2V) lithiové baterie, a proto k ní připojíme průmyslovou Li-ion baterii 18650.

Destička s displejem, Wi-Fi, tlačítky a nabíjecím obvodem

Většina součástek z Číny trpí žalostnou dokumentací, a tak často bastlíř namísto programování dlouhé hodiny googlí a snaží se vyčmuchat (v lepším případě) anglický datasheet. TTGO T-Display je na tom ale mnohem lépe, na GitHubu totiž najdete poměrně blbuvzdorný příklad včetně odkazu na použitou knihovnu TFT_eSPI pro práci s displejem, který je už podle názvu knihovnu připojený k čipu ESP32 skrze sběrnici SPI.

b2eae95a-c785-432c-b244-d9029214faed
Schéma destičky TTO T-Display a základní rozložení jejích pinů (pinout)

Destička má zároveň na čelní straně připájená dvě programovatelná tlačítka. Jedno vede na pin GPIO0, druhé na GPIO35. Na boční straně najdete tlačítko RST pro resetování čipu.

Jelikož je deska vybavená ještě zmíněným obvodem pro nabíjení lithiové jednočlánkové baterie, nechybí ani napojení jejího napětí na jeden z analogových pinů čipu ESP32, čili napětí baterie můžeme průběžně sledovat a analyzovat, kolik šťávy ještě zbývá.

c60356ec-cada-4a69-b565-cdba1771a947c82af13d-e35b-4738-8b4c-b4340330c38cba266765-d97a-4578-bba5-4b753fd48186
Čelní straně vévodí 1,14“ barevný LCD displej s rozlišením 240×135 pixelů a na té zadní najdete JST konektor pro 3,7/4,2V lithiovou baterii, kterou dobijete skrze USB-C konektor desky

Když bychom k USB portu připojili třeba 5V solární panel, nemusíme se už o nic dalšího starat a máme kompletní řešení, které při venkovní instalaci vydrží celé roky. Tedy pokud bude čip periodicky usínat a pracovat jen tehdy, když to bude třeba.

Čtení hodnot napětí na baterii je taktéž součástí příkladu na GitHubu, obšírné představování tedy tentokrát přeskočím a vrhnu se rovnou na dnešní experiment.

Bezdrátový detektor dveří

Jak už jsem napsal v úvodu, tentokrát si postavíme primitivní bezpečnostní systém – detektor otevřených dveří nebo okna. Jelikož vše poběží na baterii, do maličkého JST konektoru na spodní straně desky připojíme jednočlánkovou Li-Ion baterii (třeba typ 18650).

d8b65edf-b7d4-4dd5-8b49-2848e0065d5e
Magnetický detektor otevřených dveří s jazýčkovým kontaktem ve svém nitru

Tak, zdroj energie bychom měli, ale ještě potřebujeme ten detektor. Poslouží nám primitivní magnetický spínač – tzv jazýčkový kontakt (anglicky reed switch), který na Aliexpressu seženete v balení po pěti kusech za necelých čtyřicet korun.

Jazýčkový kontakt

Detektor se skládá z obvodu a magnetu. Když se část s magnetem (přilepená na dveřích) nachází v blízkosti obvodu (rám dveří), elektrický okruh je uzavřen. Když se však obě části vzájemně vzdálí, okruh se rozpojí.

1bae2b3a-fb84-496b-9f36-518ea7be4498
Jazýčkový kontakt (Foto: NACC BY-SA 2.5)

Jazýčkový kontakt tedy stačí přes pull-down/pull-up (dle konstrukce) připojit k mikrokontroleru a jednoduše přečíst logickou hodnotu v obvodu. V našem případě to bude 1 pro zavřené dveře a 0 pro otevřené.

Externí wake up interrupt

Naprostého začátečníka by možná napadlo, že tedy bude ve smyčce neustále číst digitální stav jazýčkového kontaktu, a když dojde ke změně, detekuje otevřené dveře. Mnohem vhodnější je ale přečtení stavu jazýčkového kontaktu opravdu jen při změně stavu – asynchronně skrze interrupt, tedy přerušení.

17696815-8c21-4c69-af9d-630e9f7ae943
Celý dnešní obvod: Destka TTGO T-Display, Li-ion baterie a detektor dveří s jazýčkovým kontaktem připojeným z pinu 3V na pin GPIO27. Detektor je zavřený, takže hlavní čip i displej spí.

V takovém případě by za nás řídící čip sledoval, co se děje na pinu, na který je připojený jazýčkový kontakt, a ozval se až tehdy, kdy skutečně dojde ke změně.

My ale potřebujeme ještě trošku něco jiného. Jelikož naši destičku napájí baterie, nemá smysl, aby byl čip celou dobu vzhůru a čekal, až dorazí informace o přerušení. Namísto toho jej přepneme do hlubokého spánku, během kterého čip ESP32 spaluje jen několik mikroampérů elektrického proudu.

d43578ed-14cd-49cc-8cf7-51572e765be6
Dveře jsou otevřené, jazýčkový kontakt se rozpojil, změna napětí na pinu GPIO27 probudila čip ESP32, který následně zobrazil varování a čeká, dokud dveře zase nezavřeme

Jak se tedy dozví, že dojde k otevření dveří? Zaregistrujeme externí wake up interrupt.  Tedy speciální typ systémového přerušení, které resetuje čip, takže jeho program začne zase od začátku. My se však na začátku programu zeptáme, kdo vlastně reset způsobil, a pokud to bude náš jazýčkový kontakt, budeme vědět, že někdo otevřel dveře.

RAM, která přežije spánek i reset

Tak, víme, že jsou dveře otevřené, a proto na displeji zobrazíme červené varování a také počet otevření od posledního připojení čipu k napájení. K tomu v případě čipů ESP8266 a ESP32 slouží speciální parametr RTC_DATA_ATTR při deklaraci proměnné.

72204bce-fe7f-45c4-a3cc-1e5fa2beef2d
Proměnná počítadla je uložená v části RAM, která přežije hluboký spánek i reset čipu

Když tedy vytvoříme třeba proměnnou:

RTC_DATA_ATTR size_t pocitadlo_otevreni = 0;

Program ji po startu neuloží v běžné adresní oblasti operační paměti RAM, ale v části, která je dostupná pro RTC obvod a přežije tedy i přechod do hlubokého spánku, kdy je většina elektroniky čipu vypnutá.

Když pak kdekoliv v našem programu napíšeme:

pocitadlo_otevreni++;

A poté přepneme čip do hlubokého spánku, načež jej zresetujeme, RTC RAM vše přežije, takže po dalším spuštění bude mít proměnná hodnotu o jedničku vyšší. Paměť se vynuluje leda v případě, že čip ztratí zdroj napájení.

Probuď se, pokud se na pinu objeví logická nula

Tak, už víme, že část RAM na čipech ESP8266/ESP32 přežije spánek i reset, ale ještě si musíme ukázat, jak čip pozná, že jej probudilo přerušení způsobené jazýčkovým kontaktem.

Aby jazýčkový kontakt způsobil reset čipu, musíme jej připojit k některému z pinů, které jsou v každém schématu nebo popisu desky (pinout) označené slovíčkem RTCIO.

a30c6da2-1ff2-4f0d-ac2e-593e6212be7a
Pinout surového modulu čipu ESP32 v provedení WROOM32. Všimněte si u některých pinů označení RTCIO0-17. Tyto piny jsou dostupné pro RTC obvod i v době, kdy hlavní procesor spí.

Čip ESP32 jich má hned několik a na těchto pinech poté můžete vytvořit externí probuzení, protože jejich stav opět čte ona nízkoúrovňová logika obvodu RTC hodin, která běží, i když hlavní čip spí a je mu opravdu jedno, co se okolo něj děje.

Detektor s jazýčkovým kontaktem na jedné straně připojíme na pin 3V a na druhé do pinu GPIO27 na opačné straně. GPIO27 je totiž jeden z pinů, který podporuje i RTCIO. Na začátku programu pak na tomto pinu aktivujeme interní pull-down rezistor, který bude fungovat i ve spánku, protože jej řídí obvod RTC:

rtc_gpio_pulldown_en(GPIO_NUM_27);

Funkce výše bude vyžadovat hlavičkový soubor driver/rtc_io.h, takže na začátek zdrojového kódu musíme vložit ještě řádek:

#include <driver/rtc_io.h>

Pull-down rezistor se postará o to, aby nám jazýčkový kontakt v rozpojeném stavu vracel čistou logickou nulu a nedocházelo ke kmitání (floating). Dále musíme GPIO pin 27 konečně zaregistrovat jako externí budíček, který probudí čip ESP32. Provedeme to třeba takto:

esp_sleep_enable_ext0_wakeup(GPIO_NUM_27, 0);

Příkazem výše říkáme RTC logice čipu ESP32, aby vyvolala reset, pokud se na pinu GPIO 27 objeví logická nula. Bohužel lze nastavit jen diskrétní stavy (logická nula/jednička) a nikoliv probíhající změna z jednoho stavu do druhého (náběžná/sestupná hrana).

Jakmile v našem programu provedeme vše potřebné, můžeme čip konečně přepnout do spánku příkazem:

esp_deep_sleep_start();

Kdo mě probudil ze spánku?

Když se čip příště resetuje probuzením z hlubokého spánku, musíme se jej konečně ještě zeptat, jaký elektrický okruh to vlastně způsobil, abychom mohli identifikovat., že to byl jazýčkový kontakt (případně bychom hned po startu mohli přečíst jeho logický stav a dopídit se, jestli je jeho obvod právě rozpojený, nebo spojený).

Důvod probuzení získáme takto:

esp_sleep_wakeup_cause_t duvod = esp_sleep_get_wakeup_cause();
if(duvod == ESP_SLEEP_WAKEUP_EXT0){
    Serial.println("Cip probuzeny pomoci jazyckoveho kontaktu");
}

Podstatné je to slovíčko EXT0, což je jeden z typů externích budíků. Čip má k dispozici ještě EXT1 a každý z nich může pracovat s hromadou budících zdrojů. My jsme EXT0 použili jen pro náš jazýčkový kontakt, a tak víme, že to byl právě on, kdo probudil hlavní čip.

94afff65-aff9-4e1d-bfa3-ef70b558bf94
Výpis ze sériové linky po probuzení ze spánku externím přerušením

Usínání a probouzení čipů ESP8266/ESP32 je hotová věda a zejména ESP32 toho umí mnohem více, to je už ale nad rámec našeho článku, a tak vás odkážu na docela lidsky zpracovanou dokumentaci přímo od výrobce s popisem hromady dalších praktických funkcí.

Čau lidi, posílám vám všem multicastovou zprávu

Probouzecí a usínací logiku bychom měli, v úvodu jsme si ale řekli, že čip po probuzení zároveň odešle zprávu do Raspberry Pi (nebo prostě do jakéhokoliv jiného pokročilého serveru), které u nás v redakci slouží jako základna všech chytrých krabiček.

Mohli bychom to udělat třeba pomocí HTTP. Na Raspberry Pi by běžel libovolný HTTP server a my bychom se po probuzení čipu ESP32 jednoduše připojili k redakční Wi-Fi a pomocí HTTP klientu zavolali třeba adresu http://192.168.1.1/dvere.php při předpokladu, že právě tuto lokální IP má naše Raspberry Pi.

ac35f9d3-1703-497a-8508-a6329445085e
Centrála chytré redakce: Raspberry Pi Zero W s Wi-Fi a 868MHz vysílačem LoRa

Jenže my nic takového neuděláme. Proč? Protože bychom museli do firmwaru natvrdo vypálit IP adresu Raspberry Pi, která se ale může snadno změnit, protože mu ji přidělil podnikový DHCP server mimo naši kontrolu. Ne, v ROM by mělo být natvrdo vypáleno co nejméně konfiguračních hodnot.

Namísto HTTP komunikace odešleme informaci o otevřených dveřích jako primitivní textovou multicastovou UDP zprávu „ALARM_DVERE_ZIVECZ“. V rámci multicastového protokolu neposíláme data počítači s konkrétní IP adresou, ale na speciální interní IP adresu vyhrazenou právě pro multicast. Jednou z nich může být třeba 224.0.0.0 a ještě rozlišující port 8888.

ESP32 pro vývojové prostředí Arduino má pro tyto účely předinstalovanou knihovnu WifiUdp.h, takže odeslání multicastové zprávy na zvolenou adresu by mohlo vypadat ve vší stručnosti následovně:

#include <WiFiUdp.h>
WiFiUDP udp;
IPAddress multicast_ip(224, 0, 0, 0);
unsigned int multicast_port = 8888;

A po zdárném probuzení a připojení k Wi-Fi:

udp.beginMulticast(multicast_ip, multicast_port);
udp.beginMulticastPacket();
udp.print("ALARM_DVERE_ZIVECZ");
udp.endPacket();

Jakýkoliv jiný počítač, který bude poslouchat na stejné multicastové IP adrese a portu, poté obdrží data. Jedním z nich bude i naše Raspberry Pi Zero W a jednoduchý program v Pythonu, který v případě příjmu zprávy „ALARM_DVERE_ZIVECZ“ může vyvolat nějakou další akci. Třeba mi poslat e-mail. Jeho kód v Pythonu je komplexnější, a tak jej najdete v závěru jako celek.

21596b04-ce06-40ea-82ba-9cac9a5b974a
ESP32 detekoval otevřené dveře, připojil se k Wi-Fi a odeslal multicastovou UDP zprávu (viz bílé okno sériového terminálu). Na stejné multicastové IP adrese poslouchá i testovací aplikace v Pythonu na redakčním Raspberry Pi, která zprávu úspěšně zachytila (viz černé okno vzdáleného SSH terminálu) .

A je hotovo

A to je celé! Dnes jsme si tedy vyzkoušeli destičku s Wi-Fi čipem a barevným displejem, kterou jste si mohli před pár týdny objednat z Aliexpressu za pouhý dolar. Dle ohlasů si ji skutečně pořídila hromada bastlířů z Česka.

I když už akce skončila, desky jako TTGO T-Display a podobné jsou samozřejmě nadále k dispozici, jen už pochopitelně za mnohem více peněz. Cena poskočí na tu obvyklou, která se při počtu součástek na drobné destičce vyšplhá na nějakou tu stokorunu.

676cdf3f-b066-4fcd-8d74-a0e3b7783ea8
Webový server na Raspberry Pi Zero W by mohl návštěvníkům nabízet třeba podobné statistiky hlavních dveří do brněnských redakcí našeho vydavatelství, ze kterých je patrná aktivita v průběhu dne a týdne. Přesně to dělá jedna z krabiček postavená na stejném principu jako dnešní experiment.

Nakonec jako vždy zdrojové kódy

Na úplný závěr nesmím jako vždy zapomenout na kompletní kód našeho jednoduchého detektoru otevřených dveří s Wi-Fi, obrazovkou a během na baterii. V první části je kód pro samotnou destičku TTGO T-Display s jazýčkovým kontaktem připojeným na piny 3V a GPIO27.

V druhé části pak najdete jednoduchý kód v Pythonu, který bude poslouchat příchozí zprávy skrze multicast a primitivní protokol UDP. Multicast nesmí být zakázaný na routeru sítě.

Zdrojový kód pro destičku TTGO T-Display a vývojové prostředí Arduino s doinstalovanou podporou pro čipy ESP32:

(Deska TTGO T-Display není v nabídce správce desek v prostředí Arduino. Stačí zvolit generickou desku ESP32 Dev Module.)

// Knihovny pro praci s displejem
// Viz https://github.com/Xinyuan-LilyGO/TTGO-T-Display
#include <SPI.h>
#include <TFT_eSPI.h>

// Knihovny pro praci s Wi-Fi a UDP/multicast
#include <WiFi.h>
#include <WiFiUdp.h>

// Knihovna pro nastaveni pull-down rezistoru na RTCIO pinu,
// ktery bude fungovat i behem hlubokeho spanku
#include <driver/rtc_io.h>

// Pomocne makro s definici pinu, kterym zapinam/vypinam podsviceni LCD
#define TFT_BL 4

// Pomocna makra s pristupovymi udaji k Wi-Fi
#define SSID "ZiveCzWiFi"
#define PASWD "nevolimburese"

// Inicializace displeje s rozlsienim 240x135 pixelu
TFT_eSPI tft = TFT_eSPI(135, 240);

// Promenna s pocitadlem otevrenych dveri, ktera se vytvori v RTC casti RAM
RTC_DATA_ATTR size_t pocitadlo_otevreni = 0;

// Objekt UDP klientu a multicast IP a port
WiFiUDP udp;
IPAddress multicast_ip(224, 0, 0, 0);
unsigned int multicast_port = 8888;

// Hlavni funkce setup se spusti hned po startu/restartu
void setup() {
  // Nastaveni pinu GPIO27 na vstup (jazyckovy kontakt)
  pinMode(GPIO_NUM_27, INPUT);
  
  // Nastaveni interniho pull-down rezistoru na pinu GPIO27
  rtc_gpio_pulldown_en(GPIO_NUM_27);

  // Nastartovani seriove linky pro potreby ladeni
  Serial.begin(115200);
  Serial.println("Start");

  // Reset Wi-Fi
  WiFi.persistent(false);
  WiFi.mode(WIFI_OFF);

  // Nastaveni pinu podsviceni displeje a jeho vypnuti, dokud nebude displej pripraveny
  pinMode(TFT_BL, OUTPUT);
  digitalWrite(TFT_BL, LOW);

  // Start displeje
  tft.init();
  // Orientace na sirku
  tft.setRotation(1);
  // Nastaveni veliksoti a geometrie pisma
  tft.setTextSize(2);
  tft.setTextDatum(MC_DATUM);
  // Zapnuti podsviceni displeje
  digitalWrite(TFT_BL, HIGH);

  // Zjisti duvod probuzeni z hlubokeho spanku
  esp_sleep_wakeup_cause_t duvodProbuzeni = esp_sleep_get_wakeup_cause();

  // Pokud je duvodem probuzeni preruseni typu EXT0,
  // probuzeni zpusobil jazykovy kontakt a nekdo tedy otevrel dvere
  if (duvodProbuzeni == ESP_SLEEP_WAKEUP_EXT0) {

    // Zvys pocitadlo otevreni dveri
    pocitadlo_otevreni++;

    // Nastav na displeji cervene pozadi a bilou barvu pisma
    tft.fillScreen(TFT_RED);
    tft.setTextColor(TFT_WHITE);

    // Vypis na stred displeje textovou hlasku
    char str_pocitadlo[50];
    sprintf(str_pocitadlo, "Pocitadlo: %zu", pocitadlo_otevreni);
    Serial.println("Cip probuzeny pomoci jazyckoveho kontaktu");
    tft.drawString(str_pocitadlo,  tft.width() / 2, tft.height() / 4 );
    tft.drawString("OTEVRENE DVERE",  tft.width() / 2, tft.height() / 2 );

    // Nyni se pripoj k Wi-Fi
    Serial.print("Pripojuji se k Wi-Fi ");
    WiFi.mode(WIFI_STA);
    WiFi.begin(SSID, PASWD);
    // Dokud se nepripojim, vypisuj kazdych 500 ms do seriove linky tecku
    // Spravne bych mel implementovat jeste nejaky timeout,
    // jinak se budou v pripade chyby tecky vypisovat az do konce veku
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print('.');
    }

    // Jsem pripojeny k Wi-Fi
    Serial.println(" OK");

    // Odesli mutlicastovou UDP zpravu "ALARM_DVERE_ZIVECZ"
    Serial.println("Odesilam multicastovou UDP zpravu");
    udp.beginMulticast(multicast_ip, multicast_port);
    udp.beginMulticastPacket();
    udp.print("ALARM_DVERE_ZIVECZ");
    udp.endPacket();
  }

  // Pokud byl oduvodem probuzeni neco jineho,
  // zobraz generickou zdravici. Zobrazi se treba po stisku tlacitka reset,
  // po pripojeni k napeti, po flashi pnoveho programu atp.
  else {
    tft.fillScreen(TFT_BLACK);
    tft.setTextColor(TFT_WHITE);
    Serial.println("Cip probuzeny jinym zpusobem");
    tft.drawString("Chytre dvere 1.0",  tft.width() / 2, tft.height() / 2 );
    delay(5000);
  }

  // Pokud jsou dvere nadale otevrene,
  // GPIO27 by mel vracet logickou nulu.
  // Ve smyce proto cekam, dokud tento pin nevrati logickou jednicku,
  // pricemz po kazde kontrole pockam sekundu, cimz primitivnim zpusobem vyresim
  // pripadny bouncing (kmitani logicke hodnoty pri zavirani dveri)
  Serial.print("Cekam na zavreni dveri ");
  while (!digitalRead(GPIO_NUM_27)) {
    delay(1000);
    Serial.print('.');
  }
  Serial.println(" OK");

  // Vse je hotovo, a tak jdu zase spat
  // Vyprazdni zasobnik seriove linky, pockej jeste sekundu a usni
  Serial.println("Jdu spat");
  Serial.flush();
  delay(1000);

  // Vypni podsviceni displeje
  digitalWrite(TFT_BL, LOW);
  // Vypni displej
  tft.writecommand(TFT_DISPOFF);
  // Prepni displej do rezimu spanku
  tft.writecommand(TFT_SLPIN);

  // Nastav znovu externi budicek typu EXT0 na pinu GPIO27
  // v pripade, ze se na nem objevi logicka nula
  esp_sleep_enable_ext0_wakeup(GPIO_NUM_27, 0);

  // Prejdi do rezimu hlubokeho spanku
  // Po tomto prikazu se nic dalsiho v programu nezpracuje,
  // coz plati i o smycce loop, ktera se spousti az po zpracovani funkce setup
  esp_deep_sleep_start();
}


// Smycka loop je prazdna, protoze k jejim uspusteni nikdy nedojde
// Cip o krok vyse prejsel do hlubokeho spanku
void loop() {
}

Zdrojový kód v Pythonu posluchače na multicastové IP adrese. Kód je testovaný výhradně na linuxovém systému a v Pythonu 3 (Raspberry Pi):

import socket
import struct

udp_ip = "224.0.0.0"
udp_port = 8888

try:
    spojeni = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    spojeni.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    spojeni.bind((udp_ip, udp_port))
    mreq = struct.pack("4sL", socket.inet_aton(udp_ip), socket.INADDR_ANY)
    spojeni.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

    print("Posloucham na " + udp_ip + ":" + str(udp_port))
    while True:
        zprava, ip = spojeni.recvfrom(1024)
        zprava = zprava.decode()
        print("UDP klient " + str(ip) + ": " + zprava)
        if "ALARM_DVERE_ZIVECZ" in zprava:
            print("!!! Nekdo prave otevrel dvere do redakce Zive.cz !!!")

except OSError:
    print("Zaviram UDP spojeni")
    spojeni.close()
Diskuze (30) Další článek: Dron místo poslíčka? Úžasné technologie, které kličkují mezi pravidly

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , ,