Pojďme programovat elektroniku: Špionážní tank s nočním viděním

  • Dost bylo míru, chceme válku!
  • A co by to bylo za válku bez tanku
  • Ten náš bude mít i kameru pro noční vidění

Už za pár hodin v nejedné české rodině po roce opět propukne legalizované domácí násilí vykoupené žlučníkovým záchvatem po pozření většího než malého množství vařených vajec. Tuto událost pochopitelně musíme reflektovat i v našem seriálu o programování elektroniky, a tak se i my tentokrát zlehka dotkneme vysoce sofistikovaných armádních technologií NATO.

Dnes si totiž postavíme tank! Co musí splňovat každý poctivý pásový tank? Musí mít spotřebu sovětského vozidla a musí být prošpikovaný americkými next-gen technologiemi. Náš tank toto všechno bude mít!

Klepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázek
Jako podvozek posloužily díly stavebnice Makeblock Starter Robot Kit, které jsem se více věnoval v lednovém vydání Computeru. Černé plošné díly zase náleží stavebnici Totem.

Zatímco o vysokou spotřebu se postará dvojice 9V elektromotorů s převodem na zhruba 200 otáček za minutu, které dodají dostatečnou sílu na zdolání jakékoliv překážky, šťávu poskytne vysoce reaktivní 11V 3S LiPo baterie z čínského AliExpressu. Při splnění několika podmínek tedy tank shoří jako tesla a promění se v opravdovou zbraň.

Klepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázek
Maličká modelářská tříčlánková LiPo baterie (při nabití každého článku na mezních 4,2 V dodá až 12,6 V). Poskytne vysoké proudy, bez dodatečné vypínací logiky ale potřebuje alarm. Jedná se o maličký voltmetr, který při poklesu napětí pod bezpečnou mez začne velmi silně pípat. V tu chvíli už patří baterie do nabíječky, hrozí totiž její poškození.

S kapacitou 550 mAh to sice není žádné terno a tank bude moci na jedno nabití jezdit jen okolo deseti minut, nicméně baterie se snadno vejde do podvozku. Podobné maličké baterie pohání třeba levné a drobné kvadrokoptéry.

Podívejte se, jak funguje zvukový alarm LiPo baterie (pípání je ve skutečnosti mnohonásobně hlasitější, aby jej uslyšel třeba pilot dronu):

Noční kamera pro bojové nasazení za tmy

Tolik tedy k podvozku, ale co ta high-tech výbava? Každý správný tank musí mít v prvé řadě optiku pro přesné vedení boje za všech podmínek. Nesmí tedy chybět kamera pro noční vidění, aby se mohl tankista orientovat i za úplné tmy a s vozidlem dojet tiše až k posteli oběti, která nic netuší.

Klepněte pro větší obrázek
Kamera s pětimegapixelovým snímačem a dvojicí infračervených reflektorů pro noční vidění. Všimněte si dvou růžových čidel. Jedná se o fotorezistory, které při nízkém osvětlení automaticky sepnou napájení reflektorů.

V našem případě tuto roli sehraje širokoúhlá pětimegapixelová kamera se snímacím čipem OV5647, IR filtrem a dvojicí pomocných IR reflektorů. Zatímco během dne bude kamera podávat obraz v relativně přirozených barvách (IR filtr lze případně odmontovat), jakmile se setmí, fotorezistory na reflektorech automaticky sepnou jejich zářiče slabým načervenalým nádechem a kamera bude nadále zaznamenávat světlo výhradně v oboru infračervených vln – nyní už tedy jen černobíle.

Nepřítele zaměří dálkoměr

Tím však výbava nekončí. Pro přesné měření musí mít každý tank k dispozici nějaký dálkoměr. Náš bude mít základní ultrazvukový dálkoměr HC-SR04 s širokým zorným polem a především ve variantě HC-SR04P, kterou lze přímo připojit k řídícímu počítači s 3,3V logikou.

Klepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázek
Na čele tanku není pouze kamera, ale také ultrazvukový a laserový (VCSEL) dálkoměr

Vedle něj bude nicméně vybavený ještě optickým dálkoměrem s čipem VL53L0X, který k měření používá 940nm nízkoenergetický laserový paprsek VCSEL. Vzdálenost k překážce změří rychleji a přesněji než ultrazvukový snímač za dolar, ale nemá tak široké zorné pole, takže není vhodný pro detekci překážek – měří jen bod přímo před sebou.

Oba dálkoměry zvládnou změřit překážku ve vzdálenosti okolo 3 metrů, i když záleží na typu plochy, od které se odráží zvuková vlna, respektive paprsek laseru.

Mozkem bude Raspberry Pi 3 Model B

Aby toto všechno mohlo fungovat, jádrem celé legrace nebude nic jiného než staré dobré Raspberry Pi 3 Model B. Český RPishop.cz jej nyní prodává za necelých devět stovek, což je podle Heuréky aktuálně nejlepší cena na věrohodných tuzemských e-shopech.

Klepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázek
Mikropočítač Raspberry Pi 3 Model B asi netřeba příliš představovat. Možná jej máte u televizoru jako síťový přehrávač i vy.

Samotné Raspberry se sice postará o zprovoznění kamery a bezdrátový přenos obrazu ve formátu MJPEG do webového prohlížeče tankisty, nicméně není pochopitelně konstruované na ovládání pohonné jednotky.

Použité motory k chodu vyžadují alespoň 6 V, aby se vůbec rozjely, nicméně aby uvezly celý náklad a dokázaly změnit směr protichůdným otáčením, potřebují alespoň 9 V a v nouzi až několik ampérů elektrického proudu. Není se čemu divit, při zatáčení se na místě otáčejí relativně měkké gumové pásy, které mají při styku třeba s kobercem obrovské tření, které je třeba překonat.

H-můstek

K pohonu tedy potřebují naprosto klíčovou součástku každého sofistikovanějšího autíčka na dálkové ovládání: H-můstek (H-bridge). Už jsme si tu o něm před rokem povídali, když jsme stavěli primitivní autíčko na dálkové ovládání, takže jen zopakuji, že je to obvod, do kterého připojíte napájení elektromotoru, samotný elektromotor a obvod nakonec propojíte s řídícím počítačem.

Základní logika ovládání spočívá v tom, že zpravidla pomocí dvou vodičů roztáčíme elektromotor jedním, nebo naopak druhým směrem. Když na prvním vodiči nastavíme logickou jedničku (HIGH) a na druhém logickou nulu (LOW), motor se rozjedete ve směru hodinových ručiček, no a když to prohodíme, bude směr opačný.

H-můstky zároveň zpravidla podporují pulzně-šířkovou modulaci (PWM), pomocí které můžeme snižovat výkon motorů. I PWM jsme se věnovali vícekrát, takže jen zopakuji, že pomocí něj namísto spojitého napětí budeme na vodičích motoru nastavovat sled krátkých elektrických pulzů. Budeme vlastně na vodičích velmi rychle blikat. Čím delší bude fáze pulzu, tím více šťávy motor dostane a vykoná tedy více práce. Na Arduinu se o základní operace s PWM stará funkce analogWrite, ovšem na Raspberry Pi to bude trošku obšírnější.

RPi Motor Driver Board

Aby byl náš tank co nejkompaktnější, namísto ručně sestaveného obvodu s několika H-můstky třeba na nepájivém poli se o vše postará maličká (ale také mnohem dražší) rozšiřující deska pro Raspberry Pi, které se říká hat.

Internetové e-shopy takových hatů nabízejí desítky a my jsme zvolili Waveshare RPi Motor Driver Board za 29 dolarů. Na eBayi jej seženete i za lehce nižší částky, i tak se ale bude jednat zhruba o pětistovku. Rizika celního řízení se ale obávat nemusíte.

Klepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázek
RPi Motor Driver Board. Nabízí dva výkonné H-můstky pro dvojici motorů a zároveň vyrábí provozní napětí i pro samotné Raspberry Pi. Rozšiřující desku lze napájet ze 7-40V baterie.

Podstatné je to, že za tuto pálku (nejlevnější H-můstky koupíte za pár korun) získáte kompletní energetické řešení pro Raspberry Pi. Deska totiž nebude pohánět pouze motory, ale vyrobí i 5V napětí pro napájení samotného Raspberry, které si bude moci říci až o 2 A elektrického proudu.

Freescale MC33886 dodá motorům výkon

K samotnému pohonu motorů deska nabízí dva H-můstky Freescale MC33886 (Dnes NXP Semiconductors), z nichž si každý poradí s napětím 5-40 V a provozním proudem až 5 A, aniž by čip začal hořet. Desku samotnou je pak třeba napájet napětím alespoň 7 V kvůli step-down měničům na 5V pro běh mikropočítače. Jelikož jako zdroj použijeme 11V tříčlánkovou LiPo baterii, nebude to žádný problém.

Klepněte pro větší obrázek
Dvojice čipů H-můstku MC33886. Dále na desce najdete logický převodník 74LVC4245AD, napěťový měnič na 5V LM2596 a ochranné kondenzátory, diody a 2A pojistku, které zachovávají elektrickou stabilitu i při velké zátěži motorů.

Když desku skrze dvouřad pinů (pin header) připojíme k Raspberry, několik jich pro svoji logiku zaberou právě oba H-můstky. Které konkrétně, se dočtete na oficiální wikistránce hatu. Tyto piny tedy nesmíme na Raspberry používat k ničemu jinému, jinak bychom totiž omylem popojížděli tankem sem a tam, aniž bychom chtěli.

Rozšiřující hat má zároveň přepínač, kterým lze chytře zvolit, jestli má deska z baterie skutečně napájet i Raspberry, anebo tomu bude naopak a desku bude napájet mikropočítač z vlastního zdroje. To se hodí při vývoji, kdy je RPi připojené ke zdroji napětí skrze USB a my nebudeme zbytečně vybíjet baterii, když s tankem zrovna nejezdíme, ale chceme testovat, jestli deska funguje, jak má.

Klepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázek
Spodní strana tanku s maličkou 3S LiPo baterií a připojení napájení motorů a baterie do šroubových zdířek na rozšiřující desce. Zatímco kabeláž motorů je pro protékající proudy spíše limitní, kabely baterie jsou dimenzované na proudy v řádu až desítek ampérů. V našem případě to není třeba, deskou bude protékat nejvýše pár ampérů.

A pak je tu ještě jedna drobnost – IR přijímač, takže bychom mohli tank ovládat infračerveným dálkovým ovladačem (není součástí balení). To je však pro děti. Raspberry má přeci Wi-Fi a my chceme streamovat obraz z kamery a tank ovládat třeba z laptopu a gauče v jiné místnosti, takže veškerá komunikace poteče výhradně rádiovým signálem.

Jak se pracuje s piny GPIO v Pythonu

Tak, hardware bychom měli a teď už jen stačí vše oživit. Jelikož tank nepohání Arduino, namísto jeho jednoduchého C++ použijeme několik málo knihoven pro práci s piny Raspberry Pi a neméně jednoduchý jazyk Python, ve kterém si napíšeme kompletní ovládací aplikaci.

Pro ovládání GPIO pinů a tedy nastavování základních logických hodnot HIGH a LOW a pro práci s PWM použijeme populární knihovnu pro Python RPi.GPIO.

Klepněte pro větší obrázek
Klepněte pro větší obrázek
Pole pinů na Raspberry Pi 3 Model B. V našem kódu je budeme číslovat pomocí označení GPIOXX na obrázku níže a nebudeme si vůbec všímat značení na rozšiřující desce. Takže například GPIO17 = pin číslo 17. Grafika: Raspberry Pi Spy

Jelikož jsme se Pythonu v našem seriálu zatím věnovali jen sporadicky, ve stručnosti se podívejme, jak se v něm pracuje s GPIO a zmíněnou knihovnou oproti Arduinu.

V Arduinu bychom například na pinu číslo 4 nastavili logickou jedničku, a tedy 5 V, takto:

pinMode(4, OUTPUT);
digitalWrite(4, HIGH);

V případě Pythonu a knihovny RPi.GPIO nastavíme logickou jedničku, a tedy 3,3 V (armové čipy používají 3,3V logiku), zase takto:

import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(4, GPIO.OUT)
GPIO.output(4, 1)

Princip je tedy stejný, jen v případě Pythonu je kód o něco delší, protože musíme použít externí knihovnu a zvolit režim číslování jednotlivých pinů (to je ta funkce setmode).

Pro pořádek zmiňme ještě čtení logického stavu na pinu. V případě Arduina by to bylo opět:

pinMode(4, INPUT);
byte stav = digitalRead(4);

A v Pythonu s knihovnou RPi.GPIO:

import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(4, GPIO.IN)
stav = GPIO.input(4)

Fajn, jednoduché jako facka, viďte? Přepis základních programů z Arduina na Python je opravdu jednoduchý, jen je u některých úloh třeba myslet na pár rozdílů. Ačkoliv má Raspberry Pi 3 výkonný procesor, v některých operacích může být paradoxně pomalejší než drobný osmibitový real-time mikrokontroler. Může se to týkat třeba generování a čtení velmi přesného PWM signálu atp.

Klepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázek
Postupné připojení dalších periferií podle potřeby a našeho designu tanku. Všimněte si, že jsem nakonec připojil i bzučák, ačkoliv v programu jej nakonec nepoužívám. Mohu jej rozezvučet pomocí signálu PWM. O něm později.

Ovládáme motory v Pythonu

Ale zpět k našemu tanku. Má dva motory a dva H-můstky, které na Raspberry Pi obsadí piny 6, 12, 13, 20, 21 a 26. Zatímco piny 12 a 26 slouží k aktivací motorů a případně nastavení rychlosti, piny 6, 13, 20 a 21 k nastavení směru otáček.

Otáčky prvního motoru ovládají piny 20 a 21, otáčky druhého piny 6 a 13.

Aby se tedy první motor roztočil jedním směrem, musíme jej nejprve aktivovat a na pinu 26 nastavit logickou jedničku (už víte jak). Poté musíme nastavit logickou jedničku ještě na pinu 20 a naopak logickou nulu na pinu 21. Když tyto hodnoty prohodíme, bude se motor otáčet opačným směrem, a když na všech řídících pinech nastavíme logickou nulu, zastaví se.

Pro přehlednost se podívejme na tabulku pro první motor:

Pin 26 (aktivační) Pin 20 (směrový) Pin 21 (směrový) Výsledek
0 Motor je vypnutý
1 Motor je zapnutý
1 1 0 Otáčí se dopředu
1 0 1 Otáčí se dozadu
0/1 0 0 Neotáčí se

H-můstek podle logiky na těchto třech pinech usoudí, jakou má nastavit polaritu ve zdířkách, do kterých je připojený motor, a podle toho se tedy motor roztočí zvoleným směrem.

Jak změnit rychlost motoru

Jak už jsem napsal výše, většina H-můstků umožňuje použít namísto spojitého logického stavu 1/0 i pulzně-šířkovou modulaci. V případě našeho hatu s čipy MC33886 tak můžeme učinit na aktivačních pinech 26 a 12 pro každý z motorů.

To znamená, že kdybychom chtěli prvnímu motoru snížit příkon zhruba na polovinu, namísto logické jedničky na pinu 26 nastavíme pulzující PWM signál, který bude složený ze stejně dlouhých fází 0 a 1. H-můstek pak bude ve zdířce napájení motoru nastavovat napětí jen po dobu jedničkové fáze, a motor tak v ideálním případě vykoná za jednotku času jen polovinu práce, což se projeví snížením rychlosti.

Nutno podotknout, že na papíře je to snazší než ve skutečnosti, takže až vlastní praxí odhalíte, kdy už je příkon tak malý, že se tank vůbec nerozjede, ale bude stát. To záleží na jeho hmotnosti, povrchu, po kterém se pohybuje a samozřejmě i odporu kol. V případě mého pásového tanku se odmítl rozjet při snížení příkonu asi na 20-30 %.

Hrátky s PWM způsobují u elektromotorů jednu nepříjemnou věc. Při určitých frekvencích a poměru délky fáze 1 a 0 začne jejich vinutí pískat – promění se vlastně v primitivní bzučák. Tento neduh lze vyřešit změnou pracovní frekvence PWM signálu, přičemž je opět třeba zkoušet a vlastním experimentováním sledovat, jak motor na změny reaguje.

Teď si těch pár odstavečků pojďme opět přetavit do jazyka Python. Pomocí několika řádků kódu změníme frekvenci PWM na 50 Hz a poté na půl sekundy roztočíme motor jedním směrem a na půl sekundy opačným při 75% příkonu/rychlosti:

# Knihovny pro funkci sleep, která plní roli delay v Arduinu, a pro práci s piny GPIO
import RPi.GPIO as GPIO
from time import sleep

# Nastavení GPIO pinů
GPIO.setmode(GPIO.BCM)
GPIO.setup(26, GPIO.OUT)
GPIO.setup(20, GPIO.OUT)
GPIO.setup(21, GPIO.OUT)

# Nastavení PWM na frekvenci 50 Hz
prikon_motoru = GPIO.PWM(26, 50)
# Nastavení PWM na 75 %
prikon_motoru.start(75)

# Otáčení jedním směrem
GPIO.output(20, 1)
GPIO.output(21, 0)
sleep(0.5)

# Otáčení druhým směrem
GPIO.output(20, 0)
GPIO.output(21, 1)
sleep(0.5)

# Zastavení motorů
GPIO.output(20, 0)
GPIO.output(21, 0)

# Nastavení všech použitých pinů GPIO do výchozí pozice
prikon_motoru.stop()
GPIO.cleanup() 

A to je celé. Pak už jen stačí podobným způsobem ovládnout druhý motor, zkoordinovat pohyby a napsat si funkce pro jízdu vpřed, vzad, vlevo a vpravo protichůdným otáčením motorů a tak dále.

Stejně tak můžeme napsat funkci pro změření vzdálenosti ultrazvukovým dálkoměrem, kdy na jeho pin TRIG nejprve pošleme kratičký aktivační pulz a poté čekáme, až se objeví kladný pulz na pinu ECHO, jehož délka bude odpovídat letu ultrazvukové vlny k překážce a zpět. Pomocí známé rychlosti šíření zvuku ve vzduchu si už pak snadno spočítáme hrubou vzdálenost.

A pak už jen taková drobnost... Zbytek celé aplikace

Největší legrace bude samozřejmě s kamerou a zprovozněním HTTP a WebSocket serveru. V tanku vlastně poběží tři servery. Na portu 8000 bude skrze HTTP a ve formátu MJPEG streamovat obraz připojená kamera. Proč MJPEG? Protože jeho zprovoznění je relativně jednoduché a tomuto formátu rozumí každý moderní prohlížeč. Proto jej používá nejedna bezpečnostní IP kamera s webovým rozhraním.

V HTML kódu stránky, pomocí které budeme tank ovládat, zároveň stačí použít značku IMG:

<img src="http://ip-adresa-tanku:8000/video.mjpeg" />

A namísto statického snímku se v něm bude streamovat obraz z kamery.

Obnovovací frekvence nebude příliš vysoká a procesor prohlížeče (i Raspberry) se trošku zapotí, nicméně v kódu můžeme libovolně snížit rozměry streamovaného obrazu a také jeho datový tok, nebo naopak obnovovací frekvenci. Nebude to H.264 nebo dokonce H.265, ale bude to fungovat.

Klepněte pro větší obrázekKlepněte pro větší obrázek
Kamera za umělého osvětlení v podvečerní kanceláři a poté, co jsem všechna světla zhasl. V tu chvíli vypadne barevná složka a kamera získává jen data o předmětech, které ozářily infračervené reflektory. Na druhém snímku tedy byla v kanceláři naprostá tma.

O běh dalších dvou serverů se postará populární knihovna pro Python Tornado, která zprovozní nejen HTTP server na portu 8888, který do prohlížeče pošle HTML stránku ovládacího pultíku s tlačítky, ale zároveň na témže portu zprovozní WebSocket server. WebSocketu jsme se v našem seriálu věnovali už před pár týdny, když jsme skrze něj do prohlížeče streamovali kresbičku na dotykovém TFT displeji připojeném k Wi-Fi mikrokontroleru ESP8266.

Jen tedy zrekapituluji, že WebSocket je technologie z široké rodiny HTML5, kterou dnes podporuje většina moderních prohlížečů. Podstatné je to, že pomocí ní může server v jednom spojení a oboustranně komunikovat s prohlížečem.

O rychlou komunikaci s minimální latencí se postará WebSocket

Laicky řečeno, když se v tanku něco stane, skrze WebSocket o této události informuje připojeného klienta (prohlížeč) a ten ji pomocí Javascriptu zpracuje. V našem případě nám bude tank každých 500 ms posílat údaj o změřené vzdálenosti a každých 10 sekund odešle neméně důležitý údaj – sílu signálu Wi-Fi (RSSI). Díky tomu budeme vědět, že se už pomalu dostáváme z dosahu a měli bychom to otočit.

Klepněte pro větší obrázek
Spuštění video přenosu a hlavní aplikace skrze SSH terminál

HTML stránka ovládacího pultu zároveň nabídne čtyři tlačítka pro jízdu vpřed, vzad, otáčení doleva a doprava. Když některé z nich myší stiskneme, skrze tentýž WebSocket se do tanku odešle patřičná instrukce a ten zareaguje. Jakmile stisk tlačítka ukončíme, prohlížeč tanku odešle instrukci k zastavení.

Klepněte pro větší obrázek
Ovládací rozhraní tanku. V levém horním rohu se každých deset sekund aktualizuje údaj o síle signálu Wi-Fi mezi tankem a redakčním Wi-Fi hotspotem. Uprostřed nahoře se každých 500 ms aktualizuje údaj z dálkoměru, který pokrývá oblast uprostřed obrazovky. Vpravo nahoře je posuvník pro nastavení příkonu motorů a konečně dole čtveřice ovládacích tlačítek pro samotnou jízdu.

Jak se přesně programuje takový server a klient, rozebírat nebudeme, článek by byl delší než jedna hutná bakalářská práce, nicméně na konci najdete kompletní a opět komentovaný kód.

(Ano, já vím, slíbil jsem, že jej konečně začnu publikovat na GitHubu, nepřítelem je mi však příliš rychlá rotace planety Země a z toho pramenící permanentní nedostatek času. Prosím tedy o trpělivost a čas na důkladnou očistu zejména kódů ze starších dílů našeho seriálu.)

Kamera a jednoduchý (ale zrádný) MJPEG

Přesto se ještě podívejme alespoň na základní práci s kamerou. Použili jsme kameru, kterou lze pomocí adekvátního plochého FFC kabelu připojit do konektoru CSI (Camera Serial Input). Poté je třeba toto rozhraní kamery aktivovat pomocí aplikace raspi-config, který je součástí operačního systému Raspbian (mimochodem, tank nepotřebuje grafický desktop, a tak jsem zvolil ořezanou verzi Raspbian Stretch Lite)

Jelikož dnes vše píšeme v Pythonu, pro práci s kamerou jsem použil skvělou a jednoduchou knihovnu Picamera. Už jsme s ní před rokem pracovali, a tak jen připomenu tyto dva články ke studiu:

Program, který by kameru spustil, počkal dvě sekundy, ať má expozimetr dostatek času, pořídil snímek a uložil jej do souboru ve formátu JPEG, by vypadal takto:

# Knihovny pro práci s časem a kamerou
from time import sleep
import picamera

print("Pořizuji snímek")
kamera = picamera.PiCamera()
# Nastavení rozlišení kamery
kamera.resolution = (1024, 768)
# Počkat 2 sekundy, ať má expozimetr dostatek času
sleep(2)
# Uložení do souboru JPEG
kamera.capture("fotka.jpg")
print("Hotovo!")

Program, který bude mít za úkol streamovat připojeným klientům obraz ve formátu MJPEG, bude v principu podobný, i když mnohonásobně větší, protože bude obsahovat kód samotného HTTP serveru.

My se ve stručnosti podíváme jen na ty řádky kódu, kdy spouštíme kameru a předáváme její obraz streamovacímu serveru. Právě zde totiž můžeme nastavit všechny parametry obrazu. Dále v kódu se již jedná pouze o tok jednotlivých rámů videa.

with picamera.PiCamera(resolution="640x480", framerate=10) as kamera:
    kamera.rotation = 180
    output = StreamingOutput()
    kamera.start_recording(output, format="mjpeg", bitrate=1000000)

Na prvním řádku si všimněte parametru resolution. Můžeme samozřejmě nastavit jakékoliv, vždyť kamera má 5megapixelový čip, nicméně aby byl tok co nejstabilnější, nastavíme relativně nízké rozlišení, které si až sám prohlížeč roztáhne na celou obrazovku.

Druhým parametrem je framerate – snímkovací frekvence. Namísto 24 snímků za sekundu jsme ji snížili na 10 fps. Obraz sice nebude tak jemný, ale dramaticky se sníží datový tok – na polovinu!

Nízké rozlišení a frekvence MJPEG má opravdu svůj důvod, samotný stream se totiž skládá z proudu plnohodnotných snímků – každý z nich je klíčový. Vlastně nebudeme příliš daleko od pravdy, když si to celé představíme jako proud jednotlivých fotek z kamery. Pro Raspberry Pi může být tento způsob kódování a přenosu videa výhodný, protože je to výpočetně jednodušší než kódování třebas H.264.

Naopak ostatní formáty přenášejí video tak, že klíčový snímek pošlou jen občas a po zbytek času odesílají jen složitě dopočítávaná rozdílová data. Při nestabilitě spojení se sice kvůli tomu v obrazu vyskytují nejrůznější artefakty a deformace (už dlouho nedorazil plnohodnotný keyframe, nebo je sám poškozený), ale na druhou stranu to celé zabere mnohem méně dat.

V iniciační sekvenci jsme dále otočili obraz o 180° a to kvůli plochému kabelu kamery a jejímu nutnému otočení při montáži. Nechtěli jsme tenký kabel příliš překrucovat a pro procesor Raspberry Pi nebude otočení žádný citelný problém.

A nakonec ten poslední řádek, ve kterém spouštíme samotné nahrávání do streamu. Vedle formátu kódování si všimněte parametru bitrate, tedy nastavení výstupního datového toku v b/s. Nastavili jsme 1 Mb/s, což by v případě H.264 stačilo na solidní obraz i při vyšším rozlišení, ale jak už jsme si řekli, MJPEG funguje úplně jinak.

Klepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázekKlepněte pro větší obrázek
Různé úrovně datového toku od 500 kb/s po 2 Mb/s u MJPEG s rozlišením 640×480 pixelů a snímkovací frekvencí 10 fps

Hranice není pevně daná a musíte si sami vyzkoušet, při jakém datovém toku se už obraz rozpadne na pár kostiček, nebo naopak bude zbytečně vysoký, aniž by se jakkoliv zvyšovala kvalita.

Jelikož náš tank komunikuje skrze Wi-Fi, co nejnižší datová zátěž pomůže tomu, abychom jej mohli ovládat i na větší vzdálenost.

V každém případě, pokud by nám nešlo o datový tok a chtěli bychom obraz přenášet v nejvyšší kvalitě, namísto parametru bitrate použijeme quality, který v případě kódování MJPEG může nabývat hodnot 0 až 100. Datový tok se pak dopočítá podle něj.

Je hotovo, pojďme se podívat na video

Tak, a to je celé. Takže si to shrňme. Dnes jsme si ukázali, jak na Raspberry Pi a s pomocí H-můstků ovládat 9V motory a rozjet tank sem a tam. Zároveň jsme si ukázali, jak skrze HTTP do webového prohlížeče přenášet živý obraz z kamery pomocí kódování MJPEG.

A co je nejdůležitější, pomocí technologie WebSocket jsme zprovoznili asynchronní oboustranné spojení mezi prohlížečem a Raspberry Pi, na obrazovce monitoru se tedy každých 500 ms aktualizují data z dálkoměru a tank také okamžitě zareaguje, pokud klepneme na plyn.

První provozní test, který odhalil nové chyby:

V závěru videa si všimněte, že když Wi-Fi signál klesl na -80 dBm (mezi tankem a Wi-Fi AP byly čtyři stěny redakčních toalet a kuchyňky), reálná přenosová rychlost klesla pod 1 fps. Tank by potřeboval dle kvality signálu variabilně měnit přenosové parametry. A o tom to celé je. Cílem totiž není hračka samotná, ale řešení problému jako takového a studium nových věcí. Samotné bastlení je cíl.

A teď opravdu ty kódy

Teď už zbývá pouze samotný kód. Pro zprovoznění kamery a vytvoření MJPEG streamu jsme použili tento skript Dava Jonese z GitHubu. Jedná se o jeden jediný soubor, k jehož chodu nepotřebujete vedle PiCamery žádnou další externí knihovnu. O zbytek se postarají ty vestavěné, které jsou součástí instalace Pythonu.

Funkci tohoto skriptu bychom samozřejmě mohli implementovat do hlavní aplikace, mně se ale docela zamlouvá, že mám oddělený skript video.py, který mohu kdykoliv spustit i na jakémkoliv jiném Raspberry Pi s kamerou a za jiným účelem.

Skript můžeme v SSH terminálu bezpečně spustit na pozadí pomocí nohup:

nohup python3 video.py &

Poběží tak dlouho, dokud jeho proces neukončíme. Anebo jej proměníme v linuxovou službu a může se pak automaticky spouštět hned po startu Raspberry Pi.

V každém případě, poté konečně spustíme ovládací program aplikace.py, ve kterém je vše ostatní. Na závěr jen připomenu, že všechny kódy píšu v Pythonu 3.x, takže některé jeho části nemusejí být kompatibilní s Pythonem 2.x.

Tak a teď už opravdu ten kód. Příjemnou četbu!

Kód hlavního programu aplikace.py

from tornado import websocket, web, ioloop
from time import sleep
import RPi.GPIO as GPIO
from time import time
import os
import re

# Výchozí frekvence PWM (50 Hz)
FREQ = 50 

# TCP port pro HTTP a WebSocket servery
TCP_PORT = 8888

# Piny levého motoru
GPIO_ML0 = 20
GPIO_ML1 = 21

# Piny pravého motoru
GPIO_MR0 = 6
GPIO_MR1 = 13

# Piny ENABLE/PWM motorů
GPIO_ENABLE_L = 26
GPIO_ENABLE_R = 12

# Pin bzučáku
GPIO_BUZZER = 5

# Piny ultrazvukového dálkoměru
GPIO_TRIGGER = 16
GPIO_ECHO = 19

# Nezobrazuj varování
GPIO.setwarnings(False)

# Nastavení adresování pinů
GPIO.setmode(GPIO.BCM)

# Nastavení směrů jednotlivých pinů
GPIO.setup(GPIO_ML0, GPIO.OUT)
GPIO.setup(GPIO_ML1, GPIO.OUT)
GPIO.setup(GPIO_MR0, GPIO.OUT)
GPIO.setup(GPIO_MR1, GPIO.OUT)
GPIO.setup(GPIO_ENABLE_L, GPIO.OUT)
GPIO.setup(GPIO_ENABLE_R, GPIO.OUT)
GPIO.setup(GPIO_TRIGGER, GPIO.OUT)
GPIO.setup(GPIO_ECHO, GPIO.IN)
GPIO.setup(GPIO_BUZZER, GPIO.OUT)

# Nastaveni PWM frekvence ovládání rychlosti na 50 Hz
pwm_L = GPIO.PWM(GPIO_ENABLE_L, FREQ)
pwm_R = GPIO.PWM(GPIO_ENABLE_R, FREQ)

# Nastavení motorů na jízdu vpřed
def dopredu():
    GPIO.output(GPIO_ML0, 1)
    GPIO.output(GPIO_ML1, 0)
    GPIO.output(GPIO_MR0, 1)
    GPIO.output(GPIO_MR1, 0)

# Nastavení motorů na jízdu vzad
def dozadu():
    GPIO.output(GPIO_ML0, 0)
    GPIO.output(GPIO_ML1, 1)
    GPIO.output(GPIO_MR0, 0)
    GPIO.output(GPIO_MR1, 1)

# Nastavení motorů na jízdu vlevo
def doleva():
    GPIO.output(GPIO_ML0, 1)
    GPIO.output(GPIO_ML1, 0)
    GPIO.output(GPIO_MR0, 0)
    GPIO.output(GPIO_MR1, 1)

# Nastavení motorů na jízdu vpravo
def doprava():
    GPIO.output(GPIO_ML0, 0)
    GPIO.output(GPIO_ML1, 1)
    GPIO.output(GPIO_MR0, 1)
    GPIO.output(GPIO_MR1, 0)

# Zastavení motorů
def brzda():
    GPIO.output(GPIO_ML0, 0)
    GPIO.output(GPIO_ML1, 0)
    GPIO.output(GPIO_MR0, 0)
    GPIO.output(GPIO_MR1, 0)

# Nastavení PWM rychlosti
def rychlost(hodnota):
    pwm_L.ChangeDutyCycle(hodnota)
    pwm_R.ChangeDutyCycle(hodnota)

# Vypnutí motorů
def vypnout():
    pwm_L.stop()
    pwm_R.stop()

# Nastartování motorů
def nastartovat():
    pwm_L.start(100)
    pwm_R.start(100)

# Nekonečná smyčka dálkoměru
def loop_dalkomer():
    vzdalenost = zmerit_vzdalenost()
    websocket_odesli_zpravu("D" + str(vzdalenost))    

# Nekonečná smyčka kvality signalu
def loop_signal():
    signal = zjistit_silu_wifi("wlan0")
    websocket_odesli_zpravu("W" + str(signal)) 

# Odeslání zprávy všem připojeným WebSocket klientům
def websocket_odesli_zpravu(zprava):
    # Pokud je v seznamu nějaký klient, pošli mu zprávu
    for websocket_klient in websocket_klienti:
        websocket_klient.write_message(zprava)

# Změření vzdálenosti ultrazvukovým senzorem HC-SR04
def zmerit_vzdalenost():
    GPIO.output(GPIO_TRIGGER, 1)
    sleep(.00001)
    GPIO.output(GPIO_TRIGGER, 0)
    start_mereni = time()
    stop_mereni = time()
    while GPIO.input(GPIO_ECHO) == 0:
        start_mereni = time()
    while GPIO.input(GPIO_ECHO) == 1:
        stop_mereni = time()
    delta = stop_mereni - start_mereni
    vzdalenost = (delta * 34300) / 2
    return int(vzdalenost)

# Zjištění síly signálu pomocí linuxových příkazů a regulárních výrazů
def zjistit_silu_wifi(interface):
    radek = os.popen('iwconfig ' + interface + ' | grep "Signal level"').read().strip()
    hodnoty = re.findall(r"(-\d+)\s*dBm", radek)
    return int(hodnoty[0])

# Zjištění IP adresy pomocí linuxových příkazů a regulárních výrazů
def zjistit_ip(interface):
    radek = os.popen("ip a show " + interface + "| grep inet").read().split("\n")[0].strip()
    ips = re.findall(r"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})", radek)
    return ips[0]

# HTTP server
class HttpIndex(web.RequestHandler):
    # Pokud se připojí klient, pošli mu soubor index.html s kódem ovládacího rozhraní
    def get(self):
        self.render("index.html")

# WebSocket server
class SocketSpojeni(websocket.WebSocketHandler):
    # Pokud se připojí klient, ulož ho do seznamu
    def open(self):
        websocket_klienti.append(self)
        self.write_message("Vítej tankisto! Co tak dnes zničit svět?")
        self.write_message("W" + str(zjistit_silu_wifi("wlan0")))
        # Pokud je to prvni klient, nastartuj motor
        if len(websocket_klienti) == 1:
            nastartovat()

    # Pokud se odpojí klient, odstraň jej ze seznamu
    def on_close(self):
        websocket_klienti.remove(self)
        # Pokud se odpojili všichni klienti, vypni motor
        if len(websocket_klienti) == 0:
            vypnout()

    # Pokud od klienta dorazila nějaká zpráva
    def on_message(self, zprava):
        # Znaky F, B, L, R a X slouží k řízení
        if zprava == "F":
            dopredu()
            self.write_message("Tank: Potvrzuji směr: F")
        elif zprava == "B":
            dozadu()
            self.write_message("Tank: Potvrzuji směr: B")
        elif zprava == "L":
            doleva()
            self.write_message("Tank: Potvrzuji směr: L")
        elif zprava == "R":
            doprava()
            self.write_message("Tank: Potvrzuji směr: R")
        elif zprava == "X":
            brzda()
            self.write_message("Tank: Potvrzuji směr: X")
        # Pokud zpráva obsahuje třeba S96, 
        # nastav rychlost na 96 %
        elif zprava[:1] == "S":
            rychlost(int(zprava[1:]))
            self.write_message("Tank: Potvrzuji rychlost: " + zprava[1:] + " %")
        else:
            self.write_message("Tank: Neznámá zpráva")  

# Pokud je URL "/", odešli klientovi index.html
# Pokud je URL "/ws", zpracuj WebSocket klienta 
app = web.Application([
    (r'/', HttpIndex),
    (r'/ws', SocketSpojeni)
])

# Běh programu
if __name__ == "__main__": 

    # Pole pro WebSocket klienty
    websocket_klienti = []

    try:
        # Zjisti moji IP adresu na rozhraní WLAN0
        ip = zjistit_ip("wlan0")

        # Zjisti sílu signálu k Wi-Fi AP
        signal = zjistit_silu_wifi("wlan0")

        print("**** TANK BOBIK T19 ****")
        print("Síla signálu Wi-Fi: {} dBm".format(signal))
        print("Webové rozhraní tanku: http://{}:{}\r\n".format(ip, TCP_PORT))
        print("Nezapomeň spustit video server: nohup python3 video.py &")
        # HTTP a WebSocket poslouchají na portu 8888
        app.listen(TCP_PORT)
        
        # Plánovač každých 500 ms spouští funkci pro zjištění a odeslání vzdálenosti
        planovac_dalkomer = ioloop.PeriodicCallback(loop_dalkomer, 500)
        planovac_dalkomer.start()

        # Plánovač každých 10 s spouští funkci pro zjištění a odeslání kvality signálu
        planovac_signal = ioloop.PeriodicCallback(loop_signal, 10000)
        planovac_signal.start()

        # Hlavni smyčka programu, která drží program při životě, aby neskončil
        smycka = ioloop.IOLoop.current()
        smycka.start()

    except:
        # V případě chyby, systémového/klávesového přerušení (CTRL+C)
        # zastav plánovače a hlavní smyčku, vypni motory a ukonči program
        planovac_dalkomer.stop()
        planovac_signal.stop()
        smycka.stop()
        brzda()
        vypnout()
        GPIO.cleanup()
        print("\r\nUkončuji program! Fňuk :-(")

Kód ovládací HTML stránky index.html:

<!DOCTYPE html>
<html>
<head>
    <title>Bobik T19</title>
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
    <link href="https://fonts.googleapis.com/css?family=Oswald&amp;subset=latin-ext" rel="stylesheet">
    <style>
        body{
            margin: 0;
            padding: 0;
            overflow:hidden;
        }
        #video{
            width:100%;
            height:100%;
            margin:0;
            padding:0;
            position: absolute;
            z-index: 10;
        }

        #vzdalenost{
            position: absolute;
            opacity: 0.9;
            z-index: 99;
            background: black;
            color: white;
            font-family: 'Oswald', sans-serif;
            font-size: 40px;
            text-align: center;
            padding: 10px;
            border-radius: 0 0 15px 15px;
            top: 0;
            left: 50%;
            transform: translate(-50%, 0%);
            width: 200px;
            height: 80px;
        }
        #signal{
            position: absolute;
            opacity: 0.9;
            z-index: 99;
            background: black;
            color: white;
            font-family: 'Oswald', sans-serif;
            font-size: 40px;
            text-align: center;
            padding: 10px;
            border-radius: 0 0 15px 0;
            top: 0;
            left: 0;
            width: 200px;
            height: 80px;
        }
        #rychlost{
            position: absolute;
            opacity: 0.9;
            z-index: 99;
            background: black;
            color: white;
            font-family: 'Oswald', sans-serif;
            font-size: 20px;
            text-align: center;
            padding: 10px;
            border-radius: 0 0 0 15px;
            top: 0;
            right: 0;
            width: 200px;
            height: 80px;
        }
        #dopredu{
            position: absolute;
            opacity: 0.9;
            z-index: 99;
            background: black;
            color: white;
            font-family: 'Oswald', sans-serif;
            font-size: 40px;
            text-align: center;
            padding: 10px;
            border-radius: 15px 15px 15px 15px;
            bottom: 200px;
            left: 50%;
            transform: translate(-50%);
            width: 100px;
            height: 80px;
            font-size: 20px;
            cursor: pointer;
        }
        #dozadu{
            position: absolute;
            opacity: 0.9;
            z-index: 99;
            background: black;
            color: white;
            font-family: 'Oswald', sans-serif;
            font-size: 40px;
            text-align: center;
            padding: 10px;
            border-radius: 15px 15px 15px 15px;
            bottom: 80px;
            left: 50%;
            transform: translate(-50%);
            width: 100px;
            height: 80px;
            font-size: 20px;
            cursor: pointer;
        }
        #doleva{
            position: absolute;
            opacity: 0.9;
            z-index: 99;
            background: black;
            color: white;
            font-family: 'Oswald', sans-serif;
            font-size: 40px;
            text-align: center;
            padding: 10px;
            border-radius: 15px 15px 15px 15px;
            bottom: 150px;
            left: 50%;
            transform: translate(-200px);
            width: 100px;
            height: 80px;
            font-size: 20px;
            cursor: pointer;
        }
        #doprava{
            position: absolute;
            opacity: 0.9;
            z-index: 99;
            background: black;
            color: white;
            font-family: 'Oswald', sans-serif;
            font-size: 40px;
            text-align: center;
            padding: 10px;
            border-radius: 15px 15px 15px 15px;
            bottom: 150px;
            left: 50%;
            transform: translate(80px);
            width: 100px;
            height: 80px;
            font-size: 20px;
            cursor: pointer;
        }
    </style>
    <script>
        $(function() {
            console.log("Stránka načtena!");

            var nebezpecna_vzdalenost = 20;
            var ws = new WebSocket("ws://" + window.location.hostname + ":8888/ws");

            $("#video").attr("src", "http://" + window.location.hostname + ":8000/video.mjpeg");
        
            $("#rychlost_posuvnik").on("change", function() {
                $("#rychlost_txt").html($(this).val() + " %");
                if(ws){
                    console.log("Prohlížeč: Nová rychlost: " + $(this).val() + " %");
                    ws.send("S" + $(this).val());
                }
            });

            $("#dopredu").mousedown(function() {
                console.log("Prohlížeč: F");
                ws.send("F");
            });

            $("#dopredu").mouseup(function() {
                console.log("Prohlížeč: X");
                ws.send("X");
            });

            $("#dozadu").mousedown(function() {
                console.log("Prohlížeč: B");
                ws.send("B");
            });

            $("#dozadu").mouseup(function() {
                console.log("Prohlížeč: X");
                ws.send("X");
            });

            $("#doleva").mousedown(function() {
                console.log("Prohlížeč: L");
                ws.send("L");
            });

            $("#doleva").mouseup(function() {
                console.log("Prohlížeč: X");
                ws.send("X");
            });

            $("#doprava").mousedown(function() {
                console.log("Prohlížeč: R");
                ws.send("R");
            });

            $("#doprava").mouseup(function() {
                console.log("Prohlížeč: X");
                ws.send("X");
            });

            ws.onmessage = function (udalost) {
                if(udalost.data.substring(0, 1) == "D"){
                    var vzdalenost = parseInt(udalost.data.substring(1));
                    $("#vzdalenost").html(vzdalenost + " cm");
                    if(vzdalenost < nebezpecna_vzdalenost){
                        $("#vzdalenost").css("color", "red");
                    }
                    else{
                        $("#vzdalenost").css("color", "aquamarine");
                    }
                }
                else if(udalost.data.substring(0, 1) == "W"){
                    var signal = parseInt(udalost.data.substring(1))
                    $("#signal").html(signal + " dBm");
                }
                else{
                    console.log(udalost.data);
                }
            };
        });
    </script>
</head>
<body>
    <img id="video" width="640" height="480" />
    <div id="vzdalenost">0 cm</div>
    <div id="signal">0 dBm</div>
    <div id="rychlost"><span id="rychlost_txt">100 %</span><br /><input type="range" id="rychlost_posuvnik" min="0" max="100"></div>
    <div id="dopredu">▲ Dopředu</div>
    <div id="dozadu">▼ Dozadu</div>
    <div id="doleva">◄ Doleva</div>
    <div id="doprava">► Doprava</div>
</body>
</html>

Pokud jste celý článek poctivě dočetli až sem a nic jste nevynechali, máte u mě pivo :-)

Diskuze (19) Další článek: Proč všichni chtějí do škol, proč Apple zlevňuje a proč se blíží konec tabletů s Androidem

Témata článku: Pojďme programovat elektroniku, Programování, Programování pro děti, Stavebnice, Raspberry Pi, Python, Bastlení, Bezpečnostní IP, Řídící počítač, Hut, Základní logika, WebSocket, SourceForge, Jones, Tenký kabel, Facka, Linuxový příkaz, Základní operace, Trpělivost, Oswald, Lipo baterie, Sans serif, Pracovní frekvence, Záchvat, Redakční wi-fi


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

Vylaďte si Wi-Fi: Podívejte se, jaká pásma máte doma volná a kam signál nedosáhne

Vylaďte si Wi-Fi: Podívejte se, jaká pásma máte doma volná a kam signál nedosáhne

** Prozkoumejte, jaké pásmo je pro vaši síť nejlepší ** Díky heat mapě snadno poznáte, kde to bude se signálem horší ** Vše zvládnete i z mobilního telefonu

Vladislav Kluska | 36

Američtí mariňáci si tisknou kasárna z betonu na 3D tiskárně

Američtí mariňáci si tisknou kasárna z betonu na 3D tiskárně

** Americká námořní pěchota nedávno představila 3D tištěná kasárna pro vojáky ** Ty jim tiskne velká 3D tiskárna na beton ** Výsledkem je solidní obytný prostor, který je slušně chráněný před nepřátelskou palbou

Stanislav Mihulka | 18


Aktuální číslo časopisu Computer

Jak vytvořit a spravovat vlastní web

Velký test herních klávesnic a DVB-T2 tunerů

Vše o formátu RAW

Vybíráme nejlepší základní desku