Fyzikální kabinet FyzKAB
TechHobby ESP32 + MicroPython ESP32: Čidla a senzory v MicroPythonu IR přijímač KY-022 s ESP32 a MicroPythonem

IR přijímač KY-022 s ESP32 a MicroPythonem

26. díl „volného seriálu“ článků

Proč právě IR, tedy infračervený bezdrátový přenos? Infračervené ovládání patří mezi nejrozšířenější technologie dálkového řízení – je levné, energeticky úsporné a dostatečně spolehlivé. Najdeme ho v dálkových ovladačích k televizím, audiotechnice, klimatizacím, hračkám i celé řadě dalších zařízení.

Jeho hlavní výhodou pro nás je snadná dostupnost, spolehlivost a – jak si ukážeme – i poměrně jednoduchá integrace do MicroPython projektu. Naprogramovat vlastní IR přijímač není nijak složité. A ruku na srdce: kdo by nechtěl ovládat svůj projekt pohodlně z gauče, stejně jako televizi?

Princip IR ovládání

Než se pustíme do dalších „bastlířských“ hrátek, tentokráte s IR přijímačem, podívejme se, jak vlastně infračervené dálkové ovládání funguje.

Infračervené elektromagnetické záření je blízké viditelnému světlu, jen má delší vlnovou délku – a proto ho lidské oko nevidí. V technice se začalo využívat například právě v dálkových ovladačích k televizím, k jeho rozšíření výrazně přispěla i nízká cena IR komponentů. Protože se IR záření chová podobně jako světlo, nemusíme řešit odstínění například jako u radiových vysílačů – takže se pak nestane, že budete omylem přepínat televizi sousedovi přes zeď, pokud má stejný ovladač. 😊

Výhodou tohoto „neviditelného světla“ je, že ho běžné světlo příliš neruší. Na druhou stranu, i běžné tepelné zdroje – žárovky, ale i Slunce – kolem sebe šíří dost IR záření. Ovládání tedy rozhodně nemůže fungovat podle principu: IR záření dopadá na čidlo → HIGH, nedopadá → LOW. Jasné osvícení a zatemnění sice roli hraje, ale ne tak jednoduše.

Celý trik je v tom, že signál je modulovaný – a právě tuto modulaci náš IR přijímač rozpoznává. Dálkový ovladač s IR LED vlastně „vyblikává“ konkrétní kód, který nese určitou informaci. A tu je třeba dekódovat. Díky přesně dané frekvenci (obvykle 38 kHz) se navíc minimalizuje rušení okolním IR šumem – v přírodě totiž jen těžko najdete zdroj, který by přesně pulzoval na 38 kHz.

Obvod IR přijímače

Obvod IR přijímače (viz obrázek č. 1) se skládá z infračerveného senzoru, který převádí světlo vysílané IR diodou na odpovídající elektrický signál. Ten je pak třeba obvykle zesílit a demodulovat – výsledkem je digitální signál, který na výstupu přechází mezi hodnotami LOW a HIGH. Tento signál pak využijeme pro obnovení původní, vyslaná informace.

V našem experimentu použijeme hotový IR set, který je běžně a levně k sehnání pro modul Arduino. Obsahuje infračervený dálkový ovladač (ale můžete klidně použít i ten od televize nebo Hi-Fi věže), modul IR přijímače KY-022, IR LED diodu (tu si zatím schováme na jiné pokusy) a několik dupont kabelů na propojení (viz obrázek č. 1).

IR sada
Obr. č. 1 – obsah sady IR setu určeného pro modul Arduino

IR přijímač HX1838 – modul KY-022

Modul KY-022 využívá senzor HX1838, který má už v sobě integrovaný obvod pro příjem, zesílení i demodulaci IR signálu. Na jeho výstupu tak rovnou získáme digitální signál s úrovněmi HIGH/LOW.

blokove schema IR-cidla
Obr. č. 2 – blokové schéma IR detektoru HX1838 (VS1838B)

Modul reaguje na infračervené záření s vlnovou délkou přibližně 840–850 nm. S běžnou IR LED diodou v této oblasti docílíme dosahu až 10 metrů – v některých případech i více (uvádí se až 18 m). Napájecí napětí se pohybuje mezi 2,7 V a 5,5 V, což je ideální pro použití s modulem ESP32, který pracuje s logikou a napájením 3,3 V.

modul KY-022
Obr. č. 3 – modul IR senzoru KY-022

Destička modulu KY-022 má tři vývody: dva slouží k napájení (+ a ; provozní proud 0,4–1,5 mA), třetí pin (S) poskytuje digitální výstup. Schéma elektronického zapojení modulu najdete na následujícím obrázku č. 4.

schema modulu KY-022
Obr. č. 4 –schéma IR modulu KY-022

Jak vidíme na schématu zapojení modulu (obr. 4), senzor VS1838B (HX1838) je doplněn o LED diodu, která signalizuje logickou nulu na výstupu. Při příjmu signálu tedy poblikává – tím dává najevo, že senzor zachytil IR záření.

Blokové schéma IR čidla VS1838B (viz obrázek č. 1) ukazuje, že uvnitř senzoru se na výstupu nachází pull-up rezistor vůči napájení. Ten by znamenal určité riziko pro modul ESP32, pokud bychom modul senzoru chtěli napájet napětím 5 V. Při připojování k modulu ESP32 je tedy důležité dodržet napájení 3,3 V.

A když už jsme u připojení k modulu ESP32, pojďme si to rovnou ukázat. Pro napájení použijeme piny GND a 3V3. Signál z IR čidla pak připojíme na libovolný GPIO pin, který lze nastavit jako digitální vstup. My jsme zvolili GPIO15, ale použít lze téměř jakýkoliv jiný. Následující obrázek (obr. č. 5) ukazuje konkrétní propojení IR modulu KY-022 s mikrokontrolérem ESP32, detaily pak shrnuje připojená tabulka.

schema zapojeni - ESP32 a IR KY-022
Obr. č. 5 – propojení IR modulu KY-022 a modlu ESP32
IR modul (KY-022) Modul ESP32
GND
+ 3V3
S GPIO15

Jako obvykle si nejprve vyzkoušíme přímé čtení IR signálu – ať vidíme, jak senzor funguje. Poté si ukážeme, jak práci zjednodušit pomocí knihovny. A protože se už skoro stává nepříjemným pravidlem, že knihovny na ESP32 nejsou vždy úplně bez problémů, opět se nám budou hodit zkušenosti z předchozích článků.

Přímé načítání IR čidla:

Prvním způsobem, jak získat data z IR čidla, je přímé načítání výstupního pinu a ruční vyhodnocování jednotlivých přijatých signálů. Ve své podstatě tak budeme „rekonstruovat“ bitový řetězec, který byl čidlem zachycen.

Jak už jsme naznačili, odeslaný kód (sériový tok bitů) je modulován pomocí protokolu NEC na základní přenosovou frekvenci – obvykle 38 kHz. Na následujícím obrázku vidíme, jak je reprezentována úroveň logické „0“ a „1“ – vždy je „vyblikán“ signál délky 560 μs, následná délka mezery určuje odeslanou úroveň.

IR modulace
Obr. č. 6 – ukázka modulace binární reprezentace v podobě světelných záblesků

Jak vypadá signál (NEC protokol)

Modulovaný signál je přesně to, co „vidí“ IR přijímač. Průběh tohoto signálu je poměrně „divoký“. Z tohoto důvodu IR čidlo není jen klasický fototranzistor nebo fotodioda, ale je osazeno určitou vyhodnocovací inteligencí (viz obr. 2). Úkolem celého IR čidla tedy je přijatý signál demodulovat a převést ho do podoby binárního průběhu, který pak již můžeme číst jako digitální hodnoty na GPIO vstupu ESP32 (viz obr. 7)

IR-Signal deModulation
Obr. č. 7 – převod IR signálu na demodulovaný signál IR čidlem

Celý signál má pevně danou strukturu (dle protokolu NEC):

  • Na začátku je sestupná hrana a 9 ms dlouhý úsek vysílání (dříve se používal k nastavení zesílení dřívějších IR přijímačů),
  • následuje pauza 4,5 ms.
  • Dále vysílaný bitový signál se skládá z 562 µs dlouhých „blikacích“ úseků (jakési uvození vysílaného bitu),
  • mezi kterými jsou „mezery“, jejichž délka určuje hodnotu vysílaného bitu (viz obr. 8):
    • Krátká mezera (∼562 µs) znamená logickou 0.
    • Dlouhá mezera (∼1675 µs) znamená logickou 1.

V řadě článků se uvádí, že výstup z IR senzoru, který je už připraven pro načtení mikrokontrolérem, by měl být jako na obrázek č. 8.

IR demodulace
Obr. č. 8 – podoba a rozložení informace v demodulovaném signálu NEC protokolu

Ve skutečnosti je však tento výstup invertovaný! Takže délku „mezery“, která určuje, zda se jedná o zakódovanou hodnotu logické „0“, nebo „1“, budeme načítat jako stav HIGH. (viz obr. 9 – vstupní signál modře, výstupní signál žlutě)

IR signal vs. vystup
Obr. č. 9 – srovnání signálů na vstupu a výstupu IR čidla (obrazovka osciloskopu)

Pomocí měření délky trvání stavu HIGH tedy můžeme jednotlivé bity dekódovat a tím získat původní sériový bitový tok.
Jinými slovy: krátký HIGH signalizuje logickou „0“, dlouhý HIGH odpovídá logické „1“.

Struktura datového rámce (NEC protokol)

Standardní NEC protokol obsahuje 32 bitů. Pokud se jedná o variantu NEC8, platí následující:

  • Celá sekvence se skládá z 32 bitů – odeslané informace: adresa a příkaz.
  • Adresa (8b) a příkaz (8b) se vysílají dvakrát. V druhé osmici dané informace (adresa/příkaz) jsou všechny bity invertovány a používají se k ověření přijaté zprávy. Pokud se hodnota a hodnota plynoucí z její inverze neshodují, je přenesená sekvence považována za neplatnou a příkaz se ignoruje.
  • Bity každého byte se přenášejí od LSB k MSB.

Pokud nemáte zájem o spolehlivost, můžete invertované hodnoty ignorovat nebo můžete adresu a příkaz rozšířit na 16 bitů. Obětováním redundance adres byl rozsah adres rozšířen z 256 možných hodnot na přibližně 65 000 různých hodnot (protokol NEC16). Tímto způsobem byl rozsah adres rozšířen z 8 bitů na 16 bitů, aniž by se změnila jakákoli jiná vlastnost protokolu.

Čistě pro zajímavost zmiňujeme, že se příkaz vysílá pouze jednou – a to i v případě, že je tlačítko na dálkovém ovladači stisknuté po dlouhou dobu. Při tomto trvalém stisknutí se každých 110 ms odvysílá pouze zvláštní opakující se kód, který potvrzuje platnost předchozího příkazu. Tento opakující se kód je 9 ms úvodní impuls, následovaný 2,25 ms mezerou a 560 μs dávkou.

Princip funkce našeho programu:

Podívejme se, co musí náš program udělat, aby dokázal načíst odeslanou sekvenci a vyhodnotit ji jako konkrétní povel, na který následně zareaguje.

Program postupně:

  • čeká na začátek IR signálu (tedy na první sestupnou hranu, která označuje start přenosu),
  • měří délku trvání vysokých pulsů (HIGH) v mikrosekundách,
  • na základě těchto hodnot rekonstruuje binární kód (krátký HIGH znamená 0, dlouhý HIGH znamená 1).

Z takto získané sekvence jedniček a nul pak program pokračuje dál:

  • z řetězce tvořeného sekvencí znaků „0“ a „1“ získá příkaz (kód zmáčknutého tlačítka) a případně i adresu (i když tu v našem případě nevyužijeme),
  • kód příkazu pak porovná s předem nadefinovanou slovníkovou tabulkou, která obsahuje přiřazení kódů k jednotlivým funkcím (tu si vytvoříme podle konkrétního ovladače).

Když tedy víme, co má program dělat, pojďme si ukázat, jak to celé zrealizovat v MicroPythonu.

1. Čekání na začátek signálu (první LOW):

Pokud není vysílán žádný signál, zůstává výstup modulu KY-022 na úrovni HIGH. Přenos začíná poklesem signálu na úroveň LOW – v tu chvíli se na modulu KY-022 rozsvítí vestavěná LED (viz schéma – obr. 4).

Program tedy nejprve čeká na první sestupnou hranu, která značí začátek přenosu:

wait == 1
while wait == 1:
    wait = ird.value()

2. Hlavní měření signálu:

Po začátku přenosu následuje samotná sekvence bitů (0 a 1). Nyní nás zajímá délka trvání stavů HIGH.

a) stavy LOW (signál = 0):

Prvním příchozím stavem je LOW, neboť tím přenos začíná. Také stavy HIGH jsou prokládány krátkým stavem LOW. Tento úsek neměříme – jednoduše ho „přečkáme“ a začneme měřit vždy až délku následujícího stavu HIGH

while ird.value() == 0:
    pass
b) Měření délky HIGH (signál = 1):

Jakmile začne stav HIGH, spustíme měření. Uložíme čas začátku a po jeho skončení čas konce. Výsledný rozdíl uložíme do seznamu seq1.

Pozor: pokud přenos skončí, modul KY-022 zůstane ve stavu HIGH – a cyklus by se nikdy neukončil. Proto kontrolujeme, jestli trvání signálu HIGH nepřesáhne 10 ms. Pokud ano, přenos považujeme za ukončený a smyčku ukončíme pomocí proměnné complete.

seq1 = []  # Délky logických 1 (signál HIGH)
complete = 0
ms1 = utime.ticks_us()
while ird.value() == 1 and complete == 0:
    ms2 = utime.ticks_us()
    diff = utime.ticks_diff(ms2, ms1)
    if diff > 10000:
        complete = 1
seq1.append(diff)

3. Převod měřených délek na binární kód:

Ze změřených délek stavu HIGH zrekonstruujeme binární řetězec. Krátký puls značí logickou „0“, delší značí „1“. Současně odfiltrujeme poslední stav, který přenos ukončuje (je výrazně delší než ostatní a přidal se díky complete).

  • < 700 µs → logická 0
  • 700–2000 µs → logická 1

Výsledná binární sekvence logických „0“ a „1“ se ukládá do textového řetězce code. Protože se přenáší nejdříve nejnižší bit (LSB) sestavujeme řetězec zprava doleva.

bin_code = ""
for val in seq1:
    if val < 2000:
        if val < 700:
            bin_code = "0" + bin_code
        else:
            bin_code = "1" + bin_code
return bin_code

Tím máme definitivně získanou zachycenou binární sekvenci, tak, jak byla odeslána.

4. Převod binárního kódu na příkaz:

Pokud jsme stiskli např. tlačítko 1, chceme, aby se ve výstupu programu objevila tato informace, tedy jako konkrétní znak „1“. K tomu bude sloužit další část kódu.

Nejprve zkontrolujeme, že délka binárního řetězce je přesně 32 bitů (NEC protokol). Pokud ne, vrátíme hodnoty 0 a 0. Jinak rozdělíme řetězec na příkaz a adresu. Kvůli způsobu načítání (kód jsme otočili už při samotném záznamu) máme segmenty přeházené oproti klasickému NEC8:

  • 8.–16. bit obsahuje příkaz (data)
  • 24.–32. bit obsahuje adresu
def parse_nec_code(bin_str):
    if len(bin_str) != 32:
        print("Chyba: Řetězec musí mít přesně 32 bitů.")
        return 0, 0
    # pořadí segmentů
    data_bin = bin_str[8:16]
    addr_bin = bin_str[24:32]

Máme-li správně získané části řetězce, můžeme je převést z binární soustavy na celé číslo:

    # Převedení do int
    data = int(data_bin, 2) # ta 2 oznacuje ciselnou soustavu
    addr = int(addr_bin, 2)

Získané výsledky funkce vrátí.

    # Výpis výsledků
    return data, addr

5. Přiřazení příkazu pomocí slovníku

Získaný číselný kód (např. 247) přiřadíme k názvu tlačítka pomocí předem nadefinovaného slovníku act. Pokud kód ve slovníku nemáme, vracíme „nedef.“:

def tlacitko(code):
    command = ""
    for k,v in act.items():
        if code == k:
            command = v
            if command == "":
                command = "nedef."

    return command
Kód celého programu (slovník příkazů je sestaven podle námi použitého dálkového ovládání):

import utime
from machine import Pin

ird = Pin(15, Pin.IN)

act = {
    69 : "1",
    70 : "2",
    71 : "3",
    68 : "4",
    64 : "5",
    67 : "6",
    7 : "7",
    21 : "8",
    9 : "9",
    25 : "0",
    28 : "OK",
    24 : "UP",
    82 : "DOWN",
    8 : "LEFT",
    90 : "RIGHT",
    22 : "*",
    13 : "#"
   }

def read_ircode(ird):
    wait = 1
    complete = 0
    seq1 = []
    # cekame na sestupnou hranu
    while wait == 1:
        wait = ird.value()

    while complete == 0:
        # cekame na nabeznou hranu
        while ird.value() == 0:
            pass

        # merime cas stavu HIGH
        ms1 = utime.ticks_us()
        while ird.value() == 1 and complete == 0:
            ms2 = utime.ticks_us()
            diff = utime.ticks_diff(ms2, ms1)
            if diff > 50000:
                complete = 1
        seq1.append(diff)

    bin_code = ""
    for val in seq1:
        if val < 2000:
            if val < 700:
                bin_code = "0" + bin_code
            else:
                bin_code = "1" + bin_code
    # print(bin_code)
    return bin_code

def tlacitko(code):
    command = ""
    for k, v in act.items():
        if code == k:
            command = v
    if command == "":
        command = "nedef."

    return command

def parse_nec_code(bin_str):
    if len(bin_str) != 32:
        print("Chyba: Řetězec musí mít přesně 32 bitů.")
        return 0, 0

    # pořadí segmentů
    data_bin = bin_str[8:16]
    addr_bin = bin_str[24:32]
    # Převedení do integerů
    data = int(data_bin, 2)
    addr = int(addr_bin, 2)

    # Výpis výsledků
    return data, addr

while True:
    result = read_ircode(ird)
    data, addr = parse_nec_code(result)
    print(f"\nbinarni sekv.: {result}")
    print(f"Data: 0x{data:02x}, Addr: 0x{addr:02x}")
    print("Tlacitko:", tlacitko(data))

    utime.sleep(0.5)

IR senzor a ESP32
Obr. č. 10 – sestava našeho IR přenosu (IR dálkový ovladač, IR čidlo KY-022 a modul ESP32 s MicroPythonem

Po spuštění kódu se program v modulu ESP32 ihned zastaví na funkci read_ircode(), která čeká na příjem IR signálu. Jakmile na dálkovém ovladači stiskneme jakékoliv tlačítko, je zachycena a zpracována celá odeslaná sekvence. Funkce read_ircode() tuto sekvenci vrátí jako binární řetězec.

Pomocí funkce parse_nec_code() z něj následně získáme adresu a příkaz. Ty jsou zpracovány podle předpokládaného protokolu NEC8, tedy s bitovou adresou. Obě hodnoty se převedou na celá čísla a vypíšou ve formátu hexadecimálních čísel.

Pro praktické použití je ale mnohem pohodlnější, když místo čísel vidíme přímo označení stisknutého tlačítka. K tomu slouží předem nadefinovaný slovník a funkce tlacitko(), která podle kódu vrátí textový popis.

Kód, jak je zde uvedený, není zamýšlen jako produkční, ale spíše jako didaktický příklad. Víme, že by šlo algoritmus optimalizovat – například dekódovat jednotlivé bity rovnou během měření a neukládat nejprve celé pole časů. To už ale ponecháváme na zvídavosti a experimentování čtenářů, kteří si mohou kód upravit podle svého.

Výstup programu vidíme na následujícím obrázku č. 11:

vystup IR
Obr. č. 11 – Načtení IR signálu z dálkového ovladače (výstup v prostředí Thonny IDE)

Pokud použijeme jiné dálkové ovládání (například od televize), program sice na stisknutá tlačítka zareaguje, ale nezobrazí jejich názvy – nemáme je totiž zahrnuté v našem převodním slovníku. Pro plnohodnotné použití bychom si museli tabulku příkazů doplnit sami podle konkrétního ovladače.

Načítání IR čidla knihovnou:

Přestože jsme si sami dokázali přečíst kód stisknutého tlačítka z IR ovladače, má náš původní program jednu výraznou slabinu. Funkce read_ircode() zastaví běh programu a čeká na příchod signálu – konkrétně na první sestupnou hranu. A ta může přijít… ale také nemusí. Pokud dálkový ovladač právě nikdo nepoužívá, je ESP32 zcela paralyzovaný a nedělá vůbec nic.

Pokud chceme ovladač použít pro řízení nějaké „užitečné“ funkce (např. spouštění akce nebo přepínání režimu), potřebujeme, aby ESP32 zůstalo aktivní a reagovalo na signál jen tehdy, když skutečně přijde.

Řešení jsou v zásadě dvě:

  • celý původní přístup přepracovat (např. pomocí přerušení),
  • nebo sáhnout po hotovém řešení v podobě knihovny, která to za nás již vyřešila.

Jednou z možností je knihovna ir_rx.py od Petera Hinche (🔗 GitHub – micropython_ir), která nabízí komfortní a asynchronní zpracování IR signálu. Podle GitHubu je její poslední verze z roku 2024, což je skvělá zpráva – autor ji stále udržuje aktuální.

Z praxe víme, že u neaktualizovaných knihoven starších pěti a více let často dochází k problémům s kompatibilitou, zejména s novějšími verzemi MicroPythonu pro ESP32. Tady by nás to ale díky aktivnímu vývoji nemělo potkat.

Hlavní předností použití knihovny je však pochopitelně v tom pohodlí, kdy nemusíme řešit nějaké stavu LOW a HIGH plynoucí z protokolu NEC, a prostě jen načíst čidlo zvolenou metodou. To je někdy fakt k nezaplacení!

Problém s knihovnou ir_rx.py

Bohužel – a jak už to u ESP32 s MicroPythonem bývá – ani knihovna ir_rx.py nefunguje hned napoprvé tak, jak bychom očekávali.

Po stažení všech potřebných souborů a jejich nahrání do ESP32 program selže. Po důkladnějším prozkoumání zdrojového kódu jsme zjistili, že problém nastává při pokusu knihovny použít tzv. softwarový časovač. Zatímco hardwarové časovače modulu ESP32 dobře známe – používají se například pro přerušení nebo generování PWM signálu – o existenci softwarového časovače v MicroPythonu slyšíme v této souvislosti poprvé. A zřejmě nejsme sami. Zdá se, že ani náš konkrétní firmware ESP32 o takové možnosti netuší – a přesně na tom knihovna hlásí chybu.

Je možné, že v některé starší verzi MicroPythonu, nebo na jiném typu firmwaru, tato konstrukce fungovala. Ale zde jsme narazili.

Abychom mohli v článku pokračovat a zároveň čtenářům nabídli prakticky použitelný kód, rozhodli jsme se knihovnu upravit. Vytvořili jsme zjednodušenou verzi, kterou lze použít jako lehký a přehledný modul v jednom souboru – ideální pro naše účely. Věříme, že autor knihovny na GitHubu brzy vydá aktualizovanou verzi s podporou i pro aktuální buildy ESP32, kde tento problém již nebude.

Kód knihovny ir_rx.py (verze FyzKAB):

# Author: Peter Hinch
# temporary version 2025 – modified by FyzKAB (fixed problem with Timer(-1) for ESP32)
# Copyright Peter Hinch 2020-2024 Released under the MIT license

# Thanks are due to @Pax-IT for diagnosing a problem with ESP32C3.

from machine import Timer, Pin
from array import array
from utime import ticks_us, ticks_diff

# from micropython import alloc_emergency_exception_buf
# alloc_emergency_exception_buf(100)


# On 1st edge start a block timer. While the timer is running, record the time
# of each edge. When the timer times out decode the data. Duration must exceed
# the worst case block transmission time, but be less than the interval between
# a block start and a repeat code start (~108ms depending on protocol)


class IR_RX:
    Timer_id = 2  # Software timer but enable override
    # Result/error codes
    # Repeat button code
    REPEAT = -1
    # Error codes
    BADSTART = -2
    BADBLOCK = -3
    BADREP = -4
    OVERRUN = -5
    BADDATA = -6
    BADADDR = -7

    def __init__(self, pin, nedges, tblock, callback, *args):  # Optional args for callback
        self._pin = pin
        self._nedges = nedges
        self._tblock = tblock
        self.callback = callback
        self.args = args
        self._errf = lambda _: None
        self.verbose = False

        self._times = array("i", (0 for _ in range(nedges + 1)))  # +1 for overrun
        pin.irq(handler=self._cb_pin, trigger=(Pin.IRQ_FALLING | Pin.IRQ_RISING))
        self.edge = 0
        self.tim = Timer(self.Timer_id)  # Defaul is sofware timer
        self.cb = self.decode

    # Pin interrupt. Save time of each edge for later decode.
    def _cb_pin(self, line):
        t = ticks_us()
        # On overrun ignore pulses until software timer times out
        if self.edge <= self._nedges:  # Allow 1 extra pulse to record overrun
            if not self.edge:  # First edge received
                self.tim.init(period=self._tblock, mode=Timer.ONE_SHOT, callback=self.cb)
            self._times[self.edge] = t
            self.edge += 1

    def do_callback(self, cmd, addr, ext, thresh=0):
        self.edge = 0
        if cmd >= thresh:
            self.callback(cmd, addr, ext, *self.args)
        else:
            self._errf(cmd)

    def error_function(self, func):
        self._errf = func

    def close(self):
        self._pin.irq(handler=None)
        self.tim.deinit()

class NEC_ABC(IR_RX):
    def __init__(self, pin, extended, samsung, callback, *args):
        # Block lasts <= 80ms (extended mode) and has 68 edges
        super().__init__(pin, 68, 80, callback, *args)
        self._extended = extended
        self._addr = 0
        self._leader = 2500 if samsung else 4000  # 4.5ms for Samsung else 9ms

    def decode(self, _):
        try:
            if self.edge > 68:
                raise RuntimeError(self.OVERRUN)
            width = ticks_diff(self._times[1], self._times[0])
            if width < self._leader:  # 9ms leading mark for all valid data
                raise RuntimeError(self.BADSTART)
            width = ticks_diff(self._times[2], self._times[1])
            if width > 3000:  # 4.5ms space for normal data
                if self.edge < 68:  # Haven't received the correct number of edges
                    raise RuntimeError(self.BADBLOCK)
                # Time spaces only (marks are always 562.5µs)
                # Space is 1.6875ms (1) or 562.5µs (0)
                # Skip last bit which is always 1
                val = 0
                for edge in range(3, 68 - 2, 2):
                    val >>= 1
                    if ticks_diff(self._times[edge + 1], self._times[edge]) > 1120:
                        val |= 0x80000000
                    elif width > 1700: # 2.5ms space for a repeat code. Should have exactly 4 edges.
                        raise RuntimeError(self.REPEAT if self.edge == 4 else self.BADREP)  # Treat REPEAT as error.
                    else:
                        raise RuntimeError(self.BADSTART)
                    addr = val & 0xff  # 8 bit addr
                    cmd = (val >> 16) & 0xff
                    if cmd != (val >> 24) ^ 0xff:
                        raise RuntimeError(self.BADDATA)
                    if addr != ((val >> 8) ^ 0xff) & 0xff:  # 8 bit addr doesn't match check
                        if not self._extended:
                            raise RuntimeError(self.BADADDR)
                        addr |= val & 0xff00  # pass assumed 16 bit address to callback
                    self._addr = addr
                except RuntimeError as e:
                    cmd = e.args[0]
                    addr = self._addr if cmd == self.REPEAT else 0  # REPEAT uses last address
                # Set up for new data burst and run user callback
                self.do_callback(cmd, addr, 0, self.REPEAT)

class NEC_8(NEC_ABC):
    def __init__(self, pin, callback, *args):
        super().__init__(pin, False, False, callback, *args)

class NEC_16(NEC_ABC):
    def __init__(self, pin, callback, *args):
        super().__init__(pin, True, False, callback, *args)

Neztrácejme čas – pojďme rovnou na příklad s knihovnou ir_rx.py

Než ztrácet čas láteřením nad problémy s knihovnami v MicroPythonu pro modul ESP32 raději se rovnou pustíme do ukázky použití knihovny ir_rx.py.

Použití knihovny ir_rx.py

Následující příklad ilustruje nejen použití samotné knihovny ir_rx.py, ale hlavně její největší výhodu – asynchronní zpracování IR signálu. Zatímco v hlavní smyčce programu bude blikat vestavěnou LED, IR přijímač mezitím bude na pozadí a nezávisle detekovat příchozí signály z dálkového ovladače.

Kód programu:

import time
from machine import Pin
from ir_rx import NEC_8

def callback(data, addr, ctrl):
    if data > 0:   # NEC protocol sends repeat codes.
    print('Data {:02x}'.format(data))

ir = NEC_16(Pin(15, Pin.IN), callback)

led = Pin(2, Pin.OUT)

print('LED blika...')

while True:
    led.on()
    time.sleep(0.1)
    led.off()
    time.sleep(0.1)

Pro načítání IR čidla potřebujeme z knihovny ir_rx.py importovat třídu NEC_8, která odpovídá protokolu použitého v modulu KY-022:

from ir_rx import NEC_8

Vytváříme instanci třídy NEC_8, kde:

  • první parametr je GPIO pin (zde Pin(15)), na který je připojen signálový výstup IR modulu,
  • druhý parametr je funkce callback, která se zavolá pokaždé, když čidlo zachytí kód z ovladače.
ir = NEC_8(Pin(15, Pin.IN), callback)

Funkce callback() přijímá tři parametry, ale pro začátek nás zajímá jen ten data, tedy samotný kód přijatý z IR ovladače. Pokud je hodnota větší než nula (což vylučuje opakované kódy NEC protokolu), vypíšeme ji na výstup:

def callback(data, addr, ctrl):
    if data > 0:   # NEC protocol sends repeat codes.
    print('Data {:02x}'.format(data))

Tím máme funkční IR čidlo! Zbytek programu tvoří jednoduchá nekonečná smyčka, ve které bliká LED – jako simulace jiné paralelní činnosti.

Následující okno prostředí Thonny IDE ukazuje výstup programu:

vystup IR
Obr. č. 12 – Načtení IR signálu pomocí knihovny ir_rx.py (výpis kódů)

Převod kódu na název tlačítka

Podobně jako v dřívějším příkladu můžeme převést přijatý číselný kód na popis stisknutého tlačítka. Stačí si nadefinovat „slovník“, ve kterém bude každému číslu odpovídat konkrétní tlačítko:

ir_key = {
    0x1C: 'OK',
    0x5A: 'RIGHT',
    0x08: 'LEFT',
    0x52: 'DOWN',
    0x18: 'UP',
    0x0D: '#',
    0x16: '*',
    0x19: '0',
    0x45: '1',
    0x46: '2',
    0x47: '3',
    0x44: '4',
    0x40: '5',
    0x43: '6',
    0x07: '7',
    0x15: '8',
    0x09: '9'
    }

Výše uvedené hodnoty jsou pochopitelně nastaveny pro námi použité IR dálkové ovládání.

Funkci callback() pak jednoduše upravíme, aby vytiskla název odpovídajícího tlačítka:

def callback(data, addr, ctrl):
    if data > 0:
        print(ir_key.get(data, "nedef."))
Poznámka:
Použili jsme metodu .get() místo klasického indexování, abychom předešli chybě, pokud přijde neznámý kód, v takovém případě se vypíše nedef.

Obrázek č. 13 ukazuje výpis výstupu programu v okně prostředí Thonny IDE:

vystup IR
Obr. č. 13 – Načtení IR signálu pomocí knihovny ir_rx.py (výpis znaků)

IR ovládání vestavěné LED

A do třetice všeho dobrého – když už nám detekce tlačítek funguje spolehlivě, zkusíme vytvořit program, který bude i trochu praktický. Navíc si na něm ukážeme jednu nenápadnou past, kterou jsme omylem nastražili naší opravou knihovny ir_rx.py.

Poznámka:
Inspirací pro tento příklad byl projekt George Bantiqua ze stránek TechToTinker.blogspot.com.

Co program dělá?

Pomocí dálkového ovladače budeme ovládat vestavěnou LED na modulu ESP32. Program rozpozná tři povely:

  • Tlačítko 1 – LED se rozsvítí.
  • Tlačítko 0 – LED zhasne.
  • Tlačítko OK – LED začne blikat.

Změna oproti předchozímu příkladu spočívá hlavně v úpravě hlavní smyčky, která se bude řídit podle příkazu přijatého z IR ovladače.

Hlavní smyčka – řízení LED

Než si ukážeme celý kód, pojďme se podívat na samotnou část smyčky, která vykonává dané akce:

while True:
    if ir_data > 0:
        if ir_data == 0x19:   # 0
            led.value(0)
            if isLedBlinking == True:
                tim1.deinit()
                isLedBlinking = False
        elif ir_data == 0x45:   # 1
            led.value(1)
            if isLedBlinking == True:
                tim1.deinit()
                isLedBlinking = False
        elif ir_data == 0x1C:   # OK
            isLedBlinking = True
            tim1.init(period= 500, mode= Timer.PERIODIC, callback= timer_callback)
        ir_data = 0

V  hlavní smyčce budeme testovat proměnnou ir_data, která bude obsahovat kód naposledy stisknutého tlačítka na IR ovladači.

Tato proměnná bude:

  • globální, protože ji budeme nastavovat uvnitř funkce callback()
  • použita v hlavní smyčce pro rozhodování, co dělat s LED (rozsvítit, zhasnout, nebo spustit blikání)
Úprava funkce callback()

Změníme ji tak, aby kromě výpisu také ukládala hodnotu do ir_data:

def ir_callback(data, addr, ctrl):
    global ir_data
    if data > 0:
        ir_data = data
        print('Data {:02x}'.format(data))
Reakce na tlačítka

V hlavní smyčce následně testujeme obsah proměnné ir_data. Pro tlačítka:

  • 1 (kód 0x45) – rozsvítíme LED
  • 0 (kód 0x19) – LED zhasneme
  • OK (kód 0x1C) – spustíme blikání pomocí časovače
Blikání pomocí přerušení – funkce časovače

Pro blikání použijeme časovač, který bude každých 500 ms měnit stav LED pomocí funkce timer_callback():

def timer_callback(timer):
    led.value(not led.value())

Funkce každým zavoláním přepne hodnotu pinu (z 0 na 1 a zpět), čímž vytvoří efekt blikání.

Když je stisknuto tlačítko 0 nebo 1, program:

  • nejprve zkontroluje, zda časovač běží (indikováno proměnnou isLedBlinking)
  • pokud ano, časovač zastaví pomocí tim1.deinit()
  • LED buď rozsvítí, nebo zhasne podle konkrétního příkazu
Celý kód programu„IR ovládání vestavěné LED“:

from machine import Pin
from machine import Timer
from ir_rx import NEC_8

def ir_callback(data, addr, ctrl):
    global ir_data
    if data > 0:
        ir_data = data
        print('Data {:02x}'.format(data))

def timer_callback(timer):
    led.value(not led.value())

ir = NEC_8(Pin(15, Pin.IN), ir_callback)

led = Pin(2, Pin.OUT)
tim1 = Timer(1)
isLedBlinking = False
ir_data = 0

while True:
    if ir_data > 0:
        if ir_data == 0x19:   # 0
            led.value(0)
            if isLedBlinking == True:
                tim1.deinit()
                isLedBlinking = False
        elif ir_data == 0x45:   # 1
            led.value(1)
            if isLedBlinking == True:
                tim1.deinit()
                isLedBlinking = False
        elif ir_data == 0x1C:   # OK
            isLedBlinking = True
            tim1.init(period=500, mode=Timer.PERIODIC, callback=timer_callback)
        ir_data = 0


Poznámky:
Než se definitivně rozloučíme s tímto tématem, podívejme se na dvě drobnosti, které mohou způsobit zbytečné potíže:

1. Globální proměnná ir_data

Ve funkci callback() měníme hodnotu proměnné ir_data. Aby to fungovalo správně, musíme tuto proměnnou uvnitř funkce označit jako globální. Jinak by Python vytvořil svou vlastní lokální kopii a hlavní program by o změně vůbec nevěděl.

Správný zápis vypadá tedy takto:

def ir_callback(data, addr, ctrl):
    global ir_data
    if data > 0:
        ir_data = data
        print('Data {:02x}'.format(data))

2. Problém Timer(2) – naše vlastní past

A teď slíbená past – schválně si ji můžete vyzkoušet:

V původním programu jsme použili hardwarový časovač Timer(1). Co se stane, když v kódu časovač změníte třeba na Timer(2)?

tim1 = Timer(2)

Program se sice spustí, tlačítka dál fungují… Skoro! Přestalo nám fungovat blikání LED (tlačítko OK).

Vysvětlení: Kolize Timer(2)

Narazili jsme zde na konflikt mezi vlastními funkcemi a knihovnou ir_rx.py. V původní verzi knihovny autor použil softwarový časovač Timer(-1) (někdy označovaný jako „virtuální“), který však nefunguje na všech verzích MicroPythonu pro ESP32. A protože jsme chtěli, aby knihovna fungovala spolehlivě i na aktuálních verzích, museli jsme ji opravit – a v naší upravené verzi ir_rx.py jsme místo toho zvolili hardwarový časovač Timer(2). To ale znamená, že knihovna ir_rx.py nyní interně využívá Timer(2) pro svou vlastní práci s IR signálem.

Pokud tedy v našem programu Timer(2) použijeme znovu – například pro blikání LED nebo jiné časově řízené operace – dojde ke kolizi. Výsledkem je, že část programu (např. blikání LED) přestane fungovat, protože časovač už je vázán na knihovnu.

Dopad na ostatní funkce

Naše úprava knihovny tedy obsadila Timer(2). To má i další důsledky:

  • Nelze použít PWM na některých pinech, které sdílejí časovač 2 – konkrétně jde o GPIO10 a GPIO12
  • Jakákoli další práce s Timer(2) bude problém a nejspíše nefunkční
Co s tím?

Musíme si tedy pamatovat, že Timer(2) je „obsazený“ knihovnou ir_rx.py, a pokud v programu potřebujeme:

  • PWM výstup
  • vlastní časovače

…musíme je vázat na jiné časovače: Timer(0), Timer(1) nebo Timer(3). To by mělo být pro běžné projekty více než dostačující.

A nebo, pokud chceme úplnou volnost, můžeme zkusit jiný firmware nebo pozdější verzi knihovny (autor Peter Hinch knihovnu pravidelně aktualizuje, takže časem může být vše opět jinak).

Závěr:

Původně jsme plánovali věnovat se v tomto článku jak přijímání, tak i vysílání IR signálu. Projekt se ale komplikoval nefunkčností původní knihovny ir_rx.py, proto jsme se zatím soustředili pouze na IR přijímač.

Co jsme si tedy dnes odnesli:

  • Pochopili jsme základní principy infračervené (IR) komunikace, kde se data přenáší pomocí pulzů světla nesených na nosné frekvenci (typicky kolem 38 kHz).
  • Seznámili jsme se s protokolem NEC, jedním z nejrozšířenějších protokolů pro IR dálkové ovládání, který zahrnuje přenos adresy zařízení a příkazů ve specifické časové posloupnosti impulsů.
  • Vyzkoušeli jsme si přímé načítání IR signálu bez použití specializované knihovny – „ruční“ zpracování časů trvání pulsů a jejich dekódování do binární podoby.
  • Naučili jsme se využívat knihovnu ir_rx.py, která nabízí asynchronní detekci signálů pomocí callback funkcí a výrazně tak zjednodušuje práci s IR.

  • Díky opravě knihovny jsme zjistili, že modul ESP32 má omezený počet hardwarových časovačů a je třeba dávat pozor na kolize. Upravená knihovna ir_rx.py totiž nyní používá Timer(2), což může způsobit problémy s dalšími funkcemi (například PWM).

Při objevení knihovny ir_rx.py jsme narazili i na její „sestřičku“ ir_tx.py, která umožňuje přeměnit IR LED diodu připojenou k modulu ESP32 na dálkové ovládání – tedy vysílač IR signálu! Naše IR sada, kterou jsme při dnešních hrátkách používali, obsahuje nejen dálkové ovládání a IR přijímač KY-022, ale právě i IR LED. Jak jsme na začátku říkali, že si ji schováme na další pokusy. Tak to by mohlo být ono! 😊

Do rukou se nám také dostala barevná RGB žárovka ovladatelná IR dálkovým ovladačem. To je výzva pro každého bastlíře! Představa, jak můžeme s pomocí modulu ESP32 a MicroPythonu dálkově ovládat barevné osvětlení nad pracovním stolem, je opravdu takovým „vánočním dárkem… v červnu“!

Příště nás tedy čekají hrátky s knihovnou ir_tx.py – a hlavně pořádná zábava s ovládáním RGB žárovky! Samozřejmě, pokud někde zase nečíhá nějaké „prohnilé“ překvapení, jak tomu bylo už  párkrát při pokusu o použití cizí hotové knihovny.

Pokračování: Připravujeme…
UPOZORNĚNÍ:
Nesouhlasíme s vyřazením Newtonových zákonů, Ohmova zákona a zákona zachování energie z učiva fyziky základních škol v České republice!