Každý ví, jak vypadá srážkový radar v mobilu nebo třeba na webu ČHMÚ. My ho dnes zhmotníme na RGB LED mapě republiky. Má 72 měst a LaskaKit ji prodává za necelých sedm stovek 

Každý ví, jak vypadá srážkový radar v mobilu nebo třeba na webu ČHMÚ. My ho dnes zhmotníme na RGB LED mapě republiky. Má 72 měst a LaskaKit ji prodává za necelých sedm stovek 

 Interaktivní Mapa ČR , pořídíte ji za 698 Kč 

Interaktivní Mapa ČR, pořídíte ji za 698 Kč 

Desku pohání Wi-Fi SoC ESP32-WROOM-32 s čipem ESP32 od Espressifu

Desku pohání Wi-Fi SoC ESP32-WROOM-32 s čipem ESP32 od Espressifu

Desku můžeme programovat a napájet pomocí USB-C, nechybí totiž UART/USB převodník CH9102F

Desku můžeme programovat a napájet pomocí USB-C, nechybí totiž UART/USB převodník CH9102F

Plošky se signály modulu ESP32-WROOM pro připájení dalších periferií

Plošky se signály modulu ESP32-WROOM pro připájení dalších periferií

Alternativní napájecí piny pro 5V/3A zdroj

Alternativní napájecí piny pro 5V/3A zdroj

Webová aplikace ČHMÚ s daty ze srážkových radarů pro 14. květen, 12:20 UTC

Webová aplikace ČHMÚ s daty ze srážkových radarů pro 14. květen, 12:20 UTC

Zvolená licence v podstatě umožňuje jen převzít obrázek radaru v jeho původní podobě, ale nesmíte jej nijak zpracovávat – zveřejňovat odvozená díla

Zvolená licence v podstatě umožňuje jen převzít obrázek radaru v jeho původní podobě, ale nesmíte jej nijak zpracovávat – zveřejňovat odvozená díla

Transparentní radarová vrstva ve formátu PNG 

Transparentní radarová vrstva ve formátu PNG 

Stažený radarový snímek převedený na barevné znaky a zobrazený v příkazové řádce

Stažený radarový snímek převedený na barevné znaky a zobrazený v příkazové řádce

Pro srovnání jeho plnotučná předloha v PNG s rozměry 680 × 460 px

Pro srovnání jeho plnotučná předloha v PNG s rozměry 680 × 460 px

Databáze 72 měst na mapě Česka s indexy RGB LED a geografickými souřadnicemi

Databáze 72 měst na mapě Česka s indexy RGB LED a geografickými souřadnicemi

Zjištění úhlových rozměrů vrstvy s bitmapou radaru pro snadný přepočet na pixely

Zjištění úhlových rozměrů vrstvy s bitmapou radaru pro snadný přepočet na pixely

Pokud budeme kontrolovat jen souřadnice středu Plzně (černý puntík), blížící se déšť na předměstí neodhalíme. Vyplatí se proto projít třeba čtverec 5×5 okolních pixelů, do kterého nám už zasahují modré segmenty přicházejícího deště

Pokud budeme kontrolovat jen souřadnice středu Plzně (černý puntík), blížící se déšť na předměstí neodhalíme. Vyplatí se proto projít třeba čtverec 5×5 okolních pixelů, do kterého nám už zasahují modré segmenty přicházejícího deště

Náš program na závěr volitelně ukládá transformovaný radarový snímek do souboru s vyznačenými městy. Ty, ve kterých právě prší, jsou označené červeným rámem, který vyplňuje barva – intenzita – deště

Náš program na závěr volitelně ukládá transformovaný radarový snímek do souboru s vyznačenými městy. Ty, ve kterých právě prší, jsou označené červeným rámem, který vyplňuje barva – intenzita – deště

Zjistili jsme barvu deště ve městech

Zjistili jsme barvu deště ve městech

„OK“ je odpověď našeho HTTP serveru na mapě republiky, pokud vše proběhne v pořádku

„OK“ je odpověď našeho HTTP serveru na mapě republiky, pokud vše proběhne v pořádku

Pokud bude mapu skrze USB-C napájet počítač, můžeme se přímo ve vývojovém prostředí Arduino a jeho terminálu sériové linky podívat na stavové zprávy. Jak vidno program právě aktivuje desítky LED, prší totiž nad celou republikou

Pokud bude mapu skrze USB-C napájet počítač, můžeme se přímo ve vývojovém prostředí Arduino a jeho terminálu sériové linky podívat na stavové zprávy. Jak vidno program právě aktivuje desítky LED, prší totiž nad celou republikou

Fyzický srážkový radar se rozsvítil barvami ČHMÚ

Fyzický srážkový radar se rozsvítil barvami ČHMÚ

A takto vypadá skutečný radarový snímek

A takto vypadá skutečný radarový snímek

 Interaktivní Mapa ČR , pořídíte ji za 698 Kč 
Desku pohání Wi-Fi SoC ESP32-WROOM-32 s čipem ESP32 od Espressifu
Desku můžeme programovat a napájet pomocí USB-C, nechybí totiž UART/USB převodník CH9102F
Plošky se signály modulu ESP32-WROOM pro připájení dalších periferií
20
Fotogalerie

Naprogramovali jsme radarovou mapu Česka. Ukáže, kde právě prší a můžete si ji dát i na zeď

  • Každý ví, jak vypadá srážkový radar v mobilu nebo na webu ČHMÚ
  • My ho dnes zhmotníme na RGB LED mapě republiky
  • Má 72 měst a LaskaKit ji prodává za necelých sedm stovek

Pokud vám zakaboněná víkendová obloha narušila rodinné plány, rád bych se tu veřejně omluvil, za vše totiž může můj čtvrteční tweet. Stručně řečeno, potřeboval jsem otestovat funkčnost své fyzické radarové mapy srážek, a tak jsem si objednal přechod deštivé fronty napříč Českem.

Počkat, počkat, jaké fyzické radarové mapy? Inu, jedním ze žhavých témat na českém kutilském Twitteru v posledních několika měsících byla výroba desky plošných spojů s konturou České republiky a s desítkami adresovatelných RGB LED v místě krajských a okresních měst.

S prvním exemplářem se na GitHubu už zkraje roku pochlubila tvoje máma, no a před pár dny vyrukoval se svou vlastní a plně zkompletovanou mapou také tuzemský LaskaKit.

Video: Podívejte se, jak jsme naprogramovali fyzickou radarovou mapu srážek nad Českem krok za krokem

Jmenuje se jednoduše Interaktivní Mapa ČR, pořídíte ji za 698 Kč a její deska vyřezaná ve tvaru republiky nabízí:

  • Rozměry 265×150 mm
  • 72× adresovatelná RGB LED s řadičem WS2812B (PDF) s popisky měst
  • Řídící Wi-Fi SoC modul ESP32-WROOM
  • USB/UART čip CH9102F (PDF)
  • USB-C pro napájení a programování
  • Alternativní napájecí piny pro 5V/3A zdroj
  • Konektor uŠup/STEMMA/Qwiic pro připojení I²C periferií
  • Plošky se signály modulu ESP32-WROOM pro připájení dalších periferií
  • 4× montážní otvor s průměrem 2,5 mm

Suma sumárum, mapa je vlastně kompletní mikropočítač, který můžete po doinstalování podpory pro čipy ESP32 jednoduše naprogramovat třeba i v Arduinu.

Propojení se službou TMEP

Na GitHubu najdete i několik příkladů, které se spojí s českým webem TMEP, stáhnou údaje o teplotě vzduchu v jednotlivých okresech a poté rozsvítí LED každého z měst adekvátní barvou. TMEP je totiž skvělá meteorologická databáze s grafy pro kutily, kteří do ní mohou nahrávat svá data, aniž by museli na zelené louce budovat celý vlastní web.

Za projektem TMEP stojí Michal Ševčík aka MultiTricker a my si s jednou z kritických služeb tuzemské kutilscény pohráli v samostatném článku.

Srážková data pod svobodnou licencí, ale...

My si ale dnes vyzkoušíme něco jiného. Jelikož jezdím do práce na kole a v dnešní nejisté době skoro pokaždé zmoknu, hned jak se LaskaKit začal na svém Twitteru chlubit, na čem dělá, měl jsem jasno.

f9e3ab54-7380-4b44-bc48-13b2b94c49e7
Webová aplikace ČHMÚ s daty ze srážkových radarů pro 14. květen, 12:20 UTC

Města jsou napříč republikou rozprostřená relativně rovnoměrně, a tak mě napadlo, že bych se mohl pokusit promítnout na mapu aktuální data ze srážkových radarů ČHMÚ.

Abych byl konkrétní, jde mi o data z této webové aplikace, která jsou k dispozici pod svobodnou licencí Creative Commons BY-NC-ND. Podmínku Uveď autora (BY) jsem právě splnil na jedničku a stejně tak Neužívejte komerčně (NC), kód dnešního programu totiž najdete v zápatí článku a je volně k dispozici.

f2c3b801-a42c-407a-9ad6-1202e38e2a54
Zvolená licence v podstatě umožňuje jen převzít obrázek radaru v jeho původní podobě, ale nesmíte jej nijak zpracovávat – zveřejňovat odvozená díla

Poruším ale poněkud nesmyslný předpis Nezpracovávejte (ND), který v podstatě nařizuje, že nemohu se získanými daty provádět žádné výpočty a transformace. Pro potřeby článku tedy na pomoc povolávám principy novinářské licence a fair use. Vy ostatní a pro vlastní potřebu data směle využívejte. To je v naprostém souladu s licencí.

Webová adresa radarové vrstvy

Když se na mapu Českého hydrometeorologického ústavu dobře podíváte, jistě si všimnete, že animovaná aplikace používá několik vrstev se samostatnými rastry. Je tu zjednodušená podkladová mapa republiky, nad kterou se promítá transparentní bitmapa s vlastními srážkovými daty.

148733c3-b9bd-4b7e-aaf3-4749c11a4e85
Transparentní radarová vrstva ve formátu PNG

Srážková vrstva se generuje každých celých deset minut a její čas odpovídá tomu světovému UTC, respektive greenwichskému GMT. Takže pokud článek sepisuji v neděli 14. května 2023 a hodiny ukazují právě 14:22 SELČ (GMT+2), nejčerstvější bitmapu s radarovou vrstvou ve formátu PNG mohu stáhnout z webové adresy:

https://www.chmi.cz/files/portal/docs/meteo/rad/inca-cz/data/czrad-z_max3d_masked/pacz2gmaps3.z_max3d.20230514.1220.0.png

Všimněte si, že časový údaj dat je přímo součástí URL ve formátu RRRRMMDD.HHM0. Tedy rok 2023, měsíc 05, den 14, hodina 12 a minuta 20. Ještě jednou, jedná se o čas UTC/GMT, takže odpovídá 14:20 středoevropského letního času.

Python provede analýzu, Arduino rozsvítí LED

Dnešní projekt se bude skládat ze dvou částí. Tou první bude jednoduchý skript v Pythonu 3.x a tou druhou ještě jednodušší program pro Arduino s HTTP serverem, který poběží na naší mapě ČR.

Proč jsme vše rozdělili na dvě části? Protože Python běžící na velkém počítači, Raspberry Pi nebo třeba někde na internetu se nám postará o svižnou analýzu obrázku ve formátu PNG. Technicky by to ale zvládl i program pro Arduino, jen bychom potřebovali nějakou knihovnu pro dekódování PNG.

Takové samozřejmě existují, třeba PNGdec, nicméně kód by už byl docela komplexní, museli bychom se totiž postarat také o stažení a uložení souboru do flashové paměti nebo třeba někam na externí úložiště v podobě SD karty apod.

Requests stáhne obrázek a Pillow jej zpracuje

Já chci naopak na čipu ESP32 co nejjednodušší program, který jen bude skrze HTTP poslouchat, která města má rozsvítit, takže veškerou magii provedu právě v Pythonu a skriptu, který pak jen stačí periodicky spouštět.

V Pythonu je to vše opravdu otázka několika málo desítek řádků zdrojového kódu, do boje totiž povoláme dvojici oblíbených knihoven Requests a Pillow. Requests je dnes už v podstatě standard HTTP klientu pro Python a Pillow tím samým na poli základní knihovny pro práci s obrázky.

Na počítači s Pythonem obě knihovny doinstalujete třeba skrze balíčkový instalátor PIP, případně nahlédněte do dokumentace pod odkazy výše:

pip install pillow requests

Co nejstručnější úryvek kódu, který pomocí Requests stáhne poslední radarový obrázek a načte jej do knihovny Pillow jako objekt a ten následně opět uloží na disk počítače, by tedy mohl vypadat třeba takto:

import requests
from PIL import Image
from io import BytesIO

r = requests.get("https://www.chmi.cz/files/portal/docs/meteo/rad/inca-cz/data/czrad-z_max3d_masked/pacz2gmaps3.z_max3d.20230514.1220.0.png")
bitmapa = Image.open(BytesIO(r.content))
bitmapa.save("snimek.png")

Náš produkční kód bude složitější a samozřejmě ošetřený na případy, kdy nastane nějaká chyba – třeba zadáte špatnou URL obrázku ke stažení.

V kódu jsme použili ještě vestavěnou knihovnu io a objekt BytesIO, který nám převede odpověď webového serveru ČHMÚ na proud surových bajtů, jako bychom otevírali běžný binární soubor z disku na počítači.

Bitmapu převedeme na formát RGB

Bitmapa radarového snímku používá paletu indexovaných barev, což sice není na škodu, nicméně všichni si na první dobrou představíme spíše běžné červené, zelené a modré kanály ve formátu RGB. Obrázek proto v dalším kroku převedeme právě do RGB:

bitmapa = bitmapa.convert("RGB")

Projdeme bitmapu a vykreslíme ji pomocí barevných znaků přímo do příkazové řádky

Jelikož máme obrázek načtený v RAM, můžeme jej nyní projít řádek po řádku a získat hodnoty kanálu R, G a B:

for y in range(bitmapa.height):
    for x in range(bitmapa.width):
        r, g, b = bitmapa.getpixel((x, y))

Mnoho moderních příkazových řádek/shellů už dnes umí pomocí speciálních escape sekvencí ANSI pracovat s RGB barvou, takže zkusme vykreslit radarový snímek třeba rovnou do Terminálu Windows.

Budeme zobrazovat jen každý desátý pixel bitmapy – aby se nám celá vešla do terminálu – jako zdvojený textový znak ██ s barvou daného pixelu.

for y in range(0, bitmapa.height, 10):
    for x in range(0, bitmapa.width, 10):
        r, g, b = bitmapa.getpixel((x, y))
        print(f"\x1b[38;2;{r};{g};{b}m██\x1b[0m", end="")
    print("")

A takhle jako na obrázku níže bude vypadat výsledek. To není vůbec špatně, viďte? A přitom celý program v Pythonu zabral směšných 11 řádků kódu!

ef7fea87-88dd-4a50-8f13-e3ab1cd8fb6f
Stažený radarový snímek převedený na barevné znaky a zobrazený v příkazové řádce
af099f88-d1f0-4447-9247-e15b2129571c
Pro srovnání jeho plnotučná předloha v PNG s rozměry 680 × 460 px

Takže ještě jednou a tentokrát celý kód na jedné hromadě:

import requests
from PIL import Image
from io import BytesIO
r = requests.get("https://www.chmi.cz/files/portal/docs/meteo/rad/inca-cz/data/czrad-z_max3d_masked/pacz2gmaps3.z_max3d.20230514.1220.0.png")
bitmapa = Image.open(BytesIO(r.content))
bitmapa = bitmapa.convert("RGB")
for y in range(0, bitmapa.height, 10):
    for x in range(0, bitmapa.width, 10):
        r, g, b = bitmapa.getpixel((x, y))
        print(f"\x1b[38;2;{r};{g};{b}m██\x1b[0m", end="")
    print("")

Mesta.csv obsahuje souřadnice všech 72 obcí

Heuréka! Už umíme zjišťovat barvu pixelů radarového snímku na různých souřadnicích X;Y a také víme, že Česko se v obrázku nachází uprostřed, kde jakákoliv nenulová barva představuje déšť dle stupnice od modré přes zelenou, žlutou, červenou až po bílou pro naprostou spoušť.

Nulová RGB barva (0, 0, 0) ve skutečnosti nepředstavuje černou, ale průhlednou, PNG je totiž transparentní.

ef43f064-2bbd-42bd-928e-353588dceb20
Databáze 72 měst na mapě Česka s indexy RGB LED a geografickými souřadnicemi

Takže co dál? Nyní bych si mohl podle mapky na webu ČHMÚ zaznamenat hrubé X;Y souřadnice měst, která jsou i na desce od LaskaKitu, a poté zjišťovat barevný odstín jen v těchto místech.

Já jsem ovšem lenoch, který ručně nic měřit nebude, a tak jsem raději využil otevřených dat ČÚZK a stáhl si seznam a geografické souřadnice všech obcí v Česku, ze kterých jsem poté pouze křížově vytáhl oněch 72 měst na tištěné desce. Jejich názvy jsou na GitHubu projektu.

Výsledkem je textový soubor mesta.csv, kde má každý řádek formát:

ID;název;zem. šířka; zem. délka

Takže třeba hned první záznam pro Děčín:

0;Děčín;50.772656;14.212861

ID je v tomto případě index adresovatelné RGB LED na mapě od LaskaKitu, takže kdybych chtěl rozsvítit světýlko Děčína, vím, že je hned první v pořadí.

Přepočítáme GPS souřadnice na pixelové

Zjistil jsem si zeměpisné souřadnice krajních bodů radarové vrstvy, která má fixní rozměry 680×460 pixelů, a tak mezi nimi nyní mohu jednoduše přepočítávat stupňové a pixelové vzdálenosti:

  • Levý horní roh: 52,1670717° s.š., 11,2673442° v.d.
  • Pravý dolní roh: 48,1° s.š., 20,7703153° v.d.
  • Pixelové rozměry: 680 × 460 px
  • Stupňové rozměry: 9.5029711° z.d. × 4.0670717° z.š
  • 1 vertikální pixel: je roven 0,008841460217 stupňům zeměpisné šířky
  • 1 horizontální pixel: je roven 0,0139749575 stupňům zeměpisné délky

Primitivní přepočty samozřejmě nemají kartografickou přesnost, ale vzhledem k náhledovému měřítku je to úplně jedno. Odchylky budou nejvýše v jednotkách pixelů a náš kód bude alespoň maximálně jednoduchý.

3a30c636-d9b2-451d-b1c7-69748130de7c
Zjištění úhlových rozměrů vrstvy s bitmapou radaru pro snadný přepočet na pixely

Máme-li tedy seznam měst představující RGB LED na desce od LaskaKitu a jejich geografické souřadnice, můžeme jedno po druhém projít ve smyčce a podle údajů výše je přepočítat na pixelové souřadnice X;Y radarového snímku.

Pokud bude mít pixel RGB hodnotu vyšší než 0, 0, 0, víme, že v daném městě asi právě prší. Jak moc, už záleží na samotné barvě.

Zjišťovat barvu jediného pixelu, nebo plochy?

Zatímco geografické souřadnice představují jen jeden bod na mapě, plocha města je mnohem větší, a tak bychom mohli při kontrole, jestli v něm prší, používat nějaký širší záběr.

dfdfbedc-700a-42c8-bdaf-66b9f6a4250d
Pokud budeme kontrolovat jen souřadnice středu Plzně (černý puntík), blížící se déšť na předměstí neodhalíme. Vyplatí se proto projít třeba čtverec 5×5 okolních pixelů, do kterého nám už zasahují modré segmenty přicházejícího deště

Namísto zjištění barvy na souřadnicích X;Y bychom mohli třeba projít ve smyčce celý čtverec několika okolních pixelů a jako finální hodnotu použít buď aritmetický průměr zjištěných kanálů R, G a B, nebo naopak třeba tu nejvyšší hodnotu pro hledání a umocnění extrémů.

Pokud objevíme barvu, uložíme ji do pole JSON

My to ale nebudeme komplikovat, takže pokud na souřadnicích (středu) města objevíme barvu deště, index LED města a hodnoty kanálů RGB uložíme do dalšího pole ve formě struktury:

{
    "id": id,
    "r": r, 
    "g": g, 
    "b": b
}

Jakmile projdeme všechna města a zjistíme, že seznam těch, ve kterých prší, má nenulovou velikost, pomocí vestavěné knihovny json a její funkce dumps převedeme celý seznam se strukturami do prostého textu:

mesta_json = json.dumps(mesta_s_destem)

Kdyby pršelo jen v Děčíně, který, jak už víme, okupuje LED s indexem 0, pak by JSON vypadal třeba takto:

[
    {
        "id":0, 
        "r":0, 
        "g":108, 
        "b":192
    }
]

Při přechodu fronty přes celé území se nám naopak může JSON roztáhnout na několik tisíc znaků. Je třeba s tím počítat, čip ESP32 jej totiž bude později načítat do své vyhrazené RAM a musí se tam vše vejít. Naštěstí ji má dostatečně velikou.

0ef6e30a-9eb9-45b1-a01d-834e6ceb6ff3
Náš program na závěr volitelně ukládá transformovaný radarový snímek do souboru s vyznačenými městy. Ty, ve kterých právě prší, jsou označené červeným rámečkem, který vyplňuje barva

JSON konečně odešleme do mapy k zobrazení

Jelikož jsme si v úvodu řekli, že přímo v mapě od LaskaKitu běží jednoduchý HTTP server, jenž očekává seznam LED, které má rozsvítit kýženou barvou, pošleme mu právě tento vygenerovaný JSON pomocí protokolu HTTP POST – podobně jako když přes web posíláme třeba obsah formuláře.

5d5a48f9-b7fd-43ba-9785-dbd91de59706
Zjistili jsme barvu deště ve městech

Použijeme k tomu opět knihovnu Requests. Pokud by deska s mapou po přihlášení k místní Wi-Fi získala LAN IP 192.168.1.100, na které bude poslouchat i jednoduchý HTTP server přímo na čipu ESP32, pak by mohl vypadat úryvek kódu takto:

formular = {"mesta": mesta_json}
r = requests.post(f"http://192.16.1.100/", data=formular)

Na Arduinu běží HTTP server a dekodér JSON

Webový server na straně čipu ESP32 bude jen tiše čekat, dokud nedorazí od nějakého klientu HTTP dotaz na kořenový adresář /, no a pokud ano, zeptáme se, jestli obsahuje argument mesta s naším JSONem.

04277d51-3622-4482-adf7-4c7f3b2b7edd
„OK“ je odpověď našeho HTTP serveru na mapě republiky, pokud vše proběhne v pořádku

Bude-li tomu tak, pomocí dnes už také téměř standardní knihovny pro Arduino ArduinoJson dekódujeme pole s městy, ve kterých prší, a na jednotlivých RGB dle jejich indexu nastavíme adekvátní RGB barvu.

RGB LED rozsvítí knihovna Adafruit NeoPixel

V úvodu jsme si řekli, že deska s mapou Česka je vyzbrojená 72 adresovatelnými RGB LED s řadičem WS2812B. Ten je poměrně běžný, a tak je k dispozici nespočet knihoven, které spínání LED výrazně zjednoduší.

db1686cf-1253-4453-a131-eed17ed79e57
Pokud bude mapu skrze USB-C napájet počítač, můžeme se přímo ve vývojovém prostředí Arduino a jeho terminálu sériové linky podívat na stavové zprávy. Jak vidno program právě aktivuje desítky LED, prší totiž nad celou republikou

LaskaKit v příkladech na GitHubu používá knihovnu Freenove, já ale sáhnu po univerzálnější a provařenější NeoPixel od Adafruitu. Rozsvícení konkrétní LED je pak otázkou několika málo řádků kódu.

V úplném úvodu si vytvoříme objekt pixely představující instanci třídy Adafruit_NeopPixel, pomocí kterého budeme ovládat všech 72 LED:

Adafruit_NeoPixel pixely(72, 25, NEO_GRB + NEO_KHZ800);

První parametr 72 představuje počet LED, druhá hodnota 25 pak pin GPIO čipu ESP32, na který jsou všechny LED připojené v sérii za sebou. Poslední konfigurační parametr představuje formát kódování barvy a rychlost komunikace.

1ee69d23-55fa-4afa-a4d7-ab55a3e85ab6
Fyzický srážkový radar se rozsvítil barvami ČHMÚ
f2c7012c-11ee-44d9-99f2-808d12564650
A takto vypadá skutečný radarový snímek

V hlavní funkci Arduina setup poté knihovnu nakonfigurujeme. Nejprve nastavíme osmibitový jas na nízkou hodnotu 5, ať vyniknou méně kontrastní odstíny a mapa nespaluje příliš elektřiny.

Zároveň všechna světýlka uvedeme do zhasnutého stavu a všechny změny potvrdíme metodou .show(). Do té doby totiž jinak veškerá práce probíhá jen v mezipaměti.

pixely.begin();
pixely.setBrightness(5);
pixely.clear();
pixely.show();

Kdybychom nyní chtěli rozsvítit RGB LED města Děčín (index 0) sytou modrou barvou (R=0, G=0, B=255), stačí zavolat:

pixely.setPixelColor(0, pixely.Color(0, 0, 255));
pixely.show();

Stejné instrukce budeme volat i při procházení seznamu měst ve formátu JSON, který do ESP32 pošleme z Pythonu. Celé to zabere jen okamžik a deska ve tvaru České republiky se poté rozsvítí barvami srážkového radaru Českého hydrometeorologického ústavu.

Zdrojové kódy dnešního projektu

A to je vlastně vše. Na závěr nesmějí chybět kompletní a komentované zdrojové kódy. Nejprve kód programu ledradar.py v Pythonu a poté programu ledmapa.ino pro Arduino. Oba soubory a samozřejmě také databázi se souřadnicemi měst mesta.csv najdete na GitHubu našeho seriálu o programování elektroniky.

Ledradar.py jako první argument skriptu požaduje IP adresu/URL RGB LED mapy. Pokud by tedy od Wi-Fi routeru získala třeba IP adresu 192.168.1.100, pak nastavíme aktuální radarový snímek povelem:

python ledradar.py 192.168.1.100

Ledradar.py

from PIL import Image
from PIL import ImageDraw
import requests
from io import BytesIO
import json
from datetime import datetime, timedelta
import sys
from time import sleep

# -----------------------------------------------------------------------------
# GLOBALNI PROMENNE

logovani = True # Budeme vypisovat informace o běhu programu
kresleni = True # Uložíme snímek s radarem ve formátu radar_a_mesta_YYYYMMDD.HHM0.png
odesilani = True # Budeme odesílat data do LaskaKit mapy ČR

laskakit_mapa_url = sys.argv[1] # LAN IP/URL LaskaKit mapy ČR (bez http://) 

# Pracujeme v souřadnicovém systému WGS-84
# Abychom dokázali přepočítat stupně zeměpisné šířky a délky na pixely,
# musíme znát souřadnice levého horního a pravého dolního okraje radarového snímku ČHMÚ
# LEVÝ HORNÍ ROH
lon0 = 11.2673442
lat0 = 52.1670717
# PRAVÝ DOLNÍ ROH
lon1 = 20.7703153
lat1 = 48.1

# -----------------------------------------------------------------------------

# Alias pro print, který bude psát jen v případě,
# že má globální proměnná logovani hodnotu True
def printl(txt):
    if logovani:
        print(txt, flush=True)

# Funkce pro stažení bitmapy s radarovými daty z URL adresy:
# https://www.chmi.cz/files/portal/docs/meteo/rad/inca-cz/data/czrad-z_max3d_masked/pacz2gmaps3.z_max3d.{datum_txt}.0.png
# datum_txt musí být ve formátu UTC YYYYMMDD.HHM0 (ČHMÚ zveřejňuje snímky každých celých 10 minut)
# Pokud URL není validní (obrázek ještě neexistuje),
# pokusím se stáhnout bitmapu s o deset minut starší časovou značkou
# Počet opakování stanovuje proměnná pokusy 
def stahni_radar(datum=None, pokusy=5):
    if datum == None:
        datum = datetime.utcnow()

    while pokusy > 0:
        datum_txt = datum.strftime("%Y%m%d.%H%M")[:-1] + "0"
        url = f"https://www.chmi.cz/files/portal/docs/meteo/rad/inca-cz/data/czrad-z_max3d_masked/pacz2gmaps3.z_max3d.{datum_txt}.0.png"
        printl(f"Stahuji soubor: {url}")
        r = requests.get(url)
        if r.status_code != 200:
            printl(f"HTTP {r.status_code}: Nemohu stáhnout soubor")
            printl("Pokusím se stáhnout o 10 minut starší soubor")
            datum -= timedelta(minutes=10)
            pokusy -= 1
            sleep(.5)
        else:
            return True, r.content, datum_txt
    return False, None, datum_txt 

# Funkce pro obarvení textu v terminálu pomocí RGB
# Použijeme pro nápovědu, jakou barvu mají pixely radarových dat v daném městě
# Záleží na podpoře v příkazové řádce/shellu
# We Windows Terminalu a v současných linuxových grafických shellech to zpravidla funguje 
def rgb_text(r,g,b, text):
    return f"\x1b[38;2;{r};{g};{b}m{text}\x1b[0m"

# Začátek běhu programu
if __name__ == "__main__":
    printl("*** RGB LED Srážkový radar ***\n")
    # Rozměry bitmapy ve stupních
    sirka_stupne = lon1 - lon0
    vyska_stupne = lat0 - lat1

    # Pokusím se stáhnout bitmapu s radarovými daty
    # Pokud se to podaří, ok = True, bajty = HTTP data odpovědi (obrázku), txt_datum = YYYYMMDD.HHM0 staženého snímku
    ok, bajty, txt_datum = stahni_radar()
    if not ok:
        printl("Nepodařilo se stáhnout radarová data, končím :-(")
    else:
        # Z HTTP dat vytvoříme objekt bitmapy ve formátu PIL/Pillow
        try:
            bitmapa = Image.open(BytesIO(bajty))
        # Pokud se to nepodaří, ukončíme program s chybovým kódem
        except:
            printl("Nepodařilo se načíst bitmapu srážkového radaru")
            sys.exit(1)

        # Původní obrázek používá indexovanou paletu barev. To se sice může hodit,
        # pro jendoduchost příkladu ale převedeme snímek na plnotučné RGB
        printl("Převádím snímek na RGB... ")
        bitmapa = bitmapa.convert("RGB")
        if kresleni:
            platno = ImageDraw.Draw(bitmapa)

        # Z pixelového rozměru bitmapy spočítáme stupňovou velikost vertikálního
        # a horizontálního pixelu pro další přepočty mezi stupni a pixely
        velikost_lat_pixelu = vyska_stupne / bitmapa.height
        velikost_lon_pixelu = sirka_stupne / bitmapa.width

        printl(f"Šířka obrázku: {bitmapa.width} px ({sirka_stupne} stupňů zeměpisné délky)")
        printl(f"Výška obrázku: {bitmapa.height} px ({vyska_stupne} stupňů zeměpisné šířky)")
        printl(f"1 vertikální pixel je roven {velikost_lat_pixelu} stupňům zeměpisné šířky")
        printl(f"1 horizontální pixel je roven {velikost_lon_pixelu} stupňům zeměpisné délky")

        # V souboru mesta.csv máme po řádcích všechny obce na LaskaKit mapě
        # Řádek má formát: ID;název;zem. šířka;zem. délka
        # ID představuje pořadí RGB LED na LaskaKit mapě ČR
        mesta_s_destem = []
        printl("\nNačítám databázi měst... ")
        with open("mesta.csv", "r") as fi:
            mesta = fi.readlines()
            printl("Analyzuji, jestli v nich prší...")
            printl("-" * 80)
            # Projdeme město po po městě v seznamu
            for mesto in mesta:
                bunky = mesto.split(";")
                if len(bunky) == 4:
                    idx = bunky[0]
                    nazev = bunky[1]
                    lat = float(bunky[2])
                    lon = float(bunky[3])
                    # Spočítáme pixelové souřadnice města na radarovém snímku
                    x = int((lon - lon0) / velikost_lon_pixelu)
                    y = int((lat0 - lat) / velikost_lat_pixelu)
                    # Zjistíme RGB na dané souřadnici, tedy případnou barvu deště
                    r,g,b = bitmapa.getpixel((x, y))
                    # Pokud je v daném místě na radarovém snímnku nenulová barva, asi v něm prší
                    # Intenzitu deště určí konkrétní barva v rozsahu od světle modré přes zelenou, rudou až bílou
                    # Právě zde bychom tedy mohli detekovat i sílu deště, pro jednoduchost ukázky si ale vystačíme s prostou barvou 
                    if r+g+b > 0:
                        # Pokud jsme na začátku programu aktivovali kreslení,
                        # na plátno nakreslíme čtvereček s rozměry 10×10 px představující město
                        # Čtvereček bude mít barvu deště a červený obrys
                        if kresleni:
                            platno.rectangle((x-5, y-5, x+5, y+5), fill=(r, g, b), outline=(255, 0, 0))
                        # Pokud je aktivní logování, vypíšeme barevný text s údajem, že v daném městě prší,
                        # a přidáme město na seznam jako strukturu {"id":id, "r":r, "g":g, "b":b}  
                        printl(f"💦  Ve městě {nazev} ({idx}) asi právě prší {rgb_text(r,g,b, f'(R={r} G={g} B={b})')}")
                        mesta_s_destem.append({"id": idx, "r": r, "g": g, "b": b})
                    else:
                            # Pokud v daném městě neprší, nakreslíme v jeho souřadnicích prázdný čtvereček s bílým obrysem
                            if kresleni:
                                platno.rectangle((x-5, y-5, x+5, y+5), fill=(0, 0, 0), outline=(255, 255, 255))
        
        # Prošli jsme všechna města, takže se podíváme,
        # jestli máme v seznamu nějaká, ve kterých prší
        if len(mesta_s_destem) > 0:
            # Pokud jsme aktivovali odesílání dat do LaskaKit mapy ČR,
            # uložíme seznam měst, ve kterých pršelo, jako JSON pole struktur
            # a tento JSON poté skrze HTTP POST formulář s názvem proměnné "mesta"
            # odešleme do LaskaKit mapy ČR, na které běží jednoduchý HTTP server
            if odesilani == True:
                printl("\nPosílám JSON s městy do LaskaKit mapy ČR...")
                form_data = {"mesta": json.dumps(mesta_s_destem)}
                r = requests.post(f"http://{laskakit_mapa_url}/", data=form_data)
                if r.status_code == 200:
                    printl(r.text)
                else:
                    printl(f"HTTP {r.status_code}: Nemohu se spojit s LaskaKit mapou ČR na URL http://{laskakit_mapa_url}/")
        else:
            printl("Vypadá to, že v žádném městě neprší!")

        # Pokud je aktivní kreslení, na úplný závěr
        # uložíme PNG bitmapu radarového snímku s vyznačenými městy
        # do souboru radar_a_mesta_YYYYMMDD.HHM0.png
        if kresleni:
            bitmapa.save(f"radar_a_mesta_{txt_datum}.png")

Ledmapa.ino

#include <WiFi.h>
#include <WebServer.h>
#include <Adafruit_NeoPixel.h> // https://github.com/adafruit/Adafruit_NeoPixel
#include <ArduinoJson.h> // https://arduinojson.org/

// Nazev a heslo Wi-Fi
const char *ssid = "Nazev 2.4GHz Wi-Fi site";
const char *heslo = "Heslo Wi-Fi site";

// Objekt pro ovladani adresovatelnych RGB LED
// Je jich 72 a jsou v serii pripojene na GPIO pin 25
Adafruit_NeoPixel pixely(72, 25, NEO_GRB + NEO_KHZ800);
// HTTP server bezici na standardnim TCP portu 80
WebServer server(80);
// Pamet pro JSON s povely
// Alokujeme pro nej 10 000 B, coz je fakt hodne,
// ale melo by to stacit i pro JSON, ktery bude 
// obsahovat instrukce pro vsech 72 RGB LED
// Mohli bychom JSON zjednodusit a usetrit bajty,
// anebo misto nej pouzit jiny format dat (BSON, CBOR...)
StaticJsonDocument<10000> doc;

// Tuto funkci HTTP server zavola v pripade HTTP GET/POST pozoadavku na korenovou cestu /
void httpDotaz(void) {
  // Pokud HTTP data obsahuji parametr mesta
  // predame jeho obsah JSON dekoderu
  if (server.hasArg("mesta")) {
    DeserializationError e = deserializeJson(doc, server.arg("mesta"));
    if (e) {
      if (e == DeserializationError::InvalidInput) {
        server.send(200, "text/plain", "CHYBA\nSpatny format JSON");
        Serial.print("Spatny format JSON");
      } else if (e == DeserializationError::NoMemory) {
        server.send(200, "text/plain", "CHYBA\nMalo pameti RAM pro JSON. Navys ji!");
        Serial.print("Malo pameti RAM pro JSON. Navys ji!");
      }
      else{
        server.send(200, "text/plain", "CHYBA\nNepodarilo se mi dekodovat jSON");
        Serial.print("Nepodarilo se mi dekodovat jSON");
      }
    }
    // Pokud se nam podarilo dekodovat JSON,
    // zhasneme vsechny LED na mape a rozsvitime korektni barvou jen ty,
    // ktere jsou v JSON poli
    else {
      server.send(200, "text/plain", "OK");
      pixely.clear();
      JsonArray mesta = doc.as<JsonArray>();
      for (JsonObject mesto : mesta) {
        int id = mesto["id"];
        int r = mesto["r"];
        int g = mesto["g"];
        int b = mesto["b"];
        Serial.printf("Rozsvecuji mesto %d barvou R=%d G=%d B=%d\r\n", id, r, g, b);
        pixely.setPixelColor(id, pixely.Color(r, g, b));
      }
      // Teprve ted vyrobime signal na GPIO pinu 25,
      // ktery nastavi svetlo na jednotlivych LED
      pixely.show();
    }
  }
  // Pokud jsme do mapy poslali jen HTTP GET/POST parametr smazat, mapa zhasne
  else if (server.hasArg("smazat")) {
    server.send(200, "text/plain", "OK");
    pixely.clear();
    pixely.show();
  }
  // Ve vsech ostatnich pripadech odpovime chybovym hlasenim
  else {
    server.send(200, "text/plain", "CHYBA\nNeznamy prikaz");
  }
}

// Hlavni funkce setup se zpracuje hned po startu cipu ESP32
void setup() {
  // Nastartujeme seriovou linku rychlosti 115200 b/s
  Serial.begin(115200);
  // Pripojime se k Wi-Fi a pote vypiseme do seriove linky IP adresu
  WiFi.disconnect();
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, heslo);
  Serial.printf("Pripojuji se k %s ", ssid);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf(" OK\nIP: %s\r\n", WiFi.localIP().toString());
  // Pri HTTP pozadavku / zavolame funkci httpDotaz
  server.on("/", httpDotaz);
  // Aktivujeme server
  server.begin();
  // Nakonfigurujeme adresovatelene LED do vychozi zhasnute pozice
  // Nastavime 8bit jas na hodnotu 5
  // Nebude svitit zbytecne moc a vyniknou mene kontrastni barvy
  pixely.begin();
  pixely.setBrightness(5);
  pixely.clear();
  pixely.show();
}

// Smycka loop se opakuje stale dokola
// a nastartuje se po zpracovani funkce setup
void loop() {
  // Vyridime pripadne TCP spojeni klientu se serverem
  server.handleClient();
  // Pockame 2 ms (prenechame CPU pro ostatni ulohy na pozadi) a opakujeme
  delay(2);
}

Určitě si přečtěte

Články odjinud