Před pár týdny jsme si v našem seriálu o programování elektroniky napsali jednoduchý skript v Pythonu, který stáhnul z webu ČHMÚ aktuální snímek srážkového radaru a pokusil se jej co nejvěrněji zobrazit na RGB LED mapě České republiky od LaskaKitu.
Byl to oříšek, protože mapa disponuje jen zhruba sedmdesátkou světýlek reprezentující jednotlivé česká města, a tak se mnozí ptali, jak by to asi vypadalo, kdybychom podobné informace zobrazili sice na pixelovém/bodovém displeji, ale zároveň takovém, který stejně jako naše mapa Česka může svítit celý den.
Podívejte se na video barevného e-inku s meteorologickými mapami:
Sedmibarevný e-ink displej
Něco takového jsme si museli také vyzkoušet, a tak jsme si krátce po zveřejnění článku objednali sedmibarevný e-ink Good Display GDEY073D46 s rozlišením 800×480 pixelů a úhlopříčkou 7,3 palců. Dokáže zobrazit černou, bílou, červenou, zelenou, modrou, oranžovou a žlutou. V nabídce jej má opět český LaskaKit, zájemci nicméně musí v dnešní složité době rozbít celý chlív prasátek, podobná legrace totiž přijde na 2038 korun.


Sedmipalcový a sedmibarevný e-inkový displej Good Display GDEY073D46
Technika barevného míchání pigmentu ACeP
Za tuto částku získáte displej, který míchá barvy zjednodušenou technikou ACeP/Gallery Palette. Už jsme si ji ukázali v článku věnovanému nejrůznějším typům elektronického inkoustu, a tak jen zopakuji, že ACeP míchá barvy CMY+W pomocí poloprůhledného pigmentu C (azurová/tyrkysová), M (purpurová) a Y (žlutá).

Princip elektroforetického míchání barev pomocí poloprůhledného pigmentu CMY a reflexního pigmentu W. Jednotlivé vrstvy se vertikálně skládají soustavou elektrických pulzů, na které každý z pigmentů reaguje trošku jinak. Displej proto při překreslování bliká všemi barvami, které se vynořují a zase zanořují, aby v řezu dosáhly kýžené výšky
Jelikož je pigment poloprůhledný, vytváří barevný filtr pro okolní světlo, které jím prochází a odráží se od reflexního bílého pigmentu W na spodní straně. Podle toho, jak elektroforeticky (působením elektrických pulzů) promícháme jednotlivé pigmenty a do jaké výše posuneme bílou reflexní vrstvu, takovou barvou pak bude daná buňka svítit.
Podívejte se na video pod lupou, jak se vykresluje sedmibarevný vzor na elektronickém inkoustu s technologií ACeP:
Acepové e-inky sice mohou díky barevnému pigmentu dosahovat poměrně věrných barev s vysokým kontrastem, který se už opravdu přibližuje kresbičce na papíře, nicméně cenou za toto mechanické přeskupování, kdy každý z pigmentů reaguje na jiný elektrický pulz, je pochopitelně čas.
Oproti monochromatickým e-inkům s povrchovým RGB filtrem (přeskupujeme jen černý a bílý pigment) zabere překreslování dle určení displeje až několik desítek sekund. V případě našeho modelu to dělá nějakých 32 sekund!

Stručná specifikace displeje
Jednoduchá komunikace skrze sběrnici SPI
Sedmipalcový a sedmibarevný GDEY073D46 je sice postavený na technologii ACeP/Gallery Palette, ale pracuje jen se sedmi odstíny – byť i ty jsou míchané ze základního pigmentu CMY+W.
Méně barev vyžaduje jednodušší generátor elektrických pulzů pro e-ink (říkáme tomu waveform) a jednodušší řídící obvod. Vzhledem k určení displeje to ničemu nevadí, GDEY073D46 má totiž sloužit jako chytrá cenovka v obchodě a jiný reklamní poutač, který se překreslí třeba jen jednou za den.

Schéma zapojení displeje s řídící deskou ESP32-LPKit WROVER-E skrze prototypovací adaptér, který je součástí balení
Díky tomu, že má displej vlastní obvod pro generování elektrických pulzů a také frame buffer – paměť pro stav jednotlivých pixelů, můžeme s ním podobně jako s ostatními v této kategorii komunikovat skrze standardní sběrnici SPI a několik pomocných signálu. SPI nechybí na žádné prototypovací stavebnici pro Arduino, takže propojení bude hračka.
Součástí balení je také jednoduchý adaptér pro snadné připojení k Arduinu a podobným deskám s 3,3V zdrojem. Ten nemusí být nikterak silný, protože během překreslování příkon poskočí jen na 52,8 mW elektrické energie a poté padne na 0,118 mW. Klíčovou výhodou elektromechanického e-inku je ale samozřejmě to, že obraz uchová i po kompletním odpojení napájení. A to klidně i celé roky.
Pro práci s e-inkem použijeme kód od výrobce
Good Display nabízí ke stažení ukázkový kód pro Arduino bez potřeby použití jakýchkoliv externích knihoven. Odpovídá tomu i jeho jednoduchost – skrze SPI a další signály se jen uvede displej do provozu a poté do jeho frame bufferu odešleme barevné hodnoty jednotlivých pixelů.
To nám bude bohatě stačit, samotnou bitmapu 800×480 pixelů totiž vytvoříme v Pythonu na počítači nebo někde na serveru. To už záleží na implementaci. V příkladu bude bitmapa na webovém serveru a my ji stáhneme na čipu ESP32 skrze Wi-Fi.
Sedmibarevná paleta a 4bitové kódování pixelů
Bitmapa, kterou skrze SPI pošleme do frame bufferu displeje, musí mít 4bitové barevné kódování s paletou ACeP a data se posílají řádek po řádku (480 řádků o délce 800 pixelů). Díky 4bitovému kódování nám jeden bajt okupují hned dva po sobě jdoucí pixely. Horní 4 bity zabere kód barvy prvního pixelu, dolní 4 bity kód toho následujícího.
Používá se tato převodní tabulka:
- Černá: 0x0
- Bílá: 0x1
- Zelená: 0x2
- Modrá: 0x3
- Červená: 0x4
- Žlutá: 0x5
- Oranžová: 0x6
Kdybychom tedy do frame bufferu e-inku odeslali bajt s hexadecimální hodnotou 0x12 (0001 0010), řadič nastaví na dané pozici první pixel na bílou a ten následující na zelenou. A kdybychom tam poslali bajt s hodnotou 0x65 (0110 0101), jeden se obarví oranžově a ten druhý žlutě.
(Nejen) kalkulačka Windows pomůže s bity
Pokud se ztrácíte v hexadecimálním a dvojkovém zápisu bitů a bajtů, v roce 2023 vám pomůže většina systémových kalkulaček včetně té, která je součástí novějších verzí Windows.


Hexadecimální hodnota 0x65 v kalkulačce Windows včetně režimu přepínání jednotlivých bitů
Nabízí totiž programátorský režim, kde vidíte dekadický, hexadecimální i dvojkový zápis a nechybí dokonce ani režim bitového zobrazení, kdy numerickou hodnotu nastavujete přepínáním jednotlivých bitů 8bit až 64bit čísla.
Teplotní, srážková a družicová mapa Česka
Tak, základní práci s e-inkem bychom měli (dále vás odkážu na komentovaný kód v závěru článku a na GitHub našeho seriálu), no a teď si pojďme ukázat, co se vlastně pokusíme zobrazit za bitmapu. V Pythonu pomocí knihovny Requests nejprve stáhneme trojici meteorologických map.
Budou to tyto snímky:
Zatímco snímek z radaru je k dispozici pod svobodnou licencí Creative Commons, Družicový snímek (byť také zdarma) má složitější licencování. K teplotní mapě In-počasí pak drží autorská práva InMeteo, s.r.o.



Bitmapy srážkového radaru, družicového snímku oblačnosti a teplotní mapy In-počasí.cz
Předpokládáme tedy výhradně demonstrační a nekomerční použití pro vlastní potřebu a snímky v tomto článku zveřejňujeme výhradně pro edukační účely a v dobré víře pod novinářskou licencí.
O práci s pixely se postará knihovna Pillow
Poté, co snímky stáhneme, je zpracujeme pomocí knihovny Pillow pro manipulaci s rastrovými daty v Pythonu. Už jsme s ní pracovali v našem předchozím experimentu s RGB LED mapou Česka, takže jen zopakuji, že snímky nejprve převedeme z indexovaných barev na RGB a adekvátně zmenšíme na rozměry 400×240 pixelů, protože každý z nich bude zabírat právě čtvrtinu displeje.

Teplotní mapa Česka In-počasí.cz přepočítaná na sedmibarevnou paletu e-inku
Teplotní snímek můžeme zobrazit v jeho původním stavu, má totiž barvy blízké paletě našeho displeje. Na snímek ze srážkového radaru nicméně nakreslíme ještě masku s konturami Česka, kterou jsme si vyrobili v libovolném grafickém editoru. Nejvíce práce bude s družicovým snímkem v odstínech šedi.
Družicový snímek s filtrem oblačnosti a vlastními barvami
Jelikož má e-ink pouze sedm barev a žádné odstíny šedi (leda bychom je simulovali technikou ditheringu), nemůžeme družicový záběr zobrazit v původní podobě. Byl by to v lepším případě jen mix bílé a černé plochy.

Družicový snímek s maskou republiky a přepočítaný na sedmibarevnou paletu e-inku
Namísto toho projdeme bitmapu družice a různé rozsahy odstínů šedé přemapujeme na odstíny naší palety. Díky tomu můžeme některé části spektra klidně odfiltrovat a na e-inku zobrazovat jen ty původně nejsvětlejší pixely představující nejsilnější oblačnost.
Radarový snímek používá 8bitový odstín (0: černá, 255: bílá), a tak:
- hodnoty 0-69 ignorujeme
- hodnoty 70-80 zobrazíme žlutě
- hodnoty 81-90 oranžově
- hodnoty 91-100 zeleně
- a nejsilnější oblačnost >100 konečně modře
V posledním segmentu zobrazíme datum a vlastní libovolné údaje
Do posledního segmentu matice napíšeme aktuální datum a údaje z meteorologické stanice. Použiji tu svoji redakční, tuto část kódu si proto nechám pro sebe a budu předpokládat, že si do bloku vypíšete nějaké vlastní informace z jiného zdroje.

Kompletní bitmapa v sedmibarevné paletě pro e-ink typu ACeP/Gallery Palette
Ta nejjednodušší bezztrátová komprese RLE
Celou dobu jsme modifikovali RGB pixely v bitmapě knihovny Pillow, teď ji ale musíme převést na čtyřbitové kódování a paletu ACeP. Uděláme to přímo v Pythonu a aby nebyl hotový binární soubor, který pak stáhneme na čipu ESP32, zbytečně velký, ještě jej bezztrátově zkomprimujeme technikou RLE.
Run-length encoding v té nejjednodušší možné podobě je prostý součet opakovaných hodnot a v sofistikovanější podobě jej používá třeba formát BMP. My ale zvolíme opravdu jen tu nejjednodušší variantu, tím pádem totiž nepotřebujeme pro kompresi žádnou další knihovnu a stejně tak dekomprese na ESP32 bude otázkou několika řádků kódu.
Nejjednodušší možná komprese RLE je opravdu primitivní. Mějme proud znaků (v našem případě bajtů) třeba v podobě:
A, A, A, A, B, B, C, C, C, C, D, D, D, D, A, A, A, A
Dělá to 18 bajtů, ale všimněte si, že některé znaky se více než dvakrát opakují. A tak bychom mohli zápis zkrátit tímto způsobem:
4, A, 2, B, 4, C, 4, D, 4, A
Původní zápis jsme smrskli na 10 bajtů jednoduše tak, že jsme opakující se bajty sečetli. První bajt tedy představuje vždy počet bajtu nadcházejícího. A jelikož má jejich počet stejnou délku jednoho bajtu, může dosahovat rozsahu 0-255. Kdyby po sobě následovalo 500 bajtů s hodnotou A, součty jednoduše rozdělíme na 255, A, 245, A.
RLE komprese funguje velmi dobře zejména tehdy, pokud jsou data málo pestrá (souvislé řady bajtů o stejné hodnotě). Když by byla data naopak velmi proměnlivá (extrémním případem je barevný šum), RLE-kódovaná bitmapa bude paradoxně větší než originál.
Nás se ale týká první případ a velikost původní nekomprimované bitmapy o velikosti 192 000 bajtů (800 × 480 / 2) se nám zmenší 2-4× podle toho, jak ji zaplní pestrobarevné pixely srážkového radaru družicové oblačnosti.
Samozřejmě bychom mohli bitmapu rovnou zakódovat třeba do JPEG a PNG, protože pro ESP32 existují knihovny jejich dekodérů, ale chceme docílit co nejjednoduššího kódu.
EXP32 stáhne, rozbalí a zobrazí bitmapu
Firmware pro Arduino a libovolnou desku s čipem ESP32 si tedy po spuštění stáhne bitmapu z našeho serveru (ať už kdesi v cloudu, nebo třeba na domácím Raspberry Pi), který ji bude průběžně generovat a aktualizovat, anebo jen na vyžádání (při požadavku na samotné stažení).

A takhle už vypadá výsledek na e-inkovém displeji
Už při stahování se bitmapa zároveň dekomprimuje a uloží do pole v RAM o velikosti 192000 bajtů. To už by mohl být problém, a tak jsem použil prototypovací desku s modulem ESP32-WROVER-E, který má k dispozici externí 8MB RAM (PSRAM).
Při nedostatku RAM na základních modulech ESP32 bychom mohli hned po RLE dekomprimaci posílat bajty skrze sběrnici SPI do frame bufferu e-inku – doslova streamovat bez dalšího frame bufferu v RAM na řídícím čipu. To je už na vás, stačí si kód mírně poupravit. Díky vlastnímu frame bufferu ale zase můžeme s bitmapou provádět nějaké další kulišárny přímo na čipu, pokud bychom to potřebovali.
Celý systém je navržený pro běh na baterii
Po vykreslení snímku na e-inkový displej mu dáme povel, aby přešel do hlubokého spánku (sníží se aktivita jeho řídícího obvodu), načež přejdeme do spánku i na hlavním čipu ESP32. Spánek bude trvat zhruba hodinu a poté se procesor ESP32 restartem probudí a celé kolečko začne znovu.
Systém je tedy připraven i pro velmi úsporný běh třeba na baterii nebo lithiový akumulátor. A jelikož jsme použili českou prototypovací desku ESP32-LPKit WROVER-E opět od LaskaKitu, která na bateriové napájení myslí a je vyzbrojená jak konektorem pro akumulátor, tak nabíjecím obvodem skrze USB, výsledkem by mohl být e-inkový rámeček třeba na zdi, anebo položený kdesi na stolku v obývacím pokoji.
Zdrojové kódy
Zdrojových kódů je tentokrát více a kompletní projekt včetně pomocných bitmap (masky republiky) a dalších dat najdete na GitHubu našeho seriálu o programování elektroniky. Zdrojové kódy ale pro formu doplníme i sem.
Generátor v Pythonu
Nejprve tedy generátor v Pythonu, který vytvoří kompletní bitmapu pro e-ink. Budeme předpokládat, že tento soubor poté bude dostupný na vašem vlastním HTTP serveru, odkud jej bude stahovat ESP32. Server si už musíte zajistit sami. Skript v Pythonu používá knihovny Requests, Pytz a Pillow, které si budete muset doinstalovat ručně. Viz odkazy v kódu.
# Knihovna Pillow pro práci s rastrovými daty
# https://pillow.readthedocs.io/en/stable/
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
# Knihovna Requests pro HTTP komunikaci
# https://requests.readthedocs.io/en/latest/
import requests
# Knihovna Pytz pro práci s časovými pásmy
# https://pytz.sourceforge.net/
import pytz
# Ostatní pomocné knihovny, kterou jsou součástí instalace Pythonu
from io import BytesIO
from datetime import datetime, timedelta
import sys
from time import sleep
import struct
# Pokud True, budu do výstupu psát logovací informace
logovani = True
# Absolutni cesta k souborum v pripade spousteni skriptu z jineho adresare
cesta = ""
# Sedmibarevná paleta pro displeje E-ink ACeP/Gallery Palette
# Použijeme ji pro vytvoření bitmapy pro náš 7,3" 800x480 e-ink displej GDEY073D46 https://www.good-display.com/product/442.html
paleta = [
[ 0, 0, 0], # CERNA, eink kod: 0x0
[255, 255, 255], # BILA, eink kod: 0x1
[ 0, 255, 0], # ZELENA, eink kod: 0x2
[ 0, 0, 255], # MODRA, eink kod: 0x3
[255, 0, 0], # CERVENA, eink kod: 0x4
[255, 255, 0], # ZLUTA, eink kod: 0x5
[255, 128, 0] # ORANZOVA, eink kod: 0x6
]
# Naivní funkce pro převod RGB odstínu na nejpodobnější/nejbližší barvu sedmibarevné palety
def prevedPixel(r, g, b):
nejmensi_rozdil = 100e6
nova_barva = 0
for i, barva in enumerate(paleta):
rozdil_r = r - barva[0]
rozdil_g = g - barva[1]
rozdil_b = b - barva[2]
rozdil = (rozdil_r**2) + (rozdil_g**2) + (rozdil_b**2)
if(rozdil < nejmensi_rozdil):
nejmensi_rozdil = rozdil
nova_barva = i
return nova_barva
# Funkce pro okamžité psaní do výstupu, pokud je povoleno logování
def printl(txt):
if logovani:
print(txt, flush=True)
# Funkce pro stažení teplotní heatmapy z webu In-pocasi.cz
# Teplotní snímky se generují každých celých třicet minut
# :00, :30 místního času (SEČ/SELČ)
def stahni_snimek_heatmapa(datum=None, pokusy=5):
if datum == None:
datum = datetime.now()
while pokusy > 0:
datum = datum.replace(minute=(datum.minute // 30) * 30)
datum_txt = datum.strftime("%H%M")
url = f"https://www.in-pocasi.cz/data/teplotni_mapy_cz_actual/t{datum_txt}.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 30 minut starší soubor")
datum -= timedelta(minutes=30)
pokusy -= 1
sleep(.5)
else:
return True, r.content, datum
return False, None, datum
# Funkce pro stažení radarového snímky z webu ČHMÚ
# Radarové snímky se generují každých celých deset minut
# :00, :10, :20, ... UTC času
def stahni_snimek_radar(datum=None, pokusy=5):
if datum == None:
datum = datetime.utcnow()
while pokusy > 0:
# Získáme UTC datum a čas ve formátu YYYYmmdd.HHMM pro posledních celých deset minut
datum = datum.replace(minute=(datum.minute // 10) * 10)
datum_txt = datum.strftime("%Y%m%d.%H%M")
url = f"https://www.chmi.cz/files/portal/docs/meteo/rad/inca-cz/data/czrad-z_max3d/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
return False, None, datum
# Funkce pro stažení družicového snímku z webu ČHMÚ
# Družicové snímky se generují každou celou čtvrthodinu
# :00, :15, :30, :45 UTC času
def stahni_snimek_druzice(datum=None, pokusy=5):
if datum == None:
datum = datetime.utcnow()
while pokusy > 0:
# Získáme UTC datum a čas ve formátu YYYYmmdd.HHMM pro poslední celou čtvrthodinu
datum = datum.replace(minute=(datum.minute // 15 * 15))
datum_txt = datum.strftime("%Y%m%d.%H%M")
url = f"https://www.chmi.cz/files/portal/docs/meteo/sat/msg_hrit/img-msgcz-1160x800-ir108/msgcz-1160x800.ir108.{datum_txt}.0.jpg"
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 15 minut starší soubor")
datum -= timedelta(minutes=15)
pokusy -= 1
sleep(.5)
else:
return True, r.content, datum
return False, None, datum
def rgb_text(r,g,b, text):
return f"\x1b[38;2;{r};{g};{b}m{text}\x1b[0m"
if __name__ == "__main__":
printl("*** Dashboard pro eink ***\n")
# Stáhneme snímky radaru, družice a teplotní mapy
# Snímky radaru ČHMÚ jsou k dispozici pod svobodnou licencí CC
# Snímky z družice jsou k dispozici pod licencí EUMETSAT (https://www.chmi.cz/files/portal/docs/meteo/sat/info/EUM_licence.html), používáme v dobré víře pro edukační účely/soukromé použití
# Snímky teplotní heatmapy stahujeme z webu In-pocasi.cz opět výhradně pro demonstrační a edukační účely a pro soukromé použití. Data náleží společnosti InMeteo, s.r.o.
ok_radar, bajty_radar, datum_radar = stahni_snimek_radar()
ok_heatmapa, bajty_heatmapa, datum_heatmapa = stahni_snimek_heatmapa()
ok_druzice, bajty_druzice, datum_druzice = stahni_snimek_druzice()
# Pokud se některé snímky nestáhly, ukončíme skript,
# protože nemáme kompletní data ke konstrukci dashboardu pro e-ink
if not ok_radar or not ok_heatmapa or not ok_druzice:
printl("Nepodařilo se stáhnout snimky, končím :-(")
# V opačném případě...
else:
try:
# Načteme stažené snímky
snimek_radar = Image.open(BytesIO(bajty_radar))
snimek_heatmapa = Image.open(BytesIO(bajty_heatmapa))
snimek_druzice = Image.open(BytesIO(bajty_druzice))
except:
printl("Nepodařilo se načíst bitmapy")
sys.exit(1)
# Některé stažené snímky jsou v indexovaných barvách,
# a tak všechny sjeednotíme převodem na formát RGB
printl("Převádím snímky na RGB...")
snimek_radar = snimek_radar.convert("RGB")
snimek_heatmapa = snimek_heatmapa.convert("RGB")
snimek_druzice = snimek_druzice.convert("RGB")
# Načtení předpřipraveného pokladu 800x480 px se statickými kresbami (ikony aj.)
printl("Nahrávám podkladovou bitmapu...")
podklad = Image.open(f"{cesta}podklad.png")
podklad.convert("RGB")
# Zmenšení stažených snímků a načtení černobílých masek republiky
printl("Zmenšuji snímky a načítám masky republiky...")
snimek_radar = snimek_radar.resize((400,240))
snimek_heatmapa = snimek_heatmapa.resize((400,240))
snimek_druzice = snimek_druzice.resize((400,240))
maska_cesko_druzice = Image.open(f"{cesta}maska_cesko_druzice.png")
maska_cesko_druzice = maska_cesko_druzice.convert("RGB")
maska_cesko_radar = Image.open(f"{cesta}maska_cesko_radar.png")
maska_cesko_radar = maska_cesko_radar.convert("RGB")
# ---------------------------------
# KRESBA BLOKU S TEPLOTNÍM SNÍMKEM
# ---------------------------------
printl("Kreslím heatmapu...")
podklad.paste(snimek_heatmapa, (0, 0))
# ---------------------------------
# KRESBA BLOKU S DRUŽICOVÝM SNÍMKEM
# ---------------------------------
printl("Kreslím družicový snímek v umělých barvách a s maskou republiky...")
# Protože budeme zjišťovat a modifikovat stav jednotlivých pixelů, projdeme je jeden po druhém
# Pomalý, ale jednoduchý/naivní postup
for y in range(snimek_druzice.height):
for x in range(snimek_druzice.width):
r,g,b = snimek_druzice.getpixel((x,y))
_r,_g,_b = maska_cesko_druzice.getpixel((x,y))
# Pokud je pixel masky černý, nakresli pixel masky
if _r == 0:
podklad.putpixel((x+400, y+240), (1, 1, 1))
# V opačném případě kreslíme oblačnost v umělých barvách od žluté po modrou
# Nemáme e-ink s odstíny šedi, takže nemůžeme použít originální barvy
# Tímto způsobem můžeme dofiltrovat nejslabší oblačnost a kreslit jen tu zajímavou
else:
if r >= 70 and r <= 80: # Nejslabsi mrak
podklad.putpixel((x+400, y+240), (255, 255, 0))
elif r > 80 and r <= 90: # Slaby mrak
podklad.putpixel((x+400, y+240), (255, 128, 0))
elif r > 90 and r <= 100: # Stredni mrak
podklad.putpixel((x+400, y+240), (0, 255, 0))
elif r > 100: # Silny mrak
podklad.putpixel((x+400, y+240), (0, 0, 255))
else:
podklad.putpixel((x+400, y+240), (255, 255, 255))
# --------------------------------
# KRESBA BLOKU S RADAROVÝM SNÍMKEM
# --------------------------------
printl("Kreslím radarový snímek a s maskou republiky...")
# Protože budeme zjišťovat a modifikovat stav jendotlivých pixelů, projdeme je jeden po druhém
# Pomalý, ale jednoduchý/naivní postup
for y in range(snimek_radar.height):
for x in range(snimek_radar.width):
r,g,b = snimek_radar.getpixel((x,y))
_r,_g,_b = maska_cesko_radar.getpixel((x,y))
# Pokud je pixel masky černý, nakresli pixel masky
if _r == 0:
podklad.putpixel((x, y+240), (1, 1, 1))
# V opačném případě vykreslíme radarová data
# Ignoroujeme hodnotu kanálu 0 (černá/transparentní/pozadí)
else:
if (r+g+b) > 0:
podklad.putpixel((x, y+240), (r, g, b))
printl("Kreslím popisky...")
platno = ImageDraw.Draw(podklad)
pismo_mapy = ImageFont.truetype(f"{cesta}BakbakOne-Regular.ttf", 20)
# Popisky teplotní mapy
platno.text((10,10), "TEPLOTA", font=pismo_mapy, fill="black")
platno.text((10,30), datum_heatmapa.strftime("%H:%M"), font=pismo_mapy, fill="black")
# Popisky radarové mapy
# Datum radarové mapy je v UTC, takže převedeme na časové pásmo Česka
casova_zona_cr = pytz.timezone("Europe/Prague")
datum_radar = datum_radar.replace(tzinfo=pytz.utc).astimezone(casova_zona_cr)
platno.text((10,250), "SRÁŽKY", font=pismo_mapy, fill="black")
platno.text((10,270), datum_radar.strftime("%H:%M"), font=pismo_mapy, fill="black")
# Popisky družicové mapy
# Datum družicového snímku je také v UTC, takže opět převedeme na čas Česka
datum_druzice = datum_druzice.replace(tzinfo=pytz.utc).astimezone(casova_zona_cr)
platno.text((410,250), "DRUŽICE", font=pismo_mapy, fill="black")
platno.text((410,270), datum_druzice.strftime("%H:%M"), font=pismo_mapy, fill="black")
# Nakreslení dat z meteostanice
printl("Zjišťuji stav redakční meteostanice...")
# ZDE SI DOPLŇTE SVŮJ VLASTNÍ KOD, JAK ZÍSKAT DATA Z VLASTNÍCH ZDROJŮ
teplota = "10.256"
vlhkost = "56"
svetlo = "25698"
baterie = "3.95"
printl("Kreslím údaje z meteostanice...")
pismo_meteo_datum = ImageFont.truetype(f"{cesta}BakbakOne-Regular.ttf", 80)
pismo_meteo_data = ImageFont.truetype(f"{cesta}BakbakOne-Regular.ttf", 35)
pismo_meteo_data_mensi = ImageFont.truetype(f"{cesta}BakbakOne-Regular.ttf", 25)
platno.text((480, 90), f"{teplota.replace('.',',')} °C", font=pismo_meteo_data, fill="black")
platno.text((670, 90), f"{vlhkost.replace('.',',')} %", font=pismo_meteo_data, fill="black")
platno.text((480, 177), f"{svetlo.replace('.',',')} lx", font=pismo_meteo_data_mensi, fill="black")
platno.text((670, 166), f"{baterie.replace('.',',')} V", font=pismo_meteo_data, fill="black")
dny_v_tydnu = ["Po", "Út", "St", "Čt", "Pá", "So", "Ne"]
datum = datetime.now()
platno.text((425, -20), f"{datum.strftime('%d.%m.')} {dny_v_tydnu[datum.weekday()]}", font=pismo_meteo_datum, fill="black")
# Ještě nakreslíme mřížku
platno.line([(400,0),(400,480)], fill="black", width=1)
platno.line([(0,240),(800,240)], fill="black", width=1)
# Převod bitmapy na sedmibarevnou paletu ACeP/Gallery Palette
printl("Převádím RGB na sedmibarevnou paletu E-ink ACeP/Gallery Palette...")
printl("Provádím základní bezztrátovou kompresi RLE...")
binarni = open(f"{cesta}dashboard_rle.bin", "wb")
bajty = []
for y in range(podklad.height):
for x in range((int(podklad.width/2))):
px1 = podklad.getpixel(((x * 2), y))
px2 = podklad.getpixel(((x * 2) + 1, y))
barva1 = prevedPixel(px1[0], px1[1], px1[2])
barva2 = prevedPixel(px2[0], px2[1], px2[2])
par = barva2 | (barva1 << 4)
bajty.append(par)
komprimovano = []
hodnota = bajty[0]
pocet = 0
# Bezztrátová komprese základním algoritmem RLE s osmibitovou délkou
# Naivní a pomalý přístup pro demonstraci a pochopení
for bajt in bajty:
if bajt != hodnota:
komprimovano.append(pocet)
komprimovano.append(hodnota)
hodnota = bajt
pocet = 1
else:
if pocet == 255:
komprimovano.append(pocet)
komprimovano.append(hodnota)
hodnota = bajt
pocet = 1
else:
pocet += 1
printl(f"Komprimováno: {len(komprimovano)} bajtů")
# Uložíme zkomprimovanou bitmapu do souboru
printl("Ukládám...")
for bajt in komprimovano:
binarni.write(struct.pack("B", bajt))
binarni.close()
Dále následují zdrojové kódy pro Arduino a čip ESP32. Předpokládáme, že máte doinstalovanou oficiální podporu pro programování těchto čipů. Žádné další knihovny potřebovat nebudete, vycházíme totiž z upraveného kódu přímo od výrobce displeje.
Hlavní program pro Arduino
#include <WiFi.h> // Pripojeni k Wi-Fi
#include <HTTPClient.h> // HTTP klient; stazeni bitmapy
#include "gdey073d46.h" // Hlavickovy soubor s funkcemi pro praci s e-inkem
// Nazev a heslo Wi-Fi
const char* ssid = "Nazev 2.4GHZ 802.1bgn Wi-Fi";
const char* heslo = "WPA heslo k Wi-Fi";
// URL bitmapy pro eink
// Soubor vytvori generator v Pythonu, musite jej ale spoustet na vlastnim serveru
String url = "http:// ???? /dashboard_rle.bin";
RTC_DATA_ATTR uint32_t pocitadlo_probuzeni = 0; // Pocitadlo probuzeni (vypisuje se pro kontrolu do seriove linky)
uint64_t prodleva_s = 3600; // Prodleva v sekundach mezi probuzenim hlavniho procesoru z hlubokeho spanku (1 hodina)
// Funkce pro stazeni a dekomprimaci bitmapy do RAM
uint32_t stahni_a_dekomprimuj() {
uint32_t stazeno = 0;
HTTPClient http;
http.setTimeout(10000); // Zvysimu timeout pro TCP spojeni na 10 sekund, pokud budeme bitmapu generovat pri HTTP pozadavku (muze byt velka prodleva, nez server zacne posilat data)
http.begin(url); // Vytvorime HTTP pozadavek s nasi URL
int kod = http.GET(); // Zaciname HTTP GET komunikaci
Serial.printf(" (HTTP kod: %d) ", kod);
if (kod == HTTP_CODE_OK) { // Pokud jsme navazali HTTP spojeni (odpoved HTTP OK)
uint32_t velikost_soubor = http.getSize(); // Velikost dat ke stazeni
Serial.printf(" (Ke stazeni: %d) ", velikost_soubor);
WiFiClient* stream = http.getStreamPtr(); // Otevreme stream na stahovana data
uint32_t dekomprimovane_bajty = 0;
while (http.connected() && (stazeno < velikost_soubor)) { // Pokud jsme spojeni se serverem a jeste jsme nestahli cely soubor
size_t k_dispozici = stream->available(); // Zjistime velikost aktualne stazenych dat ve streamovacim zasobniku
if (k_dispozici) { // Pokud tam neco je
uint8_t buffer[128];
size_t precteno = stream->readBytes(buffer, 128); // Precteme 128 bajtu do naseho vlastniho maleho zasobniku (je to rychlejsi, nez cist bajt po bajtu)
if (precteno) {
// RLE dekomprese davky v zasobniku
// !!! V nasem zasobniku musi byt sudy pocet bajtu, jinak mame problem !!!
for (size_t pozice = 0; pozice < precteno; pozice += 2) { // Precteme vzdy dva bajty z naseho zasobniku. Prvni bajt je pocet, druhy bajt hodnota
uint8_t pocet = buffer[pozice];
uint8_t hodnota = buffer[pozice + 1];
for (uint8_t pozice = 0; pozice < pocet; pozice++) bitmap_buffer[dekomprimovane_bajty++] = hodnota; // Vyplnime bitmapu davkou
}
stazeno += precteno; // Navysime pocitadlo stazenych bajtu
}
}
delay(1); // Drobna cekaci prodleva pro zvyseni stability prenosu. Lze s tim experimentovat
}
}
http.end(); // Ukonceni HTTP spojeni
return stazeno; // Vratime pocet stazenych bajtu pro kontrolu
}
// Hlavni funkce setup se spusti hned na zacatku
void setup() {
Serial.begin(115200); // Nastartujeme seriovou linku
delay(1000); // Jen umela prodleva, abychom stacili otevrit seriovy terminal, pokud chceme
// Konfigurace pomocnych signalu e-inku
pinMode(BUSY_Pin, INPUT);
pinMode(RES_Pin, OUTPUT);
pinMode(DC_Pin, OUTPUT);
pinMode(CS_Pin, OUTPUT);
// Nastartovani sbernice SPI
SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));
SPI.begin();
// Vytvoreni bloku pameti pro bitmapu
// Pozor, blok pameti se alokuje v externi PSRAM
// Pokud nemate desku s modulem ESP32-WROVER, je treba kosmeticky upravit podobu funkce bitmap_psram_init (nahradit ps_malloc za standardni malloc),
// anebo vetsi merou prepsat kod tak, aby se nevytvarel lokalni frame buffer v RAM pro bitmapu
Serial.printf("Alokuji %d bajtu v PSRAM pro 4bit bitmapu... ", BITMAP_SIZE);
bitmap_psram_init();
Serial.println("OK");
// Pripojeni k Wi-Fi
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(".");
}
// Vypiseme do seriove linky pro kontrolu LAN IP adresu obdrzenou od DHCP a hodnotu pocitadla probuzeni
Serial.printf(" OK\r\nIP: %s\r\nPocet probuzeni: %d\r\n", WiFi.localIP().toString(), pocitadlo_probuzeni++);
// Stahneme a dekomprimujeme bitmapu z naseho serveru
Serial.print("Stahuji a dekomprimuji... ");
uint32_t stazeno = stahni_a_dekomprimuj();
Serial.printf("%d B\r\n", stazeno);
Serial.println("Inicializuji e-ink...");
// Inicializujeme/probudime e-ink
eink_init();
// Posleme bitmapu do frame bufferu e-inku a vyvolame refresh/prekresleni
Serial.print("Kreslim bitmapu...");
eink_bitmapa();
// Prepneme e-ink do usporneho/vypnuteho rezimu
Serial.println("Vypinam e-ink...");
eink_spanek();
// Vypneme Wi-Fi
Serial.println("Odpojuji Wi-Fi...");
WiFi.mode(WIFI_OFF);
// Prepneme se do rezimu hlubokeho spanku
// Z nej se probudime za hodinu a cele kolecko se zopakuje
// Displej se tedy prekresluje jednou za hodinu
Serial.println("Prechazim do hlubokeho spanku...");
Serial.flush();
esp_sleep_enable_timer_wakeup(prodleva_s * 1000000ULL); // Registrace probuzeni timerem
esp_deep_sleep_start();
}
// Smycka loop je prazdna, nikdy se totiz nespusti
// Cip ESP32 se probuzeni z hlubokeho spanku resetuje
// a program se spusti od zacatku. Pocitadlo probuzeni prezije,
// protoze jsme jej atributem RTC_DATA_ATTR ulozili do specialni a omezene RAM,
// ktera je pod napetim i v hlubokem spanku a prezije reset
void loop() {
;
}
Hlavičkový soubor gdey073d46.h s funkcemi pro práci s displejem
#include <SPI.h>
// Ukazatel na pamet pro bitmapu
uint8_t* bitmap_buffer;
// Velikost pameti pro bitmapu
#define BITMAP_SIZE (800 * 480) / 2
// GPIO piny pro pomocne signaly displeje
#define BUSY_Pin 27
#define RES_Pin 26
#define DC_Pin 25
#define CS_Pin 5
// Makra pro nastaveni digitalnich stavu na pomocnych pinech
#define DIGITALNI_STAV_CS_0 digitalWrite(CS_Pin, LOW)
#define DIGITALNI_STAV_CS_1 digitalWrite(CS_Pin, HIGH)
#define DIGITALNI_STAV_DC_0 digitalWrite(DC_Pin, LOW)
#define DIGITALNI_STAV_DC_1 digitalWrite(DC_Pin, HIGH)
#define DIGITALNI_STAV_RST_0 digitalWrite(RES_Pin, LOW)
#define DIGITALNI_STAV_RST_1 digitalWrite(RES_Pin, HIGH)
#define BUSY_STAV digitalRead(BUSY_Pin)
// 4bit paleta barev, pokud bychom chteli menit pixely bitmapy za behu
// V nasem prikladu nicmene nepouzivame
#define black 0x00 /// 000
#define white 0x01 /// 001
#define green 0x02 /// 010
#define blue 0x03 /// 011
#define red 0x04 /// 100
#define yellow 0x05 /// 101
#define orange 0x06 /// 110
#define clean 0x07 /// 111
// Makra s ridicimi instrukcemi e-inku
#define PSR 0x00
#define PWRR 0x01
#define POF 0x02
#define POFS 0x03
#define PON 0x04
#define BTST1 0x05
#define BTST2 0x06
#define DSLP 0x07
#define BTST3 0x08
#define DTM 0x10
#define DRF 0x12
#define PLL 0x30
#define CDI 0x50
#define TCON 0x60
#define TRES 0x61
#define REV 0x70
#define VDCS 0x82
#define T_VDCS 0x84
#define PWS 0xE3
// Pouzite funkce
void eink_data(uint8_t prikaz);
void eink_cmd(uint8_t data);
void eink_init(void);
void eink_spanek(void);
void eink_cekej_na_vykresleni(void);
void eink_bitmapa(void);
void bitmap_psram_init(void);
// Vytvoreni pameti pro lokalni frame buffer
// Vytvarime ho v externi PSRAM SoC modulu ESP32-WROVER-E
// ps_malloc JE TREBA ZMENIT za malloc, POKUD POUZIVATE JINY CIP !!!!
// Anebo upravit kod takovym zpusobem, aby se pamet vubec nealokovala,
// pokud na ni neni v RAM misto. Treba tak, ze budou bajty odesilat do
// frame bufferu displeje uz pri stahovani. Viz funcke stahni_a_dekomprimuj
void bitmap_psram_init(void){
bitmap_buffer = (uint8_t*) ps_malloc(BITMAP_SIZE);
}
// Funkce pro odeslani prikazu do e-inku
void eink_cmd(uint8_t prikaz) {
DIGITALNI_STAV_CS_0;
DIGITALNI_STAV_DC_0;
SPI.transfer(prikaz);
DIGITALNI_STAV_CS_1;
}
// Funkce pro odeslani dat do e-inku
void eink_data(uint8_t data) {
DIGITALNI_STAV_CS_0;
DIGITALNI_STAV_DC_1;
SPI.transfer(data);
DIGITALNI_STAV_CS_1;
}
// Funkce pro inicializaci displeje
void eink_init(void) {
DIGITALNI_STAV_RST_0; // Reset
delay(10); // Dle dokumentace cekat alespon 10 ms
DIGITALNI_STAV_RST_1;
delay(10); // Dle dokumentace cekat alespon 10 ms
eink_cmd(0xAA); // CMDH
eink_data(0x49);
eink_data(0x55);
eink_data(0x20);
eink_data(0x08);
eink_data(0x09);
eink_data(0x18);
eink_cmd(PWRR);
eink_data(0x3F);
eink_data(0x00);
eink_data(0x32);
eink_data(0x2A);
eink_data(0x0E);
eink_data(0x2A);
eink_cmd(PSR);
eink_data(0x5F);
eink_data(0x69);
eink_cmd(POFS);
eink_data(0x00);
eink_data(0x54);
eink_data(0x00);
eink_data(0x44);
eink_cmd(BTST1);
eink_data(0x40);
eink_data(0x1F);
eink_data(0x1F);
eink_data(0x2C);
eink_cmd(BTST2);
eink_data(0x6F);
eink_data(0x1F);
eink_data(0x16);
eink_data(0x25);
eink_cmd(BTST3);
eink_data(0x6F);
eink_data(0x1F);
eink_data(0x1F);
eink_data(0x22);
eink_cmd(0x13); // IPC
eink_data(0x00);
eink_data(0x04);
eink_cmd(PLL);
eink_data(0x02);
eink_cmd(0x41); // TSE
eink_data(0x00);
eink_cmd(CDI);
eink_data(0x3F);
eink_cmd(TCON);
eink_data(0x02);
eink_data(0x00);
eink_cmd(TRES);
eink_data(0x03);
eink_data(0x20);
eink_data(0x01);
eink_data(0xE0);
eink_cmd(VDCS);
eink_data(0x1E);
eink_cmd(T_VDCS);
eink_data(0x00);
eink_cmd(0x86); // AGID
eink_data(0x00);
eink_cmd(PWS);
eink_data(0x2F);
eink_cmd(0xE0); // CCSET
eink_data(0x00);
eink_cmd(0xE6); // TSSET
eink_data(0x00);
eink_cmd(0x04); //power on
eink_cekej_na_vykresleni();
}
// Prechod displeje do spanku
void eink_spanek(void) {
eink_cmd(0X02);
eink_data(0x00);
eink_cekej_na_vykresleni();
}
// Funkce pro kresleni bitmapy
void eink_bitmapa(void) {
// Zapisujeme pixely do frame bufferu displeje
eink_cmd(0x10);
for (uint32_t bajt = 0; bajt < BITMAP_SIZE; bajt++) {
eink_data(bitmap_buffer[bajt]);
}
// Povel k refreshi displeje
eink_cmd(0x12);
eink_data(0x00);
delay(1);
eink_cekej_na_vykresleni();
}
// Blokujici funkce, dokud se na pinu BUSY nezmeni stav indikujici, ze prekreslovani je hotove
void eink_cekej_na_vykresleni(void) {
while (!BUSY_STAV)
; //0:BUSY, 1:VOLNO
}