Programování | Pojďme programovat elektroniku | Chytrá domácnost

Pojďme programovat elektroniku: Kouzlený multicast a UDP, které pomohou Wi-Fi krabičkám k vzájemné komunikaci

  • Jak by spolu mohly komunikovat jednotlivé prvky IoT?
  • Vyzkoušíme si multicastové zprávy v LAN a UDP
  • Krabičky si samy najdou šéfa v síti

V minulém pokračování našeho seriálu o programování elektroniky jsme se podívali na několik možnosti, jak skrýt nevzhledný chuchvalec drátků, čipů a nepájivých polí do hezké krabičky, za kterou se nebudete muset stydět.

Dnes se opět vrátíme ke zdrojovému kódu, ovšem namísto oživování všemožných senzorů a dalších čipů se pokusíme navrhnout velmi jednoduchý protokol, pomocí kterého by mohla celá armáda krabiček u vás doma navzájem komunikovat.

Je to totiž právě komunikační protokol, který dělá z hromady sic k síti připojených mikropočítačů skutečnou chytrou domácnost. Internet jich nabízí celou řadu. Komunita si oblíbila zejména MQTT a ovládací portál Domoticz, my však půjdeme na samotnou dřeň a pokusíme se vyřešit několik základních problémů.

Krabička připojená do Wi-Fi je jen začátek

Pro náš dnešní experiment použijeme libovolnou prototypovací destičku založenou na Wi-Fi čipu ESP8266, který nám zajistí, aby se deska po spuštění připojila do domácí sítě, a maličké Raspberry Pi Zero W, které bude zase sloužit jako centrální jednotka chytré domácnosti – jednotka, která bude z krabiček sbírat informace, zajišťovat komunikaci s okolním světem aj.

Abych byl trošku konkrétnější, dejme tomu, že jako destičku použiji Wemos D1 Mini s shieldem elektromagnetického relé, které bude na povel spínat 230V napájení velké pokojové lampy. Na čipu poběží jednoduchý webový server, takže kdyby čip získal po spuštění IP adresu 192.168.1.198, světla rozsvítím třeba zavoláním adresy http://192.168.1.198/ON.

48ed9540-b07f-41ed-9700-b057165b9e78db545639-34e8-4cf9-a0e8-d4afa21e3ad9d30cffe4-e939-41f4-b183-e0d24a860eb361897831-18b0-441e-9598-8fd1aa5495d2
Wemos D1 Mini s shieldem elektromagnetického relé, které dodá staré pokojové lampě ovládání z webového prohlížeče

Spuštění podobného HTTP serveru je na čipech ESP8266 poměrně snadné, součástí jejich podpory v Arduinu jsou totiž vestavěné knihovny. Jeden takový server, který vypisoval na webové stránce meteorologické údaje, jsme si vyzkoušeli zde a tady jsme si zase pohráli se samotným elektromagnetickým relé.

Duší chytré domácnosti je ve skutečnosti automatická integrace

Tímto způsobem tedy můžeme připojit starou lampu do internetu a zapínat ji načtením webové adresy třeba v prohlížeči, má to ale jeden ergonomický háček. Pokud nebude mít ovládací čip nastavenou statickou IP adresu, ta se může postupně měnit, a pokud takové chytré elektroniky doma připojíte více, vůbec se v tom nevyznáte.

No, a to už na řadu nastupují ony centrální jednotky chytré domácnosti – třeba zrovna levné, maličké, a přitom dostatečně výkonné Raspberry Pi Zero W s Linuxem. Může na něm běžet obvyklý HTTP server jako třeba Apache, Lighttpd nebo třeba Nginx a drobná databáze, ve které budou uložené informace o všech krabičkách v naší domácnosti.

95a3ee20-f1be-415f-ae28-c23581c67aa85ca324c4-8dfe-44a6-873a-331b5081e9710aa3892c-dfa6-4a4a-b42b-36442208cf62
Zdání klame. Ačkoliv je Raspberry Pi Zero W menší než většina prototypovacích desek s primitivními mikrokontrolery, nabízí dostatečně výkonný armový procesor pro běh Linuxu i nejrůznějších serverů. Bez problémů může fungovat jako centrální jednotka domácnosti.

Namísto přímé komunikace s každou z nich tedy prostě do prohlížeče zadáte adresu tohoto centrálního bodu a zobrazí se vám šachovnice s údaji o všech krabičkách a s jejich ovládacími prvky – třeba spínačem pro zapnutí našich světel.

Mohlo by to vypadat třeba jako ve videu níže. Právě to je totiž pohled na můj centrální bod, který běží na Raspberry Pi. Jak vidíte, mám doma jakousi Ambientní lampu a Meteostanici a na stránce se rovnou vypsaly i čerstvé informace z těchto jednotek. U lampy pak nechybí tlačítko pro zapnutí a vypnutí a také nastavení automatického zapnutí v určitý čas.

Maličké Raspberry Pi slouží jako centrála mé domácnosti:

Jmenuji se Ambientní lampa, mám roli lampy a podporuji akce 0, 1, 2 a 3

Nabízí se otázka, jak jsem vlastně tuto šachovnici vytvořil. To jsem musel všechny údaje o každé z krabiček vyplnit do nějakého formuláře, který je pak uložil do databáze? Samozřejmě že ne! Všechno se zpracovalo plně automaticky, protože každá z krabiček při prvním kontaktu s Raspberry Pi mikropočítači sama sdělila, jak se jmenuje, jaká je její role a co umí.

525318508
Nová krabička se hned po připojení k Wi-Fi ohlásí centrální jednotce k registraci

Když jsem tedy poprvé připojil mikrokontroler ovládající lampu k síti, spojil se s Raspberry Pi a řekl mu:

  1. Ahoj, moje role je lampa
  2. Jmenuji se Ambientní lampa
  3. Podporuji akce zapnout, vypnout, nastavit čas a získat data v JSON

Jakmile Raspberry Pi tato data získá, může pro tuto krabičku použít obrázek lampy, vykreslit spínač, nastavení časovače, a jelikož lampa nabízí třeba přes vlastní HTTP server ještě JSON data, zažádá si o ně. Najde v nich třeba údaj o teplotě, kterou vypíše v dlaždici úplně dole.

888247343 580821951
Každá z krabiček po prvním přihlášení do Wi-Fi sama sdělila centrálnímu hubu, co je zač a co umí. Centrální hub pak pomocí AJAX a JSON komunikace s krabičkami vykreslí jejich seznam i ovládací prvky.

Centrála si zároveň uloží IP adresu krabičky a její MAC, kterou může používat jako unikátní identifikátor.

Krabička by se takto mohla ozývat centrále třeba při každém novém přihlášení do Wi-Fi skrze HTTP GET nebo POST. Na začátku jejího programu bychom tedy zavolali třeba adresu:

http://192.168.1.8/registrace.php?role=1&akce=0,1,2,3&jmeno=ambientni+lampa&mac=60:01:94:0b:64:04

A Raspberry dostupné právě na IP adrese 192.168.1.8 by si aktualizovalo údaje – třeba IP adresu lampy, která se mohla kvůli DHCP změnit.

Haló, kdo je v této síti šéf?

Jenže je tu jeden háček.

Jak má sakra Wi-Fi čip naší lampy vědět, že je centrální jednotka Raspberry Pi Zero W dostupná zrovna na IP adrese 192.168.1.8, aniž bych ji to dopředu nesdělil přímo v programovém kódu?

No, a to se už konečně dostáváme k dnešní ukázce, vyrobíme si totiž naprosto primitivní auto-discovery a registrační systém, který v různých podobách používá celá armáda nejrůznějších síťových zařízení. Třeba televizor s DLNA, který v lokální síti hledá zdroje multimédií (počítač, mobil, NAS) a také dopředu nezná jejich IP adresy.

Multicastový UDP datagram

Jinými slovy, aby se naše lampa dozvěděla, kdo je tady šéf – centrála chytré domácnosti, vyšle do sítě LAN hromadný muticastový UDP datagram, ve kterém budou uložené jak popisné informace o lampě, tak žádost o registraci a odpověď šéfa.

299451495
Nové zařízení se nejprve skrze multicast zeptá celé sítě, kdo je centrálou. Ta zprávu zachytí, nové zařízení zaregistruje a odpoví mu, aby vědělo, jakou má IP adresu.

Cílem datagramu nebude IP adresa konkrétního zařízení, ale speciální rezervovaná lokální multicastová adresa 224.0.0.0 a port, třeba 8201 (protože představuje rok a měsíc mých narozenin, čili to nemůže být špatný port). Všechna zařízení v LAN, která budou na této speciální adrese a portu poslouchat, zprávu obdrží a mohou na ni odpovědět. Buď opět hromadně, anebo už přímou linkou.

Naše testovací lampa tedy na multicastovou adresu výše odešle datagram, který bude obsahovat prostý ASCII řetězec s údaji:

REGISTRACE/Ambientni lampa/1/0,1,2,3/60:01:94:0b:64:04

Zprávu bude moci zachytit jakékoliv zařízení v síti, které bude poslouchat na stejné adrese a portu, čehož by se dalo dále využít, v našem případě to ale bude jen naše Raspberry Pi Zero W a drobný program napsaný na pár řádcích Pythonu.

Raspberry zachytilo zprávu

Jakmile zprávu program zachytí a zjistí, že obsahuje slovo REGISTRACE, vyseparuje z textu jednotlivé hodnoty a může je dále zpracovat – třeba uložit do databáze. Já však klasickou a náročnou SQL databázi na maličkém Raspberry nepoužívám. Namísto toho vytvořím soubor 60:01:94:0b:64:04.krabicka ve složce krabicky a uložím do něj všechny hodnoty včetně aktuální IP adresy ve formátu JSON.

198417070
Vlevo testovací aplikace na desce NodeMCU (ESP8266), která po obdržení znaku „s“ skrze sériovou linku odešle multicastem svoje popisné údaje a dotaz, kdo je v této LAN centrála. Vpravo pak SSH terminál připojený k Raspberry Pi Zero W a skript v Pythonu, který poslouchá na multicastové IP adrese.

Jako databázi tedy používám samotný souborový systém. Vzhledem k počtu krabiček to naprosto stačí a zátěž mikropočítače je minimální. Když bude chtít Raspberry Pi Zero W získat pole všech mých krabiček, jednoduše projde soubor po souboru v adresáři krabičky a načte si jednotlivé JSONy.

JA JSEM TADY VELKY SEF

Nakonec zbývá jediné. Raspberry Pi Zero W musí dát krabičce vědět, že je to právě ono, které tomu tady šéfuje. Jelikož zná IP adresu krabičky, od které právě přijalo multicastovou zprávu, může to udělat nejrůznějším způsobem. Předpokládejme, že na krabičce běží HTTP server, čili Raspberry Pi by mohlo vyslat HTTP GET, ale stejně tak by mohlo odpovědět opět skrze multicast, aby se to dozvěděla nejen naše krabička, ale třeba i všechny ostatní.

Já jsem v příkladu použil odpověď přímým UDP datagramem, který dorazil jen krabičce. Obsahuje zprávu JA JSEM TADY VELKY SEF, a jakmile ji krabička zachytí, může si IP adresu centrály uložit do persistentní paměti (třeba skrze knihovnu EEPROM), aby se nemusela ptát po každém dalším startu.

Podívejte se na celý proces ve videu:

Je to jednoduché a přenositelné

A to je vlastně celé. Dnes jsme si tedy vyrobili prototyp jednoduchého síťového auto-discovery systému, díky kterému je celá chytrá domácnost velmi snadno přenositelná – pružná. Když změníte IP adresu centrály, nebudete muset natvrdo přeprogramovat všechny krabičky, ty se totiž samy zeptají, kdo je tady šéf.

Práce s multicastem a protokolem UDP je ke všemu na čipech ESP8266 a v prostředí Arduino naprosto primitivní a podobná sériové lince, čili se vše vejde na pár řádků kódu. Dokonce je to o píď jednodušší než na straně Pythonu.

A nakonec zdrojový kód

Přesvědčte se sami, na závěr totiž nesmím zapomenout na samotný kód a zkušební aplikaci. Aby byla co nejnázornější, program na straně ESP8266 odešle registrační datagram nikoliv po startu, ale pokaždé když mu skrze sériovou linku pošlete znak s. Program na straně Raspberry Pi pak bude příchozí zprávy vypisovat do terminálu.

Zdrojový kód na čipu ESP8266:

#include <Streaming.h>
#include <ESP8266WiFi.h>
#include <WiFiUDP.h>

//Podpis centraly. Pokud dorazi tato zprava, jeji odesilatel je centrala
#define PODPIS_CENTRALY "JA JSEM TADY VELKY SEF"

// Prihlasovaci udaje k Wi-Fi
const char* ssid = "Klobouk";
const char* heslo = "************";

// Multicastova IP adresa, viz https://cs.wikipedia.org/wiki/IP_multicast
IPAddress multicast_ip(224, 0, 0, 0);
// UDP port
const uint16_t multicast_port = 8201;

// Trida pro UDP/multicast komunikaci
WiFiUDP udp;

// Funkce setup se zpracuje hned po startu
void setup() {
  // Uvitani v seriove lince
  // Pri rychlejsi zapis pouzivam knihovnu Streaming.h
  // Viz http://arduiniana.org/libraries/streaming/
  Serial.begin(115200);
  Serial << endl;
  Serial << "Demonstrace identifikace centraly pomoci multicastu" << endl;
  Serial << "Prihlasuji se k Wi-Fi: " << ssid << " ";

  // Spusteni Wi-Fi v rezimu klient a prihlaseni se k AP
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, heslo);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial << '.';
  }
  Serial << endl << "Pripojeno na IP adrese " << WiFi.localIP();
  Serial << ", " << WiFi.RSSI() << " dBm" << endl << endl;
  Serial << "K odeslani multicastoveho datagramu zadej: s" << endl << endl;

  // Spusteni UDP poslechu na portu 8201
  udp.begin(multicast_port);
}

// Funkce loop se opakuje stale dokola
void loop() {
  // Pokud skrze seriovou linku dorazi znak s,
  // odesli skrze multicast zadost o registraci a odpoved centraly
  while (Serial.available()) {
    char prikaz = Serial.read();
    if (prikaz == 's') {
      Serial << "Posilam na multicast adresu ";
      Serial << multicast_ip << ":" << multicast_port << endl;
      Serial << "Obsah zpravy: ";
      Serial << "REGISTRACE/Ambientni lampa/35/0,1,2,3/";
      Serial << String(ziskejMAC()) << endl;
      odesliMulticast("REGISTRACE/Ambientni lampa/35/0,1,2,3/" + String(ziskejMAC()));
    }
  }

  // Zjisti, jestli dorazila nejaka UDP data na port 8201
  // a zkontroluj, jestli je to odpoved centraly
  udp.parsePacket();
  while (udp.available()) {
    String odpoved = udp.readString();
    odpoved.trim();
    Serial << "Prichozi zprava od " << udp.remoteIP() << ": " << odpoved << endl;
    // Pokud zprava obsahuje podpis centraly,
    // mohu si ulozit IP adresu odesilatele, je to totiz centrala
    if (odpoved == PODPIS_CENTRALY) {
      Serial << "Vyborne, toto je IP adresa centraly!" << endl;
    }
    Serial << endl;
  }
}

// funcke pro odeslani textove zpravy skrze multicast
void odesliMulticast(String zprava) {
  udp.beginPacketMulticast(multicast_ip, multicast_port, WiFi.localIP());
  udp.write(zprava.c_str());
  udp.endPacket();
}

// Funkce pro ziskani MAC adresy
// a prevedeni do textoveho formatu
char* ziskejMAC() {
  uint8_t MAC[6];
  static char cMAC[19];
  WiFi.macAddress(MAC);
  sprintf(cMAC, "%02x:%02x:%02x:%02x:%02x:%02x", MAC[0], MAC[1], MAC[2], MAC[3], MAC[4], MAC[5]);
  return cMAC;
}

Zdrojový kód aplikace v Pythonu, která poslouchá na Raspberry Pi Zero W:

# Knihovny pro praci se siti a JSON
import socket
import struct
import json

# Multicastova IP adresa a UDP port
# Viz https://cs.wikipedia.org/wiki/IP_multicast
multicast_ip = "224.0.0.0"
multicast_port = 8201

# Nastaveni UDP multicastu pro poslech dat jen v LAN a
# z nastavene multicastove IP adresy a portu
spojeni = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
spojeni.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
spojeni.bind((multicast_ip, multicast_port))
mreq = struct.pack("4sL", socket.inet_aton(multicast_ip), socket.INADDR_ANY)
spojeni.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)

print("Posloucham na 224.0.0.0:8201...")
print("")

# Nekonecna smycka, dokud program neukoncim
while True:
    # Pokud dorazila zprava, uloz ji a adresu odesilatele
    # 1024 predstavuje velikost pameti pro zpravu, tedy nejvyse 1 024 B
    zprava, adresa = spojeni.recvfrom(1024)
    print("Prichozi zprava od " + adresa[0] + ": " + zprava)
    # Pokud zprava obahuje slovo REGISTRACE,
    # vyseparuj z ni jednotlive hodnoty oddelene znakem /,
    # uloz je do JSON objektu a vypis na monitor
    if "REGISTRACE" in zprava:
        zprava = zprava.split("/")
        zarizeni = {}
        zarizeni["Nazev"] = zprava[1]
        zarizeni["Role"] = int(zprava[2])
        zarizeni["Akce"] = zprava[3]
        zarizeni["IP"] = adresa[0]
        zarizeni["MAC"] = zprava[4].upper()

        print("")
        print("Nove zarizeni touzi po registraci:")
        print("Nazev: " + zarizeni["Nazev"])
        print("Role: " + str(zarizeni["Role"]))
        print("Akce: " + zarizeni["Akce"])
        print("IP adresa: " + zarizeni["IP"])
        print("MAC adresa: " + zarizeni["MAC"])
        print("")

        # Uloz popis zarizeni v JSON do souboru krabicky/macadresa.krabicka
        with open("krabicky/" + zarizeni["MAC"] + ".krabicka", "w") as soubor:
            json.dump(zarizeni, soubor)

        # Odesli zpet zpravu s podpisem centraly,
        # aby si krabicka mohla ulozit jeji IP adresu a napriste s ni komunikovala
        # treba skrze HTTP
        print("Posilam krabicce svuj podpis, at vi, jakou mam IP adresu")
        print("")
        spojeni.sendto("JA JSEM TADY VELKY SEF", adresa)
Diskuze (35) Další článek: Týden Živě: Mashinky, KRACK, Fall Creators Update a máslo

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