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.

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.

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.

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!

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
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í.

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ý.

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.

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.

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.

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.

„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ší.

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.

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

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);
}