Pojďme programovat elektroniku | Meteorologie | E-ink

Ultimátní předpověď počasí. Na e-inku se nám kreslí radarový, družicový i teplotní pohled na Česko

  • Nedávno jsme zobrazili srážkový radar na RGB LED mapě Česka
  • Dnes to zkusíme s barevným e-inkem
  • Přidáme také teplotní mapu Česka a družicový snímek oblačnosti

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:

Zdrojové kódy dnešního projektu najdete také na GitHubu

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.

24f3bea1-41e5-4af2-899b-0934e4a41736388fd8b7-8e70-4f6b-8143-2df4b2eca008
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á).

3f5989e1-9db5-4faf-bc5c-ab50c116b047
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!

44d1ab08-6b54-4dad-b878-692838bd6b27
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.

a22575a9-13d4-4b90-9f93-a79572bb5bc7
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. 

18d6a640-3962-4b06-a66d-e312b1caebf6661efc29-c9bf-4842-8254-ebdaf9671dff
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.

15c3b0f7-7fac-4f2e-b769-a853370c8622ac1105a6-03c9-4636-9a95-c9ca73c528ed16a58035-d8ce-440f-b2c4-f631e603be6f
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.

bb6958ed-55ce-47d8-9807-8d5761a3ea50
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.

24e1e14f-3236-4a5b-8754-2b0dc05bed57
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.

8cf134ed-8a15-4f2c-a25a-75fdb762dea5
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 a 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.

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

08c7f70c-34b6-472c-a05e-84e1428124ac
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
}
Diskuze (8) Další článek: Symbol technologické vyspělosti. První čínské dopravní letadlo absolvovalo komerční let s cestujícími

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