Moje odkazy
Obsah článku:
vydáno: 12. 11. 2016 20:16, aktualizováno: 26. 12. 2023 00:35
Univerzální vstupně/výstupní piny (GPIO) můžeme ovládat pomocí zvláštních knihoven v různých programovacích jazycích (Java, Ruby, Python, C atd.). Tyto knihovny ale nejsou nutné – ukážeme si jednodušší postup.
V souladu s unixovou (resp. Plan 9) filosofií „všechno je soubor“ budeme s GPIO pracovat jako se soubory – čtení hodnoty pinu (např. dveřního čidla nebo tlačítka) znamená tedy čtení souboru. A nastavení hodnoty pinu (např. rozsvícení/zhasnutí LEDky) je zase obyčejný zápis do souboru.
Předně je potřeba říct, že se nejedná o soubory fyzicky uložené na pevném disku – při práci s GPIO tak nedochází ke čtením a zápisům na váš HDD/SSD disk. Souborový systém totiž může zpřístupňovat virtuální soubory a adresáře – pro uživatele je to transparentní – pracuje s nimi stejně, bez ohledu na to, jestli jsou data na pevném disku, vypočítávají se za běhu (např. spojením dvou jiných souborových systémů nebo třeba de/šifrováním) nebo se získávají ze sítě (NFS, SSHFS, WebDAV atd.) nebo interakcí se speciálním HW jako je GPIO.
Soubory, které nás zajímají, se nacházejí v adresáři /sys/class/gpio/
a poskytuje nám je syntetický souborový systém sysfs
:
$ df /sys/class/gpio/ Filesystem Size Used Avail Use% Mounted on sysfs 0 0 0 - /sys
Než začneme se čtením a zápisem, připomeňme si, co GPIO znamená.
/dev/ttyS*
) a částečně se podobá paralelnímu portu (LPT), který se sice používal hlavně pro připojení tiskáren, ale v zásadě jde o univerzální rozhraní – máme tam osm datových pinů, na kterých si můžeme nastavovat 1 a 0 podle svého + vstupní piny a dokonce i vstupně/výstupní.Před použitím musíme tedy GPIO rozhraní nastavit a říct systému, v jakém směru chceme který pin používat. A to uděláme – jak jinak než – zápisem do souborů:
PIN=8
# otevřeme pin pro čtení:
echo $PIN > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio$PIN/direction # in=čtení out=zápis
# čteme hodnotu:
cat /sys/class/gpio/gpio$PIN/value
# ukončíme práci:
echo $PIN > /sys/class/gpio/unexport
Hodnoty čteme a zapisujeme v ASCII jako 0 a 1. Jde tedy o jakýsi textový „protokol“ se kterým můžeme snadno pracovat z příkazové řádky a nemusíme kódovat nějaké bajty.
Pro pohodlnější práci jsem si vytvořil sadu shellových funkcí GPIO.sh. Není pak potřeba pořád psát cestu /sys/class/gpio/…
, export pinu a nastavení směru se udělá jedním příkazem a taky to kontroluje zadané hodnoty a zobrazuje nápovědu.
Ukázka použití:
PIN=8
# importujeme funkce:
. ./gpio.sh
# otevřeme pin pro čtení:
gpio_open $PIN in
# načteme hodnotu do proměnné:
hodnota=`gpio_read $PIN`
# zavřeme pin:
gpio_close $PIN
# otevřeme pin pro zápis:
gpio_open $PIN out
# nastavíme na něm logickou jedničku:
gpio_write $PIN 1
# tzn. když k němu bude připojená LEDka, tak se rozsvítí
# zavřeme pin:
gpio_close $PIN
Jestliže události vznikají mimo náš systém (např. stisk tlačítka připojeného ke GPIO pinu), bylo by dost hloupé muset se periodicky ve smyčce dotazovat na hodnotu a porovnávat ji s předchozí, zda se změnila. Místo toho chceme dostávat upozornění na události, které se staly (ve chvíli, kdy se stanou, ne když se na ně zeptáme).
Přesně k tomu slouží v případě změn souborů subsystém Linuxu zvaný inotify. A protože s GPIO pracujeme jako se soubory, je zcela logické použít inotify pro odběr notifikací o změnách hodnot jednotlivých pinů.
# přes inotify budeme sledovat změny souboru
# a hodnotu vypíšeme na standardní výstup (vypisuje 0 nebo 1)
echo both > /sys/class/gpio/gpio$PIN/edge;
while inotifywait -qq /sys/class/gpio/gpio$PIN/value; do
gpio_read $PIN;
done
Použití si ukážeme na příkladu dveřního čidla připojeného k pinu 17. Na Raspberry Pi si vytvoříme skript /root/skripty/dveře.sh
:
#!/bin/bash
# importujeme si gpio_* funkce:
DIR="`dirname $0`";
source "$DIR/gpio.sh";
# otevřeme si pin 17 pro čtení:
PIN=17;
gpio_open $PIN in;
# připravíme si funkci pro zavření pinu:
__exit() {
gpio_close $PIN;
exit 0;
}
# zavoláme ji při ukončení procesu resp. stisku Ctrl+C:
trap '__exit' INT
# přes inotify budeme sledovat změny souboru
# a hodnotu vypíšeme na standardní výstup (vypisuje 0 nebo 1):
echo both > /sys/class/gpio/gpio$PIN/edge;
while inotifywait -qq /sys/class/gpio/gpio$PIN/value; do
gpio_read $PIN;
done
# Sem to běžně nedojde a tento kód se už nevykoná – výše je cyklus
# a při jeho přerušení pomocí Ctr+C se pokračuje funkcí __exit(),
# kde voláme exit 0.
gpio_close $PIN;
# Při spouštění přes ssh je tento skript nutné volat s volbou -t:
# ssh -t root@rpi.example.com 'stty -opost; skripty/dveře.sh'
# protože jinak by proces neměl přiřazen řídící terminál
# a nedošlo by k ukončení potomků (inotifywait) - ti by zůstávali
# běžet i po ukončení spojení a rodičovského procesu.
Po spuštění tento skript sleduje události na GPIO a vypisuje na standardní výstup hodnoty 0 a 1 (na každý řádek jednu hodnotu). Takový proud událostí můžeme snadno využít v unixových rourách a propojit s jinými příkazy. A protože GPIO máme na Raspberry Pi, ale na události chceme reagovat na našem PC, jednoduše si tento proud nasměrujeme k sobě pomocí SSH.
Při otevření nebo zavření dveří si necháme zobrazit upozornění na ploše:
ssh -t root@rpi.example.com 'stty -opost; skripty/dveře.sh' \
| grep --line-buffered '^[01]$' \
| sed --unbuffered -e 's/^0$/zavřeno/g' -e 's/^1$/otevřeno/g' \
| xargs -l1 -exec bash -c 'echo_datum "$@"' "{}" \
| xargs -l1 -d\\n notify-send
Grepem si filtrujeme pouze hodnoty 0 a 1 (kdyby tam bylo např. nějaké varování, tak ho tímto přeskočíme). A sedem si překládáme nuly a jedničky na něco srozumitelného.
Volby --line-buffered
a --unbuffered
, které běžně nepoužíváme, jsou zde velmi důležité – příkazy grep
a sed
by jinak čekaly na zaplnění zásobníku a vypsaly by až více řádků najednou (což obvykle nevadí a je to naopak výkonnostní optimalizace, ale tady potřebujeme reagovat na události hned).
Rovněž je důležitá volba -t
u SSH, viz komentář v kódu výše. S tím souvisí i stty -opost
, které mj. zabrání tomu, aby se nám na výstup přidával znak CR před konce řádků LF, což by nám překáželo v dalším zpracování. Pokud máte jednodušší řešení, jak zajistit ukončení všech (pod)procesů při ukončení spojení a zároveň se zbavit CR, budu rád za komentáře.
V dalším kroku (první xargs
v rouře) si rozšíříme informaci o datum a čas. Napíšeme si na to jednoduchou funkci, která vypisuje svoje parametry (podobně jako echo
) a předřazuje před ně datum a čas:
echo_datum() { echo "`date --rfc-3339=seconds`: ${@}"; }
export -f echo_datum # funkci exportujeme, abychom ji „viděli“ v xargs
To se může hodit mj. pro logování – např. přes tee
zkopírovat proud dat i do souboru. Ukázka použití:
$ echo_datum aaa bbb ccc 2016-11-12 13:37:08+01:00: aaa bbb ccc
Příkaz xargs
čte standardní vstup a volá zadaný příkaz (nofity-send
) s parametry, které načetl ze vstupu. Volba -l1
slouží k tomu, aby xargs
předal příkazu nofity-send
vždy jen jeden parametr. A volba -d\\n
k tomu, aby se za oddělovač považoval znak konce řádku \n
– jinak by se nám totiž data rozpadla i podle mezer a vzniklo by víc notifikací, než chceme.
A zde máme konečný výsledek – upozornění na otevření/zavření dveří:
BTW: na unixových rourách se mi líbí, že člověk může odmazávat od konce jednotlivé části a snadno zjišťovat, co se mezi jednotlivými fázemi předává.
Začátek necháme stejný, jen místo zobrazení notifikace pošleme příkaz hudebnímu přehrávači, aby začal hrát nebo naopak přestal (toggle):
ssh -t root@rpi.example.com 'stty -opost; skripty/dveře.sh' \
| grep --line-buffered '^[1]$' \
| sed --unbuffered -e 's/^1$/toggle/g' \
| xargs -l1 -d\\n xmms2
Pomocí grepu jsme si vyfiltrovali pouze událost otevření dveří a navázali na ni ovládání přehrávače XMMS2. Pokud odejdeme z místnosti (tzn. otevřeme a zavřeme dveře), přehrávání se pozastaví a po našem příchodu (resp. při otevření dveří) se zase spustí tam, kde předtím skončilo.
Na XMMS2 se mi líbí jeho architektura – není to monolitický program, ale je rozdělený na démona (proces běžící na pozadí, pod daným uživatelem, není to systémový démon) a uživatelské rozhraní (UI). Démon přehrává hudbu, má v paměti seznam skladeb a další věci. UI mu pouze posílá příkazy (např. další, předchozí, přehraj, přidej skladbu) a existuje více jeho implementací – pro GTK, Qt, pro příkazovou řádku… Navíc UI není pro přehrávání vůbec nutné – tento program můžete klidně ukončit a hudba bude hrát dál (protože démon na pozadí pořád běží) a následně se můžete připojit jiným UI nebo být klidně připojen z několika UI současně:
Všechny instance UI jsou navzájem synchronizované přes démona a když např. přehazujete pořadí skladeb, projeví se to hned ve všech oknech. Pro spojení démona a UI se standardně používá unixový soket, ale můžete použít i TCP a ovládat program po síti.
I jiné přehrávače obvykle nabízejí nějaké API, díky kterému je můžete ovládat čidlem připojeným ke GPIO. Typicky se dnes používá D-Bus:
# VLC:
qdbus \
org.mpris.MediaPlayer2.vlc \
/org/mpris/MediaPlayer2 \
org.mpris.MediaPlayer2.Player.PlayPause
# Clementine:
qdbus \
org.mpris.MediaPlayer2.clementine \
/org/mpris/MediaPlayer2 \
org.mpris.MediaPlayer2.Player.PlayPause
Líbil se vám ten králík-kosmonaut na druhém obrázku? To je Glenda, symbol operačního systému Plan 9. Tento systém dotáhl asi nejdál koncept „všechno je soubor“. V jiných směrech se Plan 9 může zdát dost divný (asi jako stejnojmenný film) a v praxi se moc nerozšířil, ale něco po něm přeci jen zbylo. Jeho protokol 9P se dnes používá v qemu-kvm virtualizaci pro sdílení souborů mezi hostem a hostitelem.
Ukázali jsme si, jak pracovat s GPIO a reagovat na události pomocí klasických unixových (resp. GNU) nástrojů, rour a skriptů – bez dodatečných knihoven a programování. Pro mnoho úkolů je tento systém vhodný a i když nějaký programovací jazyk používáte, nemusíte hned shánět knihovnu – máte souborové rozhraní.
Pokud ale máte vysoké nároky na rychlost a chcete přes GPIO např. emulovat nějaký protokol nebo měřit data s vyšší frekvencí, bude přeci jen potřeba použít specializovanou knihovnu (viz test v odkazech níže).
Témata: [GNU/Linux] [svobodný hardware] [hardware] [elektřina]
Pěkné! Ten datum za xargs by šel vložit přímo v substituci sed s///e, pokud bude GNU.