Kostra hotové IoT aplikace pro ESP32 / ESP8266. A k tomu nějaký server… (3/4)

Jak vypadá aplikace na mikrokontroléru? Je to jednoduché a jde to rychle!

Koukněte na nabídku sample aplikací. Všechny jsou otestovány v Arduino IDE s implementací ESP8266 v2.7.1 resp. ESP32 v1.0.4.

Koukneme nejprve na tu nejjednodušší – demo, kde se neřeší uspávání.

Konfigurace

Základní konfigurace aplikace je v AppConfig.h.

1) Dvojice nastavení MANAGE_WIFI a RUN_WIFI_AT_STARTUP nastavuje WiFi.

  • MANAGE_WIFI=true …. WiFi se zapne, když budou připravená nějaká data k odeslání a pak se zase vypne
  • MANAGE_WIFI=false, RUN_WIFI_AT_STARTUP=true … WiFi bude zapnuté hned od startu, aplikace ho může řídit příkazy startWifi() a stopWifi().
  • MANAGE_WIFI=false, RUN_WIFI_AT_STARTUP=false … WiFi bude vypnuté, aplikace ho může řídit příkazy startWifi() a stopWifi().

2) CONFIG_AP_PASSWORD určuje heslo pro WiFi AP konfiguračního portálu.

3) CONFIG_BUTTON určuje, na jakém pinu je tlačítko, jehož držením při startu se spouští konfigurační portál. CONFIG_BUTTON_START_CONFIG určuje při jaké hodnotě pinu se portál spouští. (Rozhodnutí, zda se má spustit konfigurační portál, se dělá ve funkci wifiStatus_ShouldStartConfig(), která používá tyto konfigurační hodnoty – nicméně její funkci můžete upravit.)

4) Dále si musíte zvolit, kde bude uložena fronta zpráv k odeslání – buď v běžné RAM (RA_STORAGE_RAM) nebo (jen na ESP32) v RTC MEM (RA_STORAGE_RTC_RAM). A také nastavit její velikost (RA_STORAGE_SIZE).

  • Poznámka: jedna zpráva k odeslání má cca 18 byte.

5) Konfigurační položka RA_CONFIG_PASSPHRASE určuje passphrasi, ze které se odvodí heslo pro šifrování konfiguračního souboru.

6) A poslední důležitější sekce určuje, zda má aplikace blikáním stavové LED hlásit, co zrovna dělá. Začíná povolením funkce (USE_BLINKER), nastavením vlastní LED (BLINKER_PIN, BLINKER_LED_OFF) a následují nastavení, jaké budou jednotlivé použité sekvence blikání.

Poslední nastavení je v souboru ConfigProvider.h, kde se v položce CONFIG_FILE_NAME volí jméno konfiguračního souboru ve filesystému.


Vlastní aplikace

No a teď se můžeme podívat na vlastní aplikaci – tedy v50a-demo-power_always_on.ino.

Oproti běžné Arduino aplikaci s funkcemi setup() a loop() je v aplikaci několik „povinných“ funkcí navíc:

  • raGetAppName() vrací jméno aplikace, které se při přihlášení reportuje na server. Můžete nechat tak, jak je, kdy vyplní jméno hlavního INO souboru a timestamp sestavení aplikace. Ale můžete upravit, jak chcete.
  • raAllWasSent() se používá v low-power aplikacích. Je zavoláno, pokud jsou odeslány všechny připravené zprávy na server a je tak možno zařízení uspat. (Je zavoláno pouze tehdy, pokud aplikace po zapsání posledních dat k odeslání zavolá ra->setAllDataPreparedFlag()).
  • wifiStatus_ShouldStartConfig() je zavolána při bootu. Pokud vrátí true, je spuštěn konfigurační portál. Defaultní implementace se koukne, zda je stisknuté tlačítko na jednom z pinů mikrokontroléru.
  • wifiStatus_StartingConfigPortal() je zavolána, pokud se spouští konfigurační portál. Je určena třeba k tomu, aby zařízení s displejem mohlo vypsat na displeji informace, jak se připojit ke konfiguračnímu portálu.
  • wifiStatus_Starting() informuje, že se spouští WiFi.
  • wifiStatus_Connected() je zavoláno, když se podaří připojit k WiFi.
  • wifiStatus_NotConnected() je zavoláno, když dojde k odpojení od WiFi.

V setup() musí být zavolání ratatoskr_startup() a v loop() musí být zavolání tasker.loop(). To je vše.

No a jaké máte k dispozici funkce?


Odesílání dat

Definice kanálu

Před odesláním dat je potřeba definovat pro každý senzor komunikační kanál. Komunikační kanál je určený typem zpracování, jednotkou, jménem, předpokládaným tempem zasílání a případně násobícím faktorem.

int ch1 = ra->defineChannel( DEVCLASS_CONTINUOUS_MINMAXAVG, 8, "analog_in", 3600 );

Typ zpracování může být:

  • DEVCLASS_CONTINUOUS_MINMAXAVG – Měření spojité hodnoty (typicky teplota, napětí). Z naměřených hodnot se počítají sumární data po hodinách a dnech (minima, maxima, průměry) pro rychlejší vykreslení dlouhých grafů.
  • DEVCLASS_CONTINUOUS – Měření spojité hodnoty (typicky teplota, napětí).
  • DEVCLASS_IMPULSE_SUM – Impulzní měření (typicky impulzní výstup plynoměru, srážkoměr). Měří počet impulzů.

Jednotka (měřená veličina) je udána kódem. Ve webové administraci je vidět seznam definovaných jednotek.

Jméno identifikuje senzor. Bude vidět ve webovém rozhraní i v grafech. Smí obsahovat jen písmena bez diakritiky, čísla, pomlčku a podtržítko.

Interval zasílání zpráv (zde 3600) udává maximální odstup zpráv ze zařízení (v sekundách). Pokud nepřijdou zprávy delší dobu, než je zde nastaveno, monitoring může spustit poplach, že senzor nefunguje.

Pokud senzor vrací hodnotu, kterou je potřeba uživateli před dalším zpracování přepočítat, je zde možné dát násobící faktor. To se dá použít třeba při čtení impulzního výstupu plynoměru, který pošle impulz jednou za každých 0.1 m3 plynu – ale chceme měřit spotřebu v kWh. 0.1 m3 plynu je 1.05 kWh – takže zde nastavíme jednotku 6 (kWh) a přepočtový faktor 1.05 a z impulzů budou přímo kWh. Pokud nám v budoucnu změní plynoměr na takový, který dává 100 impulzů na 1 m3, stačí jen změnit přepočtový faktor na 0.105.

ch1 = ra->defineChannel( DEVCLASS_IMPULSE_SUM, 
6, 
(char*)"plynomer", 
3600, 
1.05 
);

Tím, že se kanály definují dynamicky z mikrokontroléru, je vyřešena třeba situace, kdy načítáte teplotu z onewire teploměrů DS18B20. Je jedno, kolik teploměrů připojíte – aplikace si je osahá a pro každou nalezenou adresu (ID teploměru) si nadefinuje kanál. Není třeba nic konfigurovat na serveru.

Pozor! Každý kanál by měl být definován jen jednou za jeden start mikrokontroléru. Ne před každým odesláním zprávy.

Odeslání dat

Odeslání naměřené hodnoty je jednoduché. Pro spojité hodnoty (teplota, napětí, …) zavoláte:

ra->postData( ch1, 1, voltage );

První parametr je číslo kanálu – vzniklé při definici kanálu výše.

Druhá je priorita. Priority jsou 1-15; vyšší číslo je vyšší priorita. Pokud v úložišti dojde místo, pak pro uložení zprávy s vyšší prioritou se smažou zprávy s nižší prioritou.

  • K čemu to je? Když bude nějakou dobu nefunkční spojení na server (nedostupná WiFi, výpadek připojení, výpadek serveru), zprávy se ukládají do úložiště. Představme si, že měříme teploty s šestiminutovou periodou (10x za hodinu) a máme úložiště na 240 zpráv. Pokud aplikace odešle běžné zprávy s prioritou 1, jednou za hodinu pošle zprávu s prioritou 2 a jednou za čtyři hodiny pošle zprávu s prioritou 3, pak při výpadku spojení prvních 24 hodin úložiště udrží všechny naměřené hodnoty a po obnovení spojení se všechny předají na server. Pokud bude výpadek trvat déle, postupně se „šestiminutové“ zprávy začnou mazat, ale v úložišti zůstanou zprávy po jedné hodině – až 10 dní. Pokud bude výpadek trvat déle, začnou se mazat i tyto zprávy, ale zůstanou zprávy po čtyřech hodinách. Pokud se spojení časem obnoví, dostanete alespoň některé naměřené body, není ztraceno vše.

Třetí je naměřená hodnota (double).


Pokud chcete odeslat impulzní hodnotu, je k dispozici odlišná funkce:

ra->postImpulseData( ch1, 1, count );

Opět je zde kanál, priorita a počet (long). Předává se celkový počet impulzů od startu mikrokontroléru. Tj. při přechodu mezi osmým a devátým pulzem nepředáváte „1“, ale „9“.

Zavolání funkce postData() resp. postImpulseData() data pouze vloží do fronty. Neodešlou se hned – data jsou odeslána až následně ve funkci loop().

Pokud po vložení dat k odeslání aplikace zavolá

 ra->setAllDataPreparedFlag();

pak po jejich odeslání bude zavolána funkce raAllWasSent().


Odeslání souboru na server je vidět např. v této aplikaci.

int rc = ra->sendBlob( (char*)data, int delka_dat, time(NULL), (char*)"camera", (char*)"jpg"  );

Parametry jsou:

  • pointer na data
  • délka dat
  • čas pořízení souboru – relativní čas na zařízení (po předání na server je to ztransformováno na absolutní čas)
  • jméno souboru
  • přípona

Poznámky:

  • Pokud je přípona „jpg“, server umožňuje zobrazovat přijaté obrázky jako galerii.
  • Pokud je jméno souboru „camera“ a přípona „jpg“, server dělá základní analýzu obrazu a dokáže při zobrazení filtrovat fotky, kde v noci není nic na obrázku zobrazeno.

Konfigurace

Pro čtení dat z konfigurace jsou k dispozici funkce

char * hodnota = config.getString( "nazev_konfiguracni_polozky", "defaultni_hodnota" )
long hodnota = config.getLong( "nazev_konfiguracni_polozky", 12345 );
  • Pokud položka v konfiguračním souboru neexistuje, je vrácena určená defaultní hodnota.
  • U funkce getString() je návratová hodnota platná až do nejbližšího dalšího zavolání loop(). Tj. můžete jí použít hned, ale neukládejte si vrácený pointer na pozdější dobu. Pokud budete stejnou hodnotu potřebovat později, zeptejte se na ní pak znovu. Je to kvůli tomu, že konfigurační soubor se může změnit – např. může přijít změna ze serveru.

Konfigurační položky je možné nastavovat ze serveru, z webové administrace.

Z aplikace je možné zapsat hodnotu do konfigurace následovně:

config.setValue( "nazev_konfiguracni_polozky", "hodnota" );

Při startu aplikace (který není bootem po deep sleep) se pro informaci vypisuje konfigurace na sériový port:

15:03:35.542 -> config:
15:03:35.542 ->   wifi_ssid='BROPKA'
15:03:35.542 ->   $wifi_pass=***
15:03:35.542 ->   ra_url='lovecka.info/ra/ra'
15:03:35.542 ->   ra_dev_name='u2:esp32b'
15:03:35.542 ->   $ra_pass=***

(Všimněte si, že hesla – položky se znakem $ na začátku jména – se nevypisují. Tyto položky se také ukládají do konfiguračního souboru šifrovaně.)

Ukládání změn konfigurace do souboru – ať po změně z aplikace, tak po změně ze serveru – je automatické.


Logování

Pro logování informací z aplikace je k dispozici

logger->log( "text" );
logger->log( "syntaxe jako printf, takze %d x %s", 10, "ahoj" );

Proč používat tohle a ne Serial.printf()? Protože logování přes logger->log se dá v AppConfig.h globálně vypnout. A taky proto, že pro něj funguje log shipping, tj. pokud to zakompilujete (AppFeatures.h) a zapnete (poslat konfigurační položku log_ship=1), tak se zapisované logy průběžně replikují na server.

Aplikace standardně výstupy logger->log loguje na sériový port (115200 bps).

Liší se výpis při startu aplikace po běžném resetu (kompletní včetně důvodu restartu, vlastností hardware, použité aplikace a výpisu konfigurace) a po probuzení z deep sleep (minimum informací).

Ukázka výpisu z aplikace, která měří teplotu přes DS18B20:

5:03:35.402 -> #
15:03:35.402 -> STRG RTCRAM data invalid
15:03:35.402 -> rst rsn 0; RAM 262.2 kB; PSRAM 4.0 MB; CPUr1 240 MHz; flash 80 MHz; RA 4.3.3
15:03:35.402 -> v43b-low_power-DS18B20__ESP32.ino - Jan  3 2021 15:03:12

15:03:35.542 -> config:
15:03:35.542 ->   wifi_ssid='BROPKA'
15:03:35.542 ->   $wifi_pass=***
15:03:35.542 ->   ra_url='lovecka.info/ra/ra'
15:03:35.542 ->   ra_dev_name='u2:esp32b'
15:03:35.542 ->   $ra_pass=***

15:03:35.542 -> 0 senzoru, zkusime to za chvili znovu
15:03:36.576 -> Senzoru: 1
15:03:36.757 -> #0 98fb5a05 ->  20.50 C
15:03:36.757 -> TP ch #1 for [98fb5a05]
15:03:36.757 -> TP #1 <= 2.050000e+01
15:03:37.086 -> ~ wifi: ON [BROPKA], on 0 s, off 3 s, usage 0 %, uptime 3 s
15:03:38.486 -> * wifi [192.168.32.142], 1 s

15:03:38.486 -> RA recs:2 len:40

15:03:38.486 -> CONN TIME

15:03:38.579 -> CONN TIME=1609682618
15:03:38.579 -> CONN LOGIN
15:03:39.091 -> CONN rc=200 [2275:6w0mN7zv]
15:03:39.091 -> CONN DATA
15:03:39.137 -> CONN rc=200
15:03:39.137 -> STRG purge 39B, 0/1992

15:03:39.184 -> ~ wifi: OFF, on 2 s, off 3 s, usage 41 %, uptime 5 s, last cycle 2 s
15:03:39.184 -> -> raAllWasSent()
15:03:39.184 -> deep sleep for 80 s, uptime 5.1 s, time 5 s

Na začátku výpisu je informace, že v RTC MEM nejsou platné údaje o přihlášení, výpis důvodu resetu, a konfigurace hardware, a informace o aplikaci. Pak následuje výpis konfigurace.

Následně aplikace změří teplotu, založí kanál pro senzor 2498fb5a0500 (číslo kanálu je 1) a zapíše naměřenou hodnotu 20.5 do tohoto kanálu.

Výsledkem zapsání dat do fronty je automatické zapnutí WiFi a tedy výpis informace o tom, že WiFi je zapnuté a posléze připojené. Následuje tučně označený výpis, že se odesílají dva záznamy, aplikace si načítá čas ze serveru, přihlašuje se a odesílá data. Na konci je uvolněno 39 byte z odesílací fronty a fronta je prázdná (0/1992 byte).

Po odeslání se WiFi vypne a je tedy vypsána změna stavu.

No a protože jsou odeslána všechna data, je zavolána funkce raAllWasSent(), která zařízení přepne do deep sleep.


WiFi

Pokud jste nenechali řízení WiFi na systému, tedy máte nastaveno v AppConfig.h MANAGE_WIFI=false, aplikace může řídit stav WiFi pomocí příkazů startWifi() a stopWifi(). Nesahejte na přímé ovládání WiFi v ESP8266/ESP32, prosím.

Zjištění aktuálního stavu (zap/vyp) WiFi je funkcí isWifiOn();


Periodické spouštění akcí

Pro periodické akce v aplikaci doporučuji použít knihovnu Tasker, která je v aplikaci již používána.

Chci-li dělat nějakou akci jednou za minutu, nadefinuji jí jako samostatnou funkci bez parametrů:

void nactiHodnotu() {
    // neco delam a odeslu
}

a v setup() nebo kdekoli jinde udělám

tasker.setInterval( nactiHodnotu, 60000 );

Jednou za minutu Tasker zavolá tuto funkci.

Chci funkci spustit jen jednou, ale za nějaký čas (tedy ne hned)? Následující kód spustí funkci nactiHodnotu() za 2 minuty.

tasker.setTimeout( nactiHodnotu, 120000 );

Poznámky:

  • Čas spuštění nemusí být přesný. Pokud aplikace dělá něco jiného (běží jiná dlouhá funkce), může se ke spuštění vaší akce dostat se zpožděním.
  • Pokud má vaše akce nastavenou krátkou periodu (třeba 1 sec) a běh aplikace je blokován jinou dlouho běžící funkcí (třeba 10 sekund v kuse), je pak vaše funkce zavolána rychle za sebou opakovaně – pro každý prošvihlý termín startu. Takže 10 sekund nic, pak 10 zavolání hned po sobě.

Ostatní užitečné funkce

Funkce, které se mají volat v rámci přerušení, musí být v RAM, a je potřeba je patřičně označit. Toto označení se ale liší pro ESP32 a ESP8266. Proto zde máme makro, které to sjednocuje. ESP_ISR_RAM je podle platformy buď ICACHE_RAM_ATTR nebo IRAM_ATTR.

void ESP_ISR_RAM isrImpulse1()
{
  // toto se zavolá při přerušení
}

Pro přepnutí do deep sleep (včetně výpisu informací a správného vypnutí periférií) je možné použít:

ra->deepSleep( long usec );

a na ESP32 je i funkce pro přepnutí do light sleep (kde druhý parametr určuje, zda se má vypsat na sériový port informace):

ra->lightSleep( long usec, bool logInfo );

Stav zaplnění úložiště pro odesílané zprávy (v procentech, 0-100) vrací funkce:

int procentni_zaplneni = ra->getStorageUsage();

A jak to spustit?

Pro začátek si to můžete vyzkoušet proti mému serveru. Nicméně to je server na hraní, na domácí lince, bez garantované dostupnosti atd. Předpokládám, že si serverovou aplikaci časem pustíte u sebe – nebo se dohodneme a udělám někde v hostingu sdílenou instanci.

Zaregistrujte si uživatelský účet na této adrese. V menu Zařízení založte zařízení a následně ho rozklikněte v seznamu. Objeví se konfigurační hodnoty:

Pak nahrajte aplikaci do mikrokontroléru. Nemá konfiguraci, tak se přepne do konfiguračního portálu.

Na telefonu najděte WiFi AP „RA_<nějaké číslo>“ a připojte se k němu. Defaultní heslo v AppConfig.h je „aaaaaaaa“ (8x „a“).

Následně se telefon často na konfigurační portál přepne sám; pokud to neudělá, zadejte v prohlížeči adresu 10.0.1.1.

Objeví se stránka s modrými tlačítky (vlevo). Stiskněte „Configure WiFi“.

Na konfigurační stránce (vpravo) je nahoře výpis viditelných AP. Klikněte na to správné, vyplní se do SSID. Doplňte heslo k WiFi, konfiguraci připojení k serveru a stiskněte Save. Pokud je vše v pořádku, aplikace se po chvíli spustí v běžném režimu.

No a jakmile začne aplikace posílat data na server, v detailu zařízení uvidíte vzniklé senzory a získané hodnoty:

V sekci Informace se u zařízení ukáže, jaká aplikace v něm běží a kdy se přihlásila:

Pak už si můžete v hlavním menu v položce Grafy nadefinovat vlastní graf, který bude hodnoty ze zařízení zobrazovat.


Poznámka: První start na zařízení, kde aplikace ještě neběžela, nebo kde se změnilo rozdělení flash paměti, může trvat delší dobu. Součástí prvního startu je formátování flash filesystému. Viz následující výpis, kde je vidět, že nejprve se vypíše chyba při mountování filesystému, protože tam žádný filesystém není, a teprve po 23 sekundách program pokračuje informací, že na filesystému nebyl nalezen soubor s konfigurací.

19:53:35.392 -> STRG RTCRAM data invalid
19:53:35.392 -> rst rsn 0; RAM 249.9 kB; PSRAM 4.0 MB; CPUr1 240 MHz; flash 80 MHz; RA 4.3.2
19:53:35.392 -> app: E:\dev.moje\RatatoskrIoT\MCU\v43d-espcam_a_DS18B20\v43d-espcam_a_DS18B20.ino - Dec 26 2020 19:53:03
19:53:35.392 -> E (1460) SPIFFS: mount failed, -10025
19:53:58.039 -> @ config file not found: /config2.ra

Všechny díly:

  1. Úvod – co to dělá a co to nedělá
  2. Bezpečnost
  3. Aplikace na mikrokontroléru
  4. Serverová strana – funkce, instalace

Napsat komentář