Praktická robotika a zpracování obrazu (BPC-PRP) online skripta

Tento text představuje online skripta předmětu BPC-PRP - povinného předmětu 3. rocniku bakalářského studijního oboru Automatizační a měřicí technika na Fakultě elektrotechniky a komunikačních technologií Vysokého Učení Technického v Brně.

Skripta rovněž obsahují texty týkajici se dalších oblastí robotiky, které již ale nejsou vyžadovány samotnou náplní předmětu.

Autoři

Ing. Adam Ligocki, Ph.D.

Ing. Tomáš Lázna

Ing. Petr Gábrlík

Ing. Tomáš Jílek, Ph.D.

Ing. František Burian, Ph.D.

Ing. Tomáš Horeličan

Operační systém Linux

Důvodem pro použití Linux ve výuce předmětu je, že momentálně neexistuje distribuce Windows pro výkonnou výpočetní jednotku mobilního robotu, tj RaspberryPi. Pravděpodobně ani nikdy existovat nebude. Kdysi existovala Windows10 IoT, takové demo, které se firmě Microsoft nechtělo udržovat, a tak projekt zařízlo již v raných fázích vývoje.

Program pro řízení robotu tedy musí být psán tak, aby běžel na OS Linux a studenti musí dokázat takový program vytvořit a odladit. V této a následujících kapitolách bude popsána instalace, jednoduchá reference základních příkazů, které je potřeba znát proto abyste dokázali program úspěšně odladit. Pro mnohé to bude vcelku nová, mnohde bolestivá zkušenost, ale pokud máme zpětnou vazbu od studentů po pár letech, většinou hodnotí pozitivně a mnohé tento předmět navnadil na trvalý přechod od Windows na alternativní operační systém.

Víme, že většina studentů je cvičena v předchozích předmětech na operačním systému MS Windows. Rýpavý žák může namítnout "však můžeme ladit pod windows a jen výsledek po odladění překompilujeme a nahrajeme do raspbery". Ano, to můžete ale pak mohu dodat "děj se vůle boží". Protože je nejspíše pánubohu jedno, jak Váš projekt dopadne, určitě vyvstane některý z myriády problémů které programátoři multiplatformních aplikací musí řešit, a program fungovat nebude. Z důvodu Vám přiděleného času jsme tedy rozhodli, že nebudeme řešit multiplatformní problémy a tím pádem musíte znát OS Linux.

Covidová léta tento požadavek výrazně umocnily, protože nebylo možné se setkávat a pracovat na reálném HW, práce byla z velké části z domu, bylo rozhodnuto že budete pracovat v simulátoru. A protože celý svět je poháněn ROS, byl tento simulátor vytvořen v ROS, který je nativní v Linuxu.

Pro spoustu studentů byla instalace Linuxu ve virtuálním prostředí první volbou. Bohužel výkonnost notebooků mnohdy nedosahovala plnohodnotnému nativnímu běhu a simulátor takto spouštěný byl velmi pomalý, prodlužovalo se dopravní zpoždění - a sami víte co dopravní zpoždění s regulací dělá.

Proto doporučuji zvážit, kterou z následujících konfigurací si zvolíte:

  • Jsem Linuxák/Linuxačka

    • Zde nastávají neřešitelné problémy, linuxáci jsou "divní" lidé kteří používají mnoho distribucí které nejsou kompatibilní (viz Distribuce).
    • Pokud nemáte Debian/Ubuntu konkrétní verze, musíte jít cestou virtualizace. Ne, ROS nezrozchodíte na Vaší oblíbené distribuci. Zkoušeli jsme to. Fakt. Ani Fedora. Ani Mint.
    • Pokud jste Linuxák/Linuxačka do morku kostí, nemusíte dál číst a přeskočte na další kapitolu, další text je pro nooby.
    • Nejspíš budete velmi cenní pro ostatní členy týmu :-)
  • Multiiboot Windows/Linux, IDE v Linuxu

    • Velmi rychlé, využíváte prostředků vašeho počítače přímo
    • Nebezpečné vůči windows (při instalaci a používání je potřeba dávat pozor co děláte, můžete přijít o data na Windows, TPM)
    • "Blbě se ten parazit odstraňuje" aneb po skončení semestru je to na kompletní přeinstalaci windows (což stejně doporučuji dělat každý semestr, aspoň otestujete funkčnost zálohování)
  • Virtuální Linux na Windows OS, IDE v Linux

    • Nejpomalejší varianta, IDE je v Javě která je interpretovaná ve virtuálním stroji který běží na virtuálním stroji ...
    • Musíte mít opravdu rychlý počítač (spíš stolní, toto není nic pro levné notebooky) a hodně paměti (u Win11 tak 32GiB, spíš víc, u Win10 stačí 16GiB)
    • Návody jsou uzpůsobeny této variantě
    • Na konci semestru stačí jednou zmáčknout klávesu delete a "je čisto", po Linuxu ani památky
  • Virtuální Linux na Windows OS, IDE ve Windows

    • Relativně rychlá varianta kdy se kód píše dobře na systému který student zná a kompilace probíhá na virtuálu
    • Oproti návodům je třeba provádět kroky konfigurace navíc
    • Výsledný setup více odpovídá ladění na reálném hardware
    • Na konci semestru kromě smazání virtuálu zbude svinčík který je potřeba odstranit (nebo si jej zamilovat)
  • Nativní Linux na RaspberryPI, IDE ve Windows

    • Vpodstatě identické ladění jako na hardware, žádná virtualizace
    • Nutné vlastnit těžko dostupný a relativně drahý hardware
    • V počítači máte jen IDE, vše s Linuxem je oddělené a můžete kdykoli na konci semestru demonstrativně zlikvidovat.
    • Pro akademický rok 2022/2023 nepodporovaná varianta (můžete ji zkusit ale nebudeme dodávat podporu - pouze ve volném čase, kterého je málo)

Podle toho se liší způsob instalace, popsaný v následujících kapitolách. Pro virtualizaci existují dva významné nástroje VirtualBox a VmWare. Obecně je VmWare rychlejší, lépe podporovaný ale je též placený ale základní verze Playeru je zcela zdarma pro nekomerční použití.

Pozor Windows 11 není momentálně podporován u VmWare Playeru, tato volba je tedy možná jen u staršího Linuxu a u Windows 10 (kterému končí podpora).

VirtualBox na Windows 11 sice nainstalujete, ale není zaručeno že pod Windows 11 poběží (Vivat Microsoft compatibility made in India!)

Pokud máte Windows 11, máte tedy možnost pouze multibootu a modlit se aby čip "TPM" v BIOSu nedělal problémy. Záloha před operací nutná, možnost následné pitvy vysoká!

Distribuce Linuxu

Hovoříme-li o operačním systému Linux, máme na mysli jádro operačního systému, které je spravováno autoritou (autor Linus Benedict Torvalds) a ta zajišťuje integritu veškerého kódu, který je do jádra OS zaintegrován.

Pro interakci jádra operačního systému s okolím je třeba přidružených programů (např balíčkovacích systémů, grafického rozhraní, a dalšího podpůrného software). Sadu těchto programů souhrnně označujeme Distribuce.

Distribuce je dodávána a garantována konkrétní právnickou osobou (komerční subjekt, organizace, atd.). Distribuce jsou většinou vzájemně nekompatibilní na binární úrovni.

Často používané distribuce:

  • Debian
    • nejstabilnější distribuce, nepodporuje však nový hardware (stable obsahuje 2-5 let starý kód)
    • správa balíčků 'apt'
  • Ubuntu
    • derivát Debianu
    • Nejrozšířenějí distribuce na domácích stanicích.
    • Snahou je urychlit testování a distribuci k uživatelům bez znatelného snížení nestability
    • správa balíčků 'apt'
  • Mint
    • derivát Ubuntu.
    • Snaha o grafické rozhraní které zachovává idiomy MS Windows.
    • Doporučený pro přechod z MS Windows
    • správa balíčků 'apt'
  • RaspberryOS (dříve Raspbian)
    • derivát Debianu upravený pro minipočítač Raspberry Pi
    • správa balíčků 'apt'
  • Arch Linux
    • Distribuce orienovaná na nejaktuálnější jádro.
    • Nižší stabilita ale vyšší kompatibilita s nejnovějším hw.
    • Doporučeno pro pokročilejší uživatele i vzhledem k široké konfigurovatelnosti
    • správa balíčků 'yay'
  • Fedora
    • Derivát RedHat Enterprise Linux
    • správa balíčků 'rpm'
  • ElementaryOS
    • Minimalistická a rychlá distribuce.
    • Vhodná pro starší a slabé počítače.
    • správa balíčků 'apt'

... a mnoho dalších (výčet není konečný)

Instalace Linuxu Windows Services for Linux

Tuto metodu zvolte, pokud chcete mít linux běžící současně s windows a nemáte dostatečné prostředky pro plnohodnotnou instalaci do virtuálního stroje.

WSL je jen částečná virtualizace, vytvořený stroj neběží v sandboxu, využívá jádra windows a proto je oproti standardn virtualizaci rychlejší.

POZOR WSL není plnohodnotným operačním systémem, chybí grafické rozhraní a spousta věcí je emulována pomocí eindows. Pod WSL nedoporučujeme spouštět ROS, i kdyby se jej nainstalovat podařilo, budete čelit různým nepředvídatelným problémům.

POZOR Na některých (zejména notebookových) procesorech není integrována podpora pro virtualizaci (takové notebooky bývají levnější). Na počítači který nemá tuto podporu bude jakákoliv virtualizace velmi pomalá a jedinou schůdnou variantou pro Vás bude multiboot.

Celý nainstalovaný operační systém vám na disku zabere cca 3 GB místa

Instalace podpory emulace

Podporu pro virtuální stroj nainstalujete pomocí:

wsl --install

wsl-prikez

Následně se Vám začne instalovat podpora pro virtualizaci

wsl-prikez

Po skončení potřebuje restartovat počítač.

Všimněte si, že v průzkumníku se vám v levém stromě adresářů vlevo objeví nová položka "Linux", která je nyní prázdná.

Po restartu se Vám automaticky spustí pokračování instalace (doinstalování operačního systému), je potřeba počkat na následující okénko.

wsl-prikez

Zde je potřeba zadat nové uživatelské jméno a heslo (heslo se při psaní nezobrazuje kvůli bezpečnosti, ani hvězdičky) a nechat instalaci dokončit

Takto nějak vypadá úspěšně nainstalované WSL s aktuálním Ubuntu 22.04.

wsl-prikez

Následně je potřeba aktualizovat databázi balíčků z internetu (nutný internet)

sudo apt update

wsl-prikez

A aktualizovat balíčky, které mají nějaké aktualizace

sudo apt upgrade

Po aktualizaci máme systém připravený pro instalaci simulátorů.

Nyní stáhneme nejnovější potřebné balíčky simulátorů z SIM a NMEA

Tyto stažené balíčky je nutné nejprve překopírovat do "virtuálního" stroje dle obrázku. tj ze "Stažené" do "Linux/Ubontu/home//"

wsl-kopiesouboru

A následně nainstalovat (všechny závislosti se vyřeší a doinstalují samy z internetu):

sudo apt install ./qsim-xxxx.deb ./sim-demo-xxxx.deb ./sim_xxxx.deb ./nmea-xxxxx.deb

Při instalaci se semtam objeví nějaká chyba. Spoustu chyb pomůže vyřešit příkaz:

sudo apt --fix-broken install

wsl-kopiesouboru

Pokud máte vše instalováno, je možné spustit dva terminály (v menu windows hledat "Ubuntu" - spustí se druhý terminál) a v jednom spustit qsim a poté v druhém spustit sim-demo:

qsim
sim-demo

měla by se objevit simulace robota, která sleduje čáru.

wsl-kopiesouboru

Máte hotový linux, na kterém běží soutěžní simulátor

Instalace Linuxu vedle Windows

Tuto metodu zvolte, pokud chcete mít linux rychlý, nebo máte postarší stroj.

Tvorba instalačního média

Připravte si prázdné USB a stáhněte nástroj Rufus pro tvorbu bootovacího média. Dále stáhněte Ubuntu 22.04 desktop image. Vložte USB do počítače a spusťte program Rufus. Ujistěte se, že je zvolené správné USB, vyberte stažený Ubuntu .iso image jako zdroj, a zvolte schému GPT a target UEFI. Spusťte program a pokud se vás zeptá ponechte režim ISO.

linux_install

Rozdělení disku / vytvoření prostoru pro instalaci

Nejprve je vhodné nachystat prostor na disku, kde bude Linux nainstalován. To udeláte ve svém Windowsu pomocí utility Disk Management. Stiskněte Start a do vyhledávaní napíšte diskmgmt.msc a spusťte.

linux_install

Otevře se vám nové okno s managerem kde uvidíte všechny dostupné disky:

linux_install

Nejpohodlnejší je samozřejmě mít druhý systém nainstalován na samostatném disku (viz. obrázek dolu, kde je označen disk s nainstalovaným Linuxem).

linux_install

Mnohokrát ovšem taková možnost není a musíte si oba systémy rozdelit do jednoho disku. Např. když máte jen jedno rychlé SSD, ze kterého chcete oba systémy bootovat. Pro to je potřeba uvolnit (odalokovat) místo z některého oddílu na disku.

POZOR! - V Disk Manageru uvidíte kromě základních oddílů, ke kterým máte jako uživatel prístup (např. C:, D:, atd.), také skryté oddíly které Windows interně využíva (obvykle bez označení písmenem). Nesnažte se zasahovat do těchto oddílů, jinak můžete poškodit svůj systém.

Záloha

Pokud chcete mít jistotu, že o svoje dáta nepřijdete můžete si celý disk nebo jeho jednotlivé oddíly zálohovat na externí disk. Můžete použít program Macrium Refect pro klonování disků. Po spuštění programu vyberete disk, který chcete klonovat.

linux_install

Následně vyberete disk, na který chcete klon převézt a zvolíte které oddíly se mají klonovat. Tento proces může v závislosti na velikosti klonovaného oddílu trvat celkem dlouho.

linux_install

Klikněte na zvolený oddíl (ideálně ten, na kterém máte nejvíce místa) a vyberte možnost "zmenšit".

linux_install

Vyberte kolik míste chcete uvolnit (např. 100GB).

linux_install

V disk manageru nyní uvidíte váš nově uvolněný prostor a můžete přejít k instalaci Linuxu.

linux_install

Pokud máte ve Windowsu zapnuté šifrování disku BitLockerem (např. u Windows 11 je to default), budete ho muset vypnout. Instalace Linuxu není možná pokud je disk šifrován. Existují způsoby jak zachovat šifrování i pro dual boot ale nejjednodušší je ho pro naše účely úplně vypnout.

Windows BitLocker

Ve Windows 11 najdete nastavení v části Soukromí a Zabezpečení > Šifrování zařízení. Pro zálohovaní obnovovacího klíče klikněte na Nástroj BitLocker Drive Encryption a vyberte možnost zálohovat. Můžete také postupovat podle návodu zde. Po vypnutí nastavení zaháji Windows dešifrování disku, tento proces může nejakou chvíli trvat.

linux_install linux_install

Po dokončení dešifrace restartujte systém.

Zahájení Instalace Linuxu

Vložte nachystané instalační USB a restartujte systém. Nezapomeňte si v BIOSu přepnout nastavení na boot z USB pokud máte nastavené prioritně bootování z disku s Windowsem. Zvolte jazyk anglicky a vyberte možnost instalovat.

linux_install

Zvolte normální instalaci a dále můžete zaškrtnout možnost stahování updatů počas instalace a instalaci driverů od třetích strán, a nebo je vynechat a doinstalovat vše potřebné později.

linux_install

Následně, pokud instalatér detekuje na vašem disku bootovací oddíl pro Windows, můžete zvolit možnost automatické instalace vedle systému Windows. Dále už proběhne samotná instalace do uvolněného prostoru. Pokud tuto možnost neuvidíte budete muset zvolit možnost Something else a manuálně vybrat kam se má Linux nainstalovat.

linux_install

Manuálni Instalace

V seznamu najděte položku s volným prostorem. Její velokost musí odpovídat tomu kolik místa jste si uvolnili v předchozích krocích. V tomto příkladu jsou obrázky z jiného počítače, proto je velikost jiná. Stiskněte plus pro vytvoření nového oddílu.

linux_install

Vyberte maximální dostupnou velikost a zbytek podle obrázku dolu:

linux_install

Poté uvidíte nově vytvořený oddíl v seznamu.

POZOR! - V spodní části můžete zvolit kam se na nainstalovat bootovací oddíl pro Linux. Tady nechejte buď celý disk (položku s názvm celého disku kde instalujete Linux) a nebo právě nově vytvořený oddíl. Nikdy nevybírejte žádnej jinej oddíl, který je na disku.

linux_install

Pak už jen vyberete časové pásmo, a vytvoříte si username a heslo k účtu.

Instrukce pro instalaci Linuxu najdete také zde.

Výsledek

Po dokončení instalace a restartu uvidíte obrazovku boot manageru (GRUB), kde můžete vždy volit jestli chcete zpustit Linux nebo Windows.

linux_install

V BIOSu můžete také vidět více možnosti pro bootování systému. Boot manager pro Linux, který zároveň detekuje i všechny ostatní nainstalované systémy, a boot manager pro Windows, který uvidí vždy jen svůj vlastní systém Windows. Je proto nejlepší ponechat jako prioritní manager Linuxu, protože z nej můžete jednoduše bootovat i do svého Windowsu.

linux_install

V programu Disk Management ve Windowsu uvidíte, že oddíl, který jste na začátku uvolňovali je nyní obsazen Linuxem.

linux_install

Odstranění Linuxu

Nejjednodušší způsob jak odstranit Linux a vrátit prostor na disku zpátky do Windowsu je odstranit celý oddíl s nainstalovaným Linuxem.

POZOR! - Tím ale Linuxový boot manager ztratí informace o instalaci systému a nebude schopen dál nijak bootovat. Proto si nejprve v BIOSu nastavte jako prioritní výchozí Windows boot manager a ujistěte se, že bootování do Windowsu přes nej funguje. Pak můžete odstránit oddíl s Linuxem.

linux_install

Po odstranění opět zůstane volný prostor.

linux_install

Pak už jen jednoduše zvolte původní oddíl a vyberte možnost "rozšířit".

linux_install

Zadejte maximální dostupnou velikost a potvrďte.

linux_install

Nyní bude disk v původním stavu tak jako před instalací Linuxu.

linux_install

Nefunkční Linux boot manager na disku bohužel už zůstane. Pokud se ho budete chtít zbavit také, můžete zkusit některý z Windows nástrojů pro opravu bootovacího sektoru. Můžete si tím ale zbytečne poškodit i aktuálně fungující instalaci Windowsu, proto pokud vše funguje je lepší ho nechat tak.

Instalace Linuxu s použitím virtulizace VirtualBox

Instalace VirtualBoxu

VirtualBox si nainstalujte podle návodu na příslušném operačním systému.

Postup pro Windows a Mac.

Pro Linux instalace záleží na distribuci a používaném baličkovacím systému. Na Debianu použijte příkaz sudo apt install virtualbox. Poté bude možné VB aktivovat z terminálu voláním virtualbox, nebo skrze ikonku v seznamu nainstalovaných programů.

VB

Příprava instalačního média

Stáhněte si obraz instalačního disku Ubuntu 20.04.

Vytvoření virtuáního stroje

Pomocí tlačítka "New" vytvořte nový virtuální stroj. Pojmenujte si jej, zvolte typ operačního systému, nastavte velikost operační paměti (vhodné 4GB a více), vytvořte nový virtuální disk, vyberte typ virtuálního disku (VDI), zvolte dynamickou alokaci disku a na poslední obrazovce vyberte umístění virtuálního disku na svém počítači a zvolte maximální možnou velikost virtuálního disku (32 - 64GB).

Nyní v hlavní obrazovce Virtual Boxu zvolte nově vytvořený virtuální stroj a přejděte do nastavení.

V záložce "System" můžete měnit velikost dedikované operační paměti, měnit počet jader procesorů, která budou pro virtuální stroj dostupná (je doporučeno dvě a více) a také můžete zapínat/vypínat HW akceleraci pro virtuální stroj.

Dále v záložce "Display" světšete množství dedikované video paměti na maximum (128MB).

V záložce "Storage" klikněte na položku s obrázkem CD a následně úplně v pravo klikněte pravým na ikonku CD s malou šipečkou. Tím otevřete okno pro nastavení cesty k instalačnímu obrazu Ubuntu, které jsme dříve stáhli na počítač. Zavřete nastavení tlačítkem OK.

VB

Nyní v hlavním okně Virtual Boxu aktivujte virtuální stroj tlačítkem start.

Pokud je vše nastaveno korektně, stroj nabootuje z instalačního obrazu.

Zahájení Instalace Linuxu

VB

Zvolte jazyk operačního systému (doporučena angličtina), zvolte rozložení klávesnice (doporučeno English US), a nechte pokračovat "Normal Installation".

Dále se Vás proces dotáže, zda chcete "Smazat disk a nainstalovat Ubuntu". Protože instalujeme do virtuálního stroje, necháme rozdělení disku na instalátoru.

Nyní zvolte časové pásmo a dále si vytvořte účet s heslem.

A dále už jen počkejte, až se systém doinstaluje a provede se reset virtuálního stoje.

Po opětovném nabootování v horní liště okna virtuálního stroje zvolte záložku "Devices" a "Insert Guest Additions CD Image". Za okamžik vyskočí okno, které se dotáže zda má aktivovat autorun vloženého CD. Souhlaste. Systém si vyžádá heslo a poté se doinstalují ovladače pro virtualizovaný hardware.

Po dalším restartu máte připarevený virtuální stroj s nainstalovaným Linuxem Ubuntu 20.04.

VB

Instalace Linuxu s použitím virtulizace VmWare

Instalace VmWare Player

Příprava instalačního média

Vytvoření virtuálního stroje

Zahájení instalace Linuxu

Orientace v systému

Souborová struktura Linuxu se odvozuje od tzv. kořene (root), který značíme jako / (vzdáleny ekvivalent C:/ na Windows).

V kořenovém adresáři pak nalezneme složky jako:

  • bin/ - obsahuje binárky (spustitelné soubory operačního systému).
  • home/ - adresář, který obsahuje domovské složky uživatelů, tj vaše soubory.
  • dev/ - obsahuje soubory které mapují hardware počítače (interní a externí disky, sériovou linku, usb, síťové rozhraní, atd.).
  • tmp/ - dočasná složka. Zde si programy odkládají svá dočasná data.
  • media/ - místo kde se připojují externí disky.
  • etc/ - složka obsahuje konfiguraci systému a všech nainstalovaných aplikací

V Linuxu neexistuje ekvivalent fyzického dělení na disky. Fyzické disky lze připojit do libovolné složky. Ze zadané cesty k souboru tedy nelze přímo usoudit na kterém fyzickém disku se data nacházejí. To je velmi odlišné od Windows kde cesta vždy začíná písmenem fyzického disku, a možnost připojit disk do složky přichází až od systému souborů NTFS.

Složka tmp vede na většině distribucí do virtuálního disku v paměťi RAM počítače. Je tedy extrémně rychlá, ale za cenu že se při vypnutí počítače smaže. Spousta nástrojů ji používá pro vytvoření mezivýsledků kompilace.

Po příhlášení do konzole se obvykle nacházíte v domovském adresáři, tj na místě /home/<jmeno_uzivatele>/

Spuštění příkazového řádku

Vytvoření souboru

touch - (touch file)

Vytvoří soubor na disku

touch  jmeno_souboru      ...      Vytvoří soubor jmeno_souboru na disku.

Kopírování souboru

cp - (copy)

cp zdrojovy_soubor cilovy_soubor                    ...      vytvoří novou kopii zdrojovy_soubor nazvanou cilovy_soubor
cp ../secter.txt secret_folder/supersecret.txt      ...      vem soubor secret.txt, který se nachází o složku výš a zkopíruj ji do složky secret_folder. Kopie původního souboru se bude jmenovat "supersecret.txt"     

P5esun souboru

mv - (move)

Příkaz původně pro přesun souboru, hlavně se však využívá pro přejmenováni soborů.

mv old_name.txt new_name.html      ...      přejmenuje soubor "old%name.txt" na "new_name.html"

Smazání souboru

rm - (remove)

Smaže soubor/složku.

rm old_file.txt      ...      vymaže soubor "old_file.txt"
rm -r my_folder      ...      smaže složku. Při mazání složky vždy musíme použít modifikátor rekurze (-r). Ten říká, že se má rekurzivně smazat také obsah složky.

Změna práv k souboru

chmod - (change file mode)

Změní přístupová práva k souboru.

chmod 777 /dev/ttyUSB0      ...      umožní všem uživatelům PC přístup na USB port s pořadovým číslem 0. Pro detail fungováni přístupových práv ve file systému viz [7].

Vypsání obsahu souboru

cat - (Concatenate FILE(s) to standard output)

Program vypíše do termínálu obsah souboru.

cat ~/my_config_file.txt      ...      vytiskne v terminálu obsah zvoleného souboru

Vytvoření nového adresáře

mkdir - (make directory)

mkdir my_folder      ...      vytvoří nový adresář s názvem "my_folder"

Smazání složky

rmdir - (remove directory)

Smaže složku z disku. Složka musí být prázdná

rmdir my_folder      ...      smaže složku.

Zjištění aktuální nastavené složky

pwd - print working directory

Vypíše aktuální složku.

pwd

Zm2na aktuáln9ho adresáře

cd - (change directory)

Změna složky.

cd my_directory      ...      přesun do adresáře s názvem my_directory
cd ~                 ...      návrat do domovské složky (v linuxu nazýváme "home")
cd ..                ...      návrat o adresář výš (dvojtečka)
cd /                 ...      návrat do kořene file systému (v linuxu nazýváme "root")
cd ../my_folder      ...      vrať se o adresář výš a pak se přesuň do adresáře "my_folder"
cd .                 ...      přesuň se do "současného adresáře". V podstatě nic neudělá. Příklad ilustruje existenci symbolu pro aktuální adresář (tečka).

Výpis souborů ve složce

ls - (list)

Vypiš všechny soubory a složky (složka je taky typ souboru) v aktuálním bodě file systému.

ls
ls -la      ...      vypíše všechny soubory, včetně sktytých a přidá k výpisu detailní informace

Editace textu pomocí GNU nano

nano

Editace textu podobná poznámkovému bloku

nano jmeno_souboru    --- zahájí editaci souboru

Klávesové ovládání:

Ctrl + X - ukončení programu. Program se zeptá, zda má uložit změny

Editace textu pomocí GNU vim

vim (visual editor improved)

Editor textu, který pracuje v příkazovém režimu, tedy uživatel zadává editoru příkazy a editor na základě těchto příkazů upravuje text.

Pro jeho použití je nutné si zapamatovat velké množství příkazů a klávesových zkratek. Pokud to však zvládnete, může být práce s ním rychlejší než s nano. Není doporučován začátečníkům.

Kdyby se Vám přeci jen povedlo vim zapnout, vězte že jej vypnete kobinací kláves Shift + Z + Z (držíme shift a dvakrát zmáčkneme klávesu 'Z').

Stahování souborů ze sítě pomocí GNU wget

wget

Program pro stahování souborů ze sítě.

Příklad stažení posledního releasu wordpresu:

wget https://wordpress.org/latest.zip

Manuálové stránky

man - (manual) referenční manuál operačního systému

Rychlá pomoc když zapomenu, jak pracovat s daným programem

man ls      ...      vytiskne v terminálu manuál k programu ls

Eskalace oprávnění

sudo

Meta příkaz. Operace specifikovaná za tímto příkazem bude provedena v režimu oprávnění administrátora operačního systému. Obvykle používáme, když zasahujeme do systémových souborů.

sudo mkdir /etc/config      ...      vytvoří složku "config" v systémovém adresáři "/etc".
sudo rm -r /                ...      příkaz rekurzivně smaže celý adresář "root" (v podstatě smaže celý disk včetně OS)

Balíčkovací systém

apt

Jedná se o Balíčkovací systém Debianu. Na Linuxu nejčastěji instalumeme programy tak, že si jej stáhneme z veřejného repozitáře, tedy obvykle ověřeného a bezpečného serveru.

Při instalaci musíme vždy disponovat administrátorskými právy.

Příklad instalace balíčku s názvem 'git':

sudo apt update                  --- aktualizuj záznamy o repozitářích na internetu 
sudo apt install git             --- nainstaluj program git

Souborový manažer

mc

Midnight Commander - grafické prostředí pro pohyb v souborovém systému. Připomíná MS Dos.

mc

Vypíná se klávesou F10.

Slovo na závěre

Pokud jste v Linuxu nováčky, hlavně se nebote experimentovat. Ideálně si nainstalulte systém do Virtual Boxu a udělejte si zálohu virtuálního disku. Když se Vás podaří systém rozhasit, stačí si natánout backup a jedete dál.

Git - Verzovací systém

Git je distribuovaný systém pro verzováni a management zálohování zdrojových kódů. Obecně ale Git funguje dobře pro verzování libovolného textu. Primární motivací k výuce Gitu v rámci tohoto předmětu je fakt, že Git je dnes nejrozšířenějí verzovací systém v komerční sféře a zároveň je na webu dostupná obrovská paleta Git-based online verzovacích služeb.

Instalace Gitu na Linuxu

V případě, že pracujeme na distribuci Debian, Git nainstalujeme následovně:

sudo apt install git

Princip fungování

Primární funkcí Gitu je verzování textových souborů. Jedním dechem je potřeba dodta, že Git NENÍ vhodny pro verzování binárních souborů. Vyvíjíme-li tedy program a verzujeme vývoj v Gitu, vždy verzujeme pouuze zdrojové kódy, nikdy ne zkompilované spustitelné soubory (binárky).

Zároveň Git umožňuje velmi efektivní spolupráci mnoha lidí na stejném projektu (repozitáři). Vývojáři mohou pracovat společně, případně každý na separátním branchi. Důležité pravidlo však je, že dva lidé nesmí přepsat stejný řádek kódu ve dvou různých commitech. To způsobi tzv. konflikt. Obecné doporučení je, aby dva lidé neměnili stejný soubor.

Ve srovnání s SVN je ale Git tzv. decentralizovaný systém. To znamená, že v systému repozitářů neexistuje žaden nadřazeny, důležitější repozitář, či něco ve smyslu centrálního serveru. Všechny repozitáře mají stejnou funkcionalitu a jsou schopny udržovat kompletní historii celého repozitáře a ponohodnotně komunikovat se všemi ostatními klony. Praxe je však taková, že obvykle existuje repozitář, který funguje jako centrální místo pro výměnu commitů mezi vývojáři. Takový repozitář se obvykle jmenuje "origin". Důležité však je, že kterýkolik repozitář, si může z originu stáhnout kompletní historii a tak v případě selhání originu nedojde ke ztrátě dat, protože každý vývojář může mít jeho plnohodnotnou kopii na svém počítačí.

Obvykla práce s Gitem vypadá následovně:

  • Na serveru vytvoříme repozitář projektu.
  • vývojáři si naklonujou repozitář na lokální počítače. Z jejich pohledu loklálních repozitářů je server tzv "origin".
  • vývojáři na lokálních počítačích vytváří kód a commitujou.
  • na konci dne každý vývojáž pushne (nahraje) své denní commity na origin.
  • na druhý den ráno si každý fetchne (stáhne) commity kolegů z dne předchozího.

Základní terminologie

Vymezme si několik základních pojmů, abychom si rozuměli.

repozitář (repo)

Sada verzovaných souborů a záznamy o jejich historii. Pokud je repozitář uložen na našem počítači, nazýváme jej lokální repozitář (local repo). Jeli uložen na jiném stroji, hovoříme o vzdáleném repozitáři (remote repo).

klonování (cloning)

Stažení repozitáře z remote repa. Klonujeme v okamžiku, kdy na lokálním počítači repozitář neexistuje.

snapshot

Stav repozitáře v konkrétním bodě v historii.

diff

Rozdíl mezi dvěmi snapshoty. Tedy rozdíl stavu verzovaných souborů.

commit

Záznam, který obsahuje referenci na předchozí, následujicí snapshot a diff mezi nimi. Zároveň každý commit má svůj unikátní dvaceti bytový hash, který jej jednoznačně identifikuje v rámci repozitáře.

push

Nahrání nových comitů na remote repo.

fetch

Stažení commitů z remote repo na lokál. Fetchujeme, pokud na lokále máme repozitář naklonovaný, ale nemáme stažené nejnovější commity.

větev (branch)

Řetězec na sebe navazujicích commitů. Ze základu má každý repozitáž jednu větev ("master", někdy "main"). Probíha-li však vývoj několika funkcionalit vedle sebe, je možné tyto vývoje rozdělit do zvláštnich větví a připojit je spátky k hlavní větni, až je funkcionalita dokončená.

Přehled příkazů

git init

Inicializace repozitře. Z obvyklé složky v souborovém systému vytvořím repozitář.

Repozitář se od obyčejné složky liší tím, že v sobě obsahuje skrytou složku s názvem .git a ta obsahuje historii repozitáře.

git init     ...      inicializuje repozitář

git add

Příkaz přidává změny vytvořené od posledního commitu do tzv. indexu. Index je soubor změn, které budou součástí nejbližšího commitu. Díky mezistupni index je možné commitnout jen některé změny, které jsme od posledního commitu vytvořili.

git add myfile.txt     ...      přidá do indexu změny provedené nad souborem myfile.txt
git add .              ...      přidá do indexu všechny aktuální změny

git commit

Vytvoř nový commit, který je odvozený od posledního commitu v současné větví, a zahrni do commitu změny (diffy), které jsou v indexu.

git commit -m "komentář k danému commitu"     ...      vytvoří nový commit v rámci větve, ve které se nacházíme

git checkout

Příkaz slouží k přecházení mezi snapshoty.

git checkout .          ...     vrať větev do stavu posledního commitu (zahoď všechny do té doby vytvořené změny)
git checkout abcdef     ...     přepni mě do stavu, který vznikl po commitu s hexadecimálním označením abcdef
git checkout master     ...     přepni mě do stavu posledního dostupného commitu na větvi master

git clone

Příkaz vytvoří klon vzdáleného repozitáře na lokále. Klonujeme-li, není potřeba inicializovat repozitář pomocí git init. Metadata repozitáře se stáhnou automaticky s obsahem.

git clone https://adresa_vzdaleneho_repozitare.git     ...      vytvoří klon daného repozitáře na lokálním stroji

git remote

Příkaz vytvoří klon vzdáleného repozitáře na lokále. Klonujeme-li, není potřeba inicializovat repozitář pomocí git init. Metadata repozitáře se stáhnou automaticky s obsahem.

git remote -v                                            ...      vypíše konfiguraci vzdálených repozitářů
git remote add origin https://adresa_repozitare.git      ...      přidá do lokálního repozitáře alias vzdáleného repozitáře s danou adresou
git remote remove origin                                 ...      smaže alias origin na vzdálený repozitář  

git push

Odešle nové commity vytvořené na lokále na vzdálený repozitář.

git push origin master     ...     odešle na mastera nové commity vytvořené v rámci větvě (branche) master

git fetch

Stáhne z remotu commity do lokálního repozitáře. Stažené komity se ale nestanou součástí větve. Změny zůstanou pouze zapsány v paměti.

git fetch origin           ...     stáhne nové commity ve všech větvích z originu na lokál
git fetch origin master    ...     stáhne nové commity pouze pro větev master z originu na lokál

git merge

Na aktuální větví vytvoří nový commit tak, že spojí naagregované diffy dvou různých větví. Tím pádem se v součacné větvi objeví všechny změny, které byly vytvořeny v jiné větví. Větve se tak spojí.

git merge cool_branch        ...      na současné větví vytvoří nový commit, který obsahuje všechny změny větve cool_branch

git pull

Kombinace příkazů git fetch a git merge. Obvykle se používá při stažení změn ze serveru. Příkaz nejprve stáhne commity z vzdáleného repozítáře (provede fetch) a následně je připojí do současné větve (provede merge).

git pull origin master        ...      stáhne z originu nové commity na větvi master a přidá je do lokální větve master

git diff

Vytiskne rozdíl stavu repozitáře mezi dvěma commity.

git diff abcdef 012345        ...      vytiskne rozdíl mezi commity, které jsou identifikovány hexadecimálními hashy abcdef a 012345

git status

Zobrazí současný stav změn provedených od posledního commitu, včetně zobrazení změn, které jsou již přidány do indexu.

git status        ...      vytiskne současný stav změn

git log

Vytiskne chronologicky výpis commitů spolu s jejich metadaty (časem vytvoření commitu, popiskem, identifikačním hashem, atd.)

git log        ...      vytiskne historii současné větve

git stash

Slouží pro ukládání a načítání změn do zásobníku. Vhodné například, když si všimnete, že píšete kód na jiné větvi, než byl záměr. Pomocí git stash uložíte změny do zásobníku, přepnete se na jinou větev a změny si ze zásobníku vytáhnete.

git stash        ...      Uloží změny provedené od posledního commitu do zásobníku a vrátí větev do stavu, v jakém byla po posledním commitu (jako by jste zdrojový kód nikdy nenapsali).
git stash pop      ...      Vytáhne změny uložené ze zásobníku a aplikuje je na současný stav (jako by jste kód právě ručně napsali).

Cvičení

Několik scénářů se kterými se můžete během vývoje software potkat. Vyzkoušejte si je opakovaně, aby jste si vryli do paměti způsob práce s Gitem. Zároveň doporučuji si příklady nejprvé projít v příkazové řádce, aby jste chápali zůpsob, jakým Git funguje na nejnižší vrstvě a následně si cvičení absolvovali i v grafickém rozhraní Vašecho vývojového prostředí.

Základní obsluha

  • Vytvořte si repozitář.
  • Vytvořte v něm 2 textové soubory a do každého napište několik řádků.
  • Přídejte provedené změny do indexu a následně změny commitněte.
  • Nyní zeditujte jeden soubor a opět jej commitněte.
  • Zeditujte druhý soubor a změny commitněte.
  • Vytvořte si účet na GitHubu, a založte si tam nový repozitář.
  • Přidejte vzdálený repozitář jako "origin" do lokálního repozitáře a pushněte změny na origin.
  • Ve vebovém prostředí ověřte obsah repozitáře.
  • Na jiním místě v počítači, nebo na jiném počítači si naklonujte právě pushnutý repozitář.
  • V novém klonu proveďte změnu a commitněte jí pushnete na origin.
  • V původní složce pullněte nové commity z originu.
  • Příkazem git log si prohlédněte historii.

Konflikt

Příklad, co se stane, když dva vývojáří změní tentýž kód.

  • Po vzoru předchozího cvičení si vytvořte na počítači, případně na dvou počítačích dvě kopie repozitáře, který bude mít společný origin na webu.
  • V prvním klonu upravte konkrétní řádek souboru, commitněte a pushněte.
  • V druhém klonu upravte tentýž řádek, commitněne a pushněte (push zahlásí chybu).
  • Nyní jsme si vyrobili konflikt. Ve stejném bodě v historii větve repozitáře proběhly dvě změny,které se navzájem vylučují (tzv. conflict).
  • Konflikt opravíme tak, že v druhém klonu, který nedokázal pushnout provedeme pull z originu.
  • Nyní nahlédněme do souboru, který obsahuje konflikt. Konflikt je označen speciální syntaxí <<<<<<< lokalni_zmena ======= zmena_z_originu >>>>>>>. Vyberte verzi, která je žádoucí a speciální syntaxi odstraňte. Tím je konflikt vyřešen.
  • Zavolejte příkaz git commit bez dalších parametrů a provede se commit s automatickým popiskem, že se jedná o řešení konflitku.
  • Pushněte nový commit na origin a poté pullněte jej v původním repozitáři.
  • Příkazem git log si prohlédněte historii.

Doporučené materiály

Tutoriál Atlassianu

Oficiální dokumentace Gitu

Užitečný rádce při potížích s Gitem (cs)

Build system CMake

CMake je soubor nástrojů, které zjednodušují kompilaci projektů a knihoven takovým způsobem, aby byly nezávislé na operačním systému a kompilátoru. Funguje tak, že pomocí jednotného konfiguračního souboru CMakeLists.txt vygeneruje Makefile pro UNIX-like systémy a pro Windows generuje MSVC pracovní prostory. Velkou výhodou CMake je správa závislostí - aplikace si mohou definovat na jakých knihovnách jsou závislé, přičemž CMake kontroluje, jestli jsou tyto knihovny dostupné a navíc v požadované verzi. Další velkou výhodou je možnost vytvářet jak spustitelné soubory tak knihovny pomocí jedné jednoduché konfigurace umístěné v CMakeLists.txt.

Ukázkový soubor CMakeLists.txt pro aplikaci:

cmake_minimum_required(VERSION 3.7)
project(MyCoolRobot)

set(CMAKE_CXX_STANDARD 17) 

add_executable(MyCoolRobot main.cpp)

Ukázkový soubor CMakeLists.txt pro knihovnu:

cmake_minimum_required (VERSION 3.7)
project (MyCoolLibrary VERSION 0.1 LANGUAGES CXX )

include(GNUInstallDirs)
set (CMAKE_CXX_STANDARD 17)
file (GLOB SOURCES src/*.cpp )

file (GLOB HEADERS include/*.h)

add_library(libmycoollibrary ${SOURCES})

target_include_directories(libmycoollibrary PUBLIC
    $<BUILD_INTERFACE : ${CMAKE_CURRENT_SOURCE_DIR}/include> 
    $<INSTALL_INTERFACE : include>
    PRIVATE src)

install (TARGETS libmycoollibrary EXPORT MyCoolLibraryConfig
    ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})

install(DIRECTORY include/DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

install(EXPORT RoboUtilsConfig DESTINATION share/MyCoolLibrary/cmake)

export(TARGETS libroboutils FILE MyCoolLibraryConfig.cmake)

Kompilace cmake projektu y p59kayov0ho 58dku

V případě, že máme nějaký projekt, který používá CMake a chceme jej spustit, provedeme to pomocí příkazů:

cd MyCoolRobot
cmake . 
make
./MyCoolRobot

Remote development v prostredi CLion

Clion je integrované vývojové prostředí vyvíjené firmou JetBrains. Jedná se o zajímavou alternativu ke konvenčním prostředím jako je například Eclipse nebo NetBeans. Oproti těmto uvedeným prostředím Clion vyčnívá zejména kvůli své rychlosti, přehlednosti a modernosti. Díky napojení na ekosystém firmy JetBrains je možné do studia doinstalovat spoustu rozšiřujících pluginů přinášejících podporu pro nové jazyky, vývojářské nástroje atp. Velkou výhodou může být i integrovaná práce s Gitem.

V tomto manuálu bude rozebráno použití prostředí Clion v předmětu BPC-PRP tedy jako nástroje pro vývoj firmware mobilního robotu založeného na platformě KAMBot. Nejprve bude stručně rozebrán build system CMake. Poté se manuál již bude věnovat CLionu a to zejména jeho instalaci, vytvoření jednoduchého projektu typu Hello World a jeho zprovoznení. Dále je popsána stěžejní kapitola tohoto manuálu, a to vzdálený vývoj umožňující vzdálené programování Raspberry Pi pomocí standardního počítače. Na konci je popsán jednoduchý projekt blikání LEDkou a kapitola zabývající se odstraňováním nejčastějších problémů se vzdáleným vývojem.

Příklady v tomto manuálu byly zpracovány ve verzi Clion 2018.3, je tedy možné, že se v budoucnosti některé postupy budou měnit.

Instalace CLion

Pro Debian/Ubuntu

sudo snap install clion

Pro Windows

Stáhněte instalační balík ze stránky https://www.jetbrains.com/clion/ po kliknutí na "GET FREE 30 DAY TRIAL".

Spousťte stažený EXE soubor

Registrace studentským účtem

Studenti mají nárok na bezplatné využívání plné verze po dobu studia, což se též týká všech produktů firmy JetBrains.

Získání plné verze je možné pomocí postupu na stránce https://www.jetbrains.com/student/.

Studenti VUT musí pro registraci použít emailovou adresu id@vut.cz

Registraci nalezneme pod Help/Register

Otevření cmake projektu

Založení nového projektu

Nejprve vytvoříme jednoduchý HelloWorld projekt, ve kterém se seznámíme s procesem vytvoření projektu, prostředím a vývojem na lokálním počítači. Po spuštění CLion nás přivítá úvodní obrazovka se seznamem naposledy otevřených projektů, kde klikneme na tlačítko "New Project".

uvodni obrazovka

Po kliknutí se nám otevře okno s konfigurací nového projektu, kde nastavíme cestu, kde chceme mít projekt vytvořený a standard jazyka C++, v našempřípadě C++17.

vytvareni projektu

Po kliknutí na "Create" nás přivítá samotné vývojové prostředí tak jak je zobrazeno na obrazku.

IDE

Pojďme se nyní podívat, co se na obrazovce nachází.

IDE popis

  1. podokno s aktuálně editovaným zdrojovým kódem
  2. aktuálně otevřené soubory
  3. soubory v projektu
  4. zleva: kompilace projektu, výběr targetu, kompilace a spuštění, kompilace aspuštění v debug módu

Verze CMake dodávaná v Raspbianu je bohužel relativně stará, zatím co CLion předpokládá použití relativně novou verzi, pro vyřešení tohoto problému je nutné změnit v souboru CMakeLists.txt verzi CMake z 3.12 na 3.7 tak jak je zobrazeno ve výpisu:

NOTE: Dnes jiz pravdepodobne neplati a Raspbian obsahuje znatelne novejsi verzi CMake.

cmake_minimum_required(VERSION  3.7)
project(HelloWorld)
set(CMAKE_CXX_STANDARD  14)
add_executable(HelloWorld  main.cpp)

Nyní klikneme na tlačítko pro kompilaci a spuštění, kdy se nám nejprve ve spodní části obrazovky zobrazí okno s průběhem kompilace a následně s konzolí spuštěného programu:

Run

Při debugování se breakpointy přidávají kliknutím vedle čísla řádku. Po přidání breakpointu je nutné program spustit v debug módu v horní části okna. Po kompilacise ve spodní části zobrazí okno debuggeru. Navigace v debuggovaném kódu probíhá pomocí šipek v debuggovacím okně.

Debug

Pro přidání nové třídy do projektu klikneme v podokně se soubory projektu na náš projekt pravým tlačítkem, najedeme na "New" a tam zvolíme "C/C++ Class", jak je zobrazeno na obrázku. Při přidávání a odebírání souborů je nutné, aby všechny .cpp soubory byly uvedeny v CMakeLists.txt.

new class

CLion obsahuje spoustu funkcí a možností, jejichž popsání je zcela nad rámec tohoto návodu, doporučuji tedy si s ním pohrát a vyzkoušet, co všechno se v něm dá dělat a jak efektivně. Dobré je rovněž si všímat toho, když je řádek označený žlutě, většinou se jedná o CLion doporučující nějaké zlepšení kódu.

Kompilace programu

Spuštění programu

Ladění programu

Vzdálený vývoj

Vzdálený vývoj (remote development) znamená, že výpočetně náročné vývojové prostředí běží na běžném počítači, zatímco kompilace a spouštění programu probíhána cílovém počítači. Tímto způsobem lze například velmi pohodlně programovat embedded hardware případně servery. Velká výhoda je, že například jde z Windows nebo MacOS programovat aplikace pro Linux, což nemusí vždy být možné kvůli chybějícím knihovnám napřiklad i2c-dev. Funkcionalita vzdáleného vývoje byla doprostředí CLion přidána ve verzi 2018.3.

Pro nakonfigurování vzdáleného vývoje je nutné nejprve přejít do nastaveníprostředí CLion (Preferences). Zde ve stromu vybereme "Build, Execution, Deployment" a položku "Toolchains" viz obrazek. Dále klikneme na tlačítko "+".

toolchain

Následně vyplníme jméno toolchainu a volbu "System" přepneme na "RemoteHost" tak, jak je to zobrazeno na obrazku. Následně klepneme na ikonu složky upoložky "Credentials" a nastavíme je podle konfigurace našeho robotu. Pokud konfigurace Raspberry Pi nebyla měněna, lze použít hodnoty uvedené v tabulce a zobrazené na obrázku. Na Raspberry Pi musí být povoleno SSH.

toolchain toolchain

parametrhodnota
Hostraspberrypi.local
Port22
User namepi
Passwordraspberry

Po kliknutí na OK, se CLion pokusí připojit k danému Raspberry Pi a zkontroluje,zda jsou všechny potřebné programy k dispozici. Připojování je vidět na obrazku a úspěšné připojení je vidět na dalsim obrazku.

toolchain toolchain

Dalším důležitým krokem je konfigurace CMake, ta se provádí rovněž v okně Preferences tak jak je zobrazeno na obrazku.

toolchain Po kliknutí na "+" nakonfigurujeme CMake tak aby používal náš nový toolchain,tak jak je to zobrazeno na obrázku a klikneme na OK.

toolchain

Po kliknutí na OK proběhne upload souborů na Raspberry Pi, po uploadu je nutné přepnout konfiguraci CMake na naši nově vytvořenou jak je zobrazenona obrazku.

toolchain toolchain

Poté následuje reload konfigurace jak je zobrazeno na obrazku.

toolchain

Po kliknutí na tlačítko kompilace a spuštění se náš firmware zkompiluje naRaspberry Pi a spustí se, tak jak je vidět na obrazku, kdy úspěch poznáme podle toho, že se spustitelný soubor spouští z adresáře tmp.

toolchain

Vzdálený vývoj je nyní nakonfigurován

Troubleshooting

V této kapitole bude rozebráno řešení problémů vznikajících při použití vzdáleného vývoje.

CMake 3.12 is required. You are running version 3.7.2

Problém je ve vašem CMakeLists.txt, změňte parametr "cmake_minimum_required".

V Raspberry Pi chybí požadované soubory

Po kliknuti na slozku projektu v levem panelu, kliknete na "Deployment", "Upload to" a vyberte Raspberry Pi.

Ultimátní řešení problémů

Zkontrolujte, jestli v CMakeLists.txt jsou uvedeny všechny soubory, které se mají kompilovat. Zkuste v podokně CMake provést reload, jak je zobrazeno na obrazku. Pokud to nepomůže, přes SSH se připojte k Raspberry Pi, v /tmp smažte momentálně používanou složku pro vzdálený vývoj, restartujte CLion.

toolchain

Integrace GIT v CLionu

GIT je integrován do CLion pomocí tří funkčních vstupů:

Menu

menu

Toolbar

menu

Okno historie

menu

V toolbaru se nacházejí nejpoužívanější zkratky z menu, které jsou velmi snadno identifikovatelné pomocí ikonky.

Z menu lze aktivovat následující funkce:

  • Potvrzení změn
  • Odeslání na server
  • Aktualizace ze serveru
  • Stažení a integrace změn ze serveru
  • Aktualizace odkazů na větve ze serveru
  • Sloučení větví
  • Překořenění větve
  • Zobrazení seznamu větví
  • Nová větev
  • Nový štítek
  • Zahodit změny
  • A další, málo používané operace

Z okna historie lze spustit následující funkce:

  • Prohlížení historie
  • Přepínání mězi větvemi
  • Vytvoření větve v konkrétním bodě historie
  • Sloučení větví

GIT Aktualizace dat ze serveru

V clionu spouštíte ikonkou git pull

CLion/GIT Potvrzení změn

Tuto funkci lze aktivovat z menu Git/Commit nebo kliknutím na zelenou ikonu zaškrtávátka:

toolbar

Po aktivaci vyskočí v levé části okno (ve starých verzích to bylo samostatné okno).

commit

V horní části můžete zaškrtnout které soubory chcete potvrdit, níže vepíšete text, který stručně popisuje tuto změnu a na spodní straně okna tlačítkem Commit potvrdíte změny.

Pozor Změny jsou uloženy pouze u vás, pokud je chcete zveřejnit, musíte provést odeslání na server, nebo využít zkratky Commit and push

CLion/GIT Odeslání na server

Tuto funkci lze aktivovat z menu Git/Push nebo kliknutím na modrou ikonu šipky směřující k sobě:

toolbar

Po aktivaci vyskočí okno

push

V levé části lze vidět vzdálené servery, do kterých je potřeba něco "natlačit", napravo jsou všechny odesílané změny.

Kliknutím na tlačítko Push se lokální data odešlou na server.

POZOR Tlačítko Push je rozbalovací a je možné "natlěčit" na server "hrubou silou" tj Force Push. Tuto funkci nepoužívejte, dochází při ní ke zničení stavu serveru a je velmi obtížné stav obnovit, aby mohl tým bez problému fungovat. Funkce se využívá ve velmi specifických případech, které během výuky nenastanou.

POZOR Může se stát, že obah serveru je novější, než ten který máte u sebe. Může být nutné nejprve aktualizovat projekt předtím než provedete Push (Program vás na to upozorní)!

Vlastnosti jakzyka C++17 / C++20

O těchto vlastnostech jazyka jste se pravděpodobně neučili a bude dobré když je začleníte do svých znalostí

Vynucená inicializace

  struct Trida {
     int A;
     int B;
     double C;
  };
  
  // ...
  Trida instance{1, 2, 3.14159265};  // Vynucena inicializace

Vynucuje inicializaci položek A,B a C v daném pořadí podle deklarace uvnitř objektu. Všechny další nastaví na 0.

Vynucené rozbalení

  struct Trida {
     int A;
     int B;
     double C;
  };
  
  // ...
  Trida instance{1, 2, 3.14159265};  // Vynucena inicializace
  
  auto [a, b, c] = instance;            // vynucene rozbaleni

Jmenný alias

  using NovyNazev = StaryNazev 

Vytvoří zástupné jméno pro komplikovanější zápis. Používá se zejména pro zvýšení čitelnsoti.

Kdekoliv je použit NovyNazev, je to jako byl na daném místě použit datový typ StaryNazev (oba typy jsou zaměnitelné)

Priklad:

  using Msg = std::vector<std::string>;
  using Mediator = std::map<std::string, std::function<void(const Msg &)>>
  
  Mediator mediator;   // odpovica std::map<std::string, std::function<void(const std::vector<std::string>&)>>

Definice callbacku

  std::function<deklarace funkce> Callback;  

Kdykoliv potřebujete zavolat funkci s neznámým cílem funkce (koho máte volat definuje někdo jiný, nadřazený) použijete callback jako ukazatel na funkci. Tato funkce ale narozdíl od C-čkového ukazatele na funkci může být i součástí objektu. Ukazatel na funkce z jazyka C prosím nepoužívejte!

Příklad pouziti:

  void funkce(std::string par) 
  {
    // ...
  }
  
  // ...
  std::function<void(std::string)> Callback;    // Deklarace callbacku
  Callback = &funkce;                           // Přiřazení koho má volat
  
  // ...
  
  if (Callback)                                 // Test zdali odkaz vede na nejaky obsah
    Callback("Ahoj světe");                     // Zavolani obsahu

  // ... 

Lambda

  [zachyt](argumenty) -> navratovy typ { kod funkce }  

Je to deklarace těla funkce bez názvu.

Do zachyt patří všechny proměnné, které funkce potřebuje ke svému běhu, nebo & pokud chceme zachytit všechny automatické proměnné referencí, nebo = pokud kopií. Pokud je v zachyt znak & nebo = a deklarace se nachází uvntř kontextu objektu, tak se implcitně zachytí i tento objekt.

Návratový typ se šipkou nemusí být specifikován, odvodí se pak z kontextu (z datovéhé typu za return)

Argumenty jsou standardní argumenty funkce jako při deklaraci. Pokud je deklarace na vhodném místě ze kterého překladač pozná kontext, můžeme používat auto místo datových typů.

Příklad:

  int a = 42;
  auto f1 = [a]() { cout << a << endl; };                   // a zachyceno hodnotou
  auto f2 = [&a]() { cout << ++a << endl; };                // a zachyceno referenci, muzu jej menit
  auto f3 = [=]() { cout << a << endl; };                   // a zachyceno hodnotou
  auto f4 = [&]() { cout << --a << endl; };                 // a zachyceno referenci, muzu jej menit
  auto f5 = [&]() { return this->NejakaFunkce(); };         // Navratova hodnota dedukovana jako navrat z NejakeFunkce (muze modifikovat objekt protoze zachyt referenci)  
  auto f6 = [=]() { this->NejakaFunkce(); };                // Pozor NejakaFunkce pracuje nad KOPII objektu - modifikuje kopii, ne puvodni objekt
  
  f1();                                                     // lambdu volam stejne jako funkci

Lambdy je dobre pouzivat az od C++17 protoze v C++14 a nizsim maji neintuitivni chovani ve specifickych pripadech a kompilator vyhazuje chyby ktere nejsou na prvni pohled zrejme

Procházení všech prvků kontejneru

  for (auto i: <kontejner>) {
    // operace s prvkem i
  }

Validátor platnosti s deklarací

Pomucka pro citelnost a optimalizaci kodu - omezení platnosti proměnné pouze na úsek, kdy je platná. Pokud není platná, kompilátor ji může zahodit.

  if (auto obj = DejObjekt()) {
    // zde muzu pracovat s obj pokud neni null nebo operator bool() objektu ktery vratila funkce DejObjekt vraci true
  }
  // zde obj neexistuje
  if (auto obj = DejObjekt(); Podminka) {
    // zde muzu pracovat s obj pokud je podminka splnena (vraci true)
  }
  // zde obj neexistuje

Analogicky u while.

STL Structures

Součástí jazyka C++ je tzv. Standard Template Library (STL). Ta obsahuje širokou paletu různých datových struktur a naimplementovaných fukncí. My se dnes zaměříme na část která implementuje datové konteinery.

Knihovna obsahuje implementace pro pole, zásobník, vektor, frontu, list, množinu, mapu (hash_tabulku/dictionary), atd. Dokumentace zde.

My se dnes zamšříme na tři struktury, a totiž std::array, std::vector a std::queue.

Struktury se mezi sebou liší a každá je vhodná pro jiný účel. std::array je struktura v paměti, která má známou svou velikost již během kompilace. Pokusme se nyní takové pole vytvořit, naplnit jej hodnotami a vypočítat průměr.

    #include <array>

    auto my_array = std::array<int, 5>{0, 1, 2, 3, 4};
    int sum = 0;
    for (const auto& val : my_array) {
        sum += val; 
        // sum += my_array.at(i); // equivalent approach
    }
    auto avg = sum / my_array.size();

Vektor se od pole liší tím, že má proměnnou velikost. Vždy když se naplní, tak se automaticky naalokuje navíc jednonásobek jeho současné velikosti.

Vyzkoušíme si naplnit vektor několika hodnotami a najít medián těchto hodnot.

    #include <vector>
    #include <algorithm>
    
    auto my_vector = std::vector<float>{};
    my_vector.push_back(5.4);
    my_vector.push_back(-3.7);
    my_vector.push_back(10.9);
    my_vector.push_back(1.3);
    my_vector.push_back(-6.5);
    my_vector.push_back(-7.8);
    my_vector.push_back(6.4);

    std::sort(my_vector.begin(), my_vector.end());
    auto med = my_vector.at(my_vector.size()/2);
    my_vector.clear();

Strukturu fronty využijeme jako buffer v ilustračním scénáří zpracování příchozích dat z UDP. Uvažujme multivláknový program. Jedno vlákno přijmá data po UDP a plní frontu. Druhé vlákno pracuje asynchronně a vždy, když přijde na řadu, zpracuje všechny doposud přijaté zprávy v pořadí tak, jak příšly.

    #include <queue>
        
    // queue shared between threads; Tip: mutex ?!
    auto my_queue = std::queue<std::string>{};
    
    // receive thread filling queue with messages
    my_queue.push("Message1");
    my_queue.push("Message2");
    my_queue.push("Message3");

    // message processing thread
    void parse_message(const std::string& s) {
        std::cout << "Parsing: " << s << std::endl;
    }

    while (!my_queue.empty()) {
        parse_message(my_queue.front());
        my_queue.pop();
    }

Reference

Reference, někdy také nazývané "alias", je datový typ, který směřuje (je aliasem) na již existujicí objekt v paměti. Při kompilaci je reference obvykle nahrazena ukazatelem, ale z pohledu programátora se jedná o výrazně bezpečnější formu práce s daty a, či objekty, protože nedovoluje některé nebezpečné operace.

Reference se liší od ukazatele ve dvou základních vlastnostech:

  • Nemůže být NULL; reference je vždy nainicializovaná
  • Reference se nemůže přesměrovat na jiný objekt/data.

Pozor, nezaměňovat datový typ reference "&" s operátorem reference "&variable" !

    int a = 5;
    int& b = a;
    const int& c = a;
    b = 10;
    c = 15 // invalid (const ref)
    std::cout << a << std::endl; // a == 10

Reference je často používaná pro předání argumentů fukce bez nutnosti kopírování, či pro sdílení jedněch dat mezi více místy v programu.

    class VeryLargeObject {
    public:
        VeryLargeObject() {}
        const std::array<double, 10000>& data() const {return data_;}
    private:
        std::array<double, 10000> data_;
    };
    
    void porocess_large_data(const VeryLargeObject& d) {
        auto& data = d.data(); // const reference
        auto data = d.data(); // mutable copy
        // ...
    }
    
    auto vlo = VeryLargeObject{};
    porocess_large_data(vlo);

Reference je často pužívaná pro vrácení hodnot z funkce skrze argument funkce. Nejedná se však o best-practice metodu. Pokud je to jen trochu možné, měla by metoda vracet hodnotu skrze návratovou hodnotu. Pokud je potřeba vrátit více hodnot, použijte strukturu jako návratový typ.

    void ops(float a, float b, float& sum, float& sub, float& mul, float& div) {
        sum = a + b;
        sub = a - b;
        mul = a * b;
        div = a / b;
    }

    float sum, sub, mul, div;
    ops(5, 10, sum, sub, mul, div);
    std::cout << sum << " " << sub << " " << mul << " " << div << std::endl;

Smart Pointers

Smart pointery jsou náhradou C-čkových ukazatelů. V základu máme 3 typy těchto smart ukazatelů:

  • std::unique_ptr<T>
  • std::shared_ptr<T>
  • std::weak_ptr<T>

kde T je datový typ na který bude ukazatel ukazovat.

Vyhodou smart pointerů je, že nemusíme jako programátoři bezprostředně řešit alokaci a zejména uvolnění paměti. Jsou li splněny podmínky, smartpointer během svého zániku zavolá také destruktor objektu, na který ukazoval a uvolní naalokovanou paměť.

Výsledkem je, že programátoru už nemusí používat klíčová slova new a delete.

Každý ze smart pointerů se však mírně liší.

std::unique_ptr<T>

std::unique_ptr je nejtriviálnější implementací smart pointeru. Smart pointer je vlastníkem objektu na který ukazuje a neumožní toto vlastnictví (ownership) předat jinému ukazateli. Když unique_ptr zanikne, zavolá destruktor nad vlasněným objektem a dealokuje paměť.

    #include <memory>

    auto unique_int = std::make_unique<int>(5);
    std::cout << *unique_int << std::endl;

    float x = 10;
    auto unique_float = std::make_unique<float>(x);
    std::cout << *unique_float << std::endl;

    std::unique_ptr<float> y = unique_float; // error

std::shared_ptr<T>

std::shared_ptr je příkladem tzv. Automatic Reference Counter (ACR). Idea je, že při vzniku objektu se vytvoří také čítač, který čítá kolik shared_pointerů na tento objekt ukazuje. Když vytvářím nové kopie shared pointeru, čítač roste, když tyto smart pointery zanikají, hodnota čítače klesá.

Když čítač dosáhne nuly, to znamená, že na objekt už nic neukazuje, je automaticky zavolán destruktor a je uvolněná paměť.

Pozor, nezaměňovat s Garbage Collectorem (GC), ten funguje výrazně jinak.

Pozor na cyklické vazby. Pokud dva objekty na sebe navzájem ukazují shared pointerem, ani jeden z objektů nikdy nezanikne. Proto zde máme weak pointery.

    #include <memory>
    
    auto shared_int = std::make_shared<int>(10);
    std::cout << *shared_int << std::endl;
    
    std::shared_ptr<int> x = shared_int;

std::weak_ptr<T>

Obdoba shared_ptr, ale neinkrementuje čitač, který počítá, kolik je platných ukazatelů na daný objekt. To znamená, že pokud na objekt ukazuje 5 weak_ptr a žáden shared_ptr, objekt zanikne.

OOP

Při tvorbě Vaších programů se snažte dodržovat OOP paradigma. Přemýšlejte o programu, jako o sadě black-boxů, kdy tyto schránky jsou každá zaměřená na velmi specifický problém. Každou Vaší třídu by měla vystihovat jedna věta. Stejně tak každá funkce by měla dělat právě jednu věc a nic víc.

Zmíněné blackboxy jsou mezi sebou propojeny a navzájem si předávají data.

Vyhněte se tvorbě "supertříd", tedy tříd, které řeší "všechno". Mějte své třídy úzce specializované.

Běžně by se měla třída vměstnant do 100 řádku. Pokud je třída nad 300 řádků, silně zvažte její rozdělení na více tříd.

Oddělte data od algoritmů. Vytvořte si oddělené třídy, které v sobě mají uložená data a oddelené třídy, které implementují algoritmy pro zpracování dat.

Příklad

Naimplementujte příklad pomocí OOP C++. Při implementaci využijte reference a smart pointery.

Mějme univerzitu. Každá univerzita má 5 ročníků, v každém ročníku je libovolný počet studentů. Když studenti nastupují na univerzitu, jsou automaticky zařazeni do 1. ročníku. Vždy, když proběhne rok, tak univerzita prozkouší všechny studenty v ročnících a s pravděpodobností 0.9 posune studenta do vyžšího ročníku. Pokud student projde pátý ročníku, univerzita si jej zaznamená jako absolventa. Na konci každého roku vytiskněte stav univerzity a všech studentů na ní.

Tip: Třídy a jejich členské proměnné:

Trida Student:
promenne:
    jmeno, 
    prijmeni
metody:
Trida Rocnik:
proměnné:
    seznam_vsech_studentu
metody:
    pridat_studenta_do_rocniku(student)
    evaluovat_ročník() -> seznam_uspesnych_studentu
Třída Univezita:
proměnné:
    seznam_rocniku
    seznam absolventu
metody:
    vykonat_akademicky_rok()
    vytisknout_stav_univerzity();

Implementace:

#include <iostream>
#include <array>
#include <vector>
#include <memory>
#include <random>

class Student {
public:
    Student(const std::string& first_name, const std::string& surname)
        : first_name_{first_name}
        , surname_{surname} {}
    std::string first_name() const {return first_name_;};
    std::string surname() const {return surname_;};
private:
        const std::string first_name_;
        const std::string surname_;
    };
    
    
class Grade {
        static constexpr float change_of_student_passes_grade = 0.8f;
public:
    void add_student(std::shared_ptr<Student> stud) {students_.push_back(stud);}
    std::vector<std::shared_ptr<Student>> evaluate_year() {
        std::vector<std::shared_ptr<Student>> successful_students{};
        std::vector<std::shared_ptr<Student>> failed_students{};
        for (auto& stud : students_) {
            auto random_num = get_random_number(0.0f, 1.0f);
            if (random_num > change_of_student_passes_grade) {
                failed_students.push_back(stud);
            }
            else {
                successful_students.push_back(stud);
            }
        }
        students_ = failed_students;
        return successful_students;
    }
    std::vector<std::shared_ptr<Student>> students() const {return students_;}
    float get_random_number(float min, float max) {
        static std::random_device rd;
        static std::mt19937 gen(rd());
        static std::uniform_real_distribution<float> distr(min, max);
        return distr(gen);
    }
private:
    std::vector<std::shared_ptr<Student>> students_;
};
    
    
class University {
    static constexpr size_t no_of_grades = 5;
public:
    void add_student(std::shared_ptr<Student> stud) {grades_.at(0).add_student(stud);}
    void evaluate_year() {
        for (int i = no_of_grades-1 ; i >= 0 ; i--) {
            auto successfull_studs = grades_.at(i).evaluate_year();
            if (i == no_of_grades-1) { // last grade
                for (auto& stud : successfull_studs) {graduated_.push_back(stud);}
            } else {
                for (auto& stud : successfull_studs) {grades_.at(i+1).add_student(stud);}
            }
        }
    }
    void print_state() {
        for(size_t i = 0 ; i < no_of_grades ; i++) {
            std::cout << "    Grade:" << i+1 << std::endl;
            auto studs = grades_.at(i).students();
            for (const auto& stud : studs) {
                std::cout << "       " << stud->first_name() << " " << stud->surname() << std::endl;
            }
        }
        std::cout << "    Graduated:" << std::endl;
        for (const auto& stud : graduated_) {
            std::cout << "       " << stud->first_name() << " " << stud->surname() << std::endl;
        }
    }
private:
    std::array<Grade, no_of_grades> grades_;
    std::vector<std::shared_ptr<Student>> graduated_;
};
    
int main() {
    University Oxenfurt;
    Oxenfurt.add_student(std::make_shared<Student>("Triss", "Merigold"));
    Oxenfurt.add_student(std::make_shared<Student>("Geralt", "of Rivia"));
    Oxenfurt.add_student(std::make_shared<Student>("Zoltan", "Chivay"));
    Oxenfurt.add_student(std::make_shared<Student>("Yennefer", "of Vengerberg"));
    Oxenfurt.add_student(std::make_shared<Student>("Cirilla", "of Cintra"));
    for (size_t i = 0 ; i < 6 ; i++) {
        std::cout << " ---------- " << std::endl;
        std::cout << "Year " << i+1 << std::endl;
        Oxenfurt.evaluate_year();
        Oxenfurt.print_state();
    }
    return 0;
}

Const

Rychlý přehled užití const v kódu


    // Helpre Object
    class Object {
    public:
        void do_non_const_work() {counter++;} // non-const member method
        void do_const_work() const {std::cout << counter << std::endl;} // const method, can not modify member variables
    private:
        int counter = 0;
    };


     // Variables
     
    int a = 1; // mutable variable
    const int b = 2; // non-mutable (const) variable
    
    
    // References
    
    int& c = a; // mutable reference to a
    const int& d = a; // const reference to a
    
    
    // Pointers
    
    int* e = &a;    // pointer to a
    const int* f = &a;  // pointer to constant a (value of a can not be changed)
    int const* g = &a;  // the same
    *f = 5; // error
    f = e;  // ok
    
    int *const h = &a;  // non-mutable (const) pointer to mutable variable
    h = e;  // error
    *h = 5; // ok
    
    const int * const i = &a; // const pointer to const variable
    *i = 5; // error
    i = e;  // error
    
    
    // Data Structures
    
    std::vector<Object> v1 = {Object{}, Object{}, Object{}};    // Vector of 3 objects
    const std::vector<Object> v2 = {Object{}, Object{}, Object{}};   // constant vector (can not add or remove values from it); returns const refs to object
    v2.push_back(Object{}); // error
    v2.clear(); // error
    v2.at(0).do_non_const_work(); // error
    v2.at(0).do_const_work(); // ok
    
    
    // Smart Pointers
    
    std::shared_ptr<int> sp1 = std::make_shared<int>(5); // normal shared pointer
    std::shared_ptr<const int> sp2 = sp1; // shared pointer to const value
    *sp2 = 5; // error
    sp2 = sp1; // ok
    const std::shared_ptr<int> sp3 = sp1;   // constant pointer to mutable value
    *sp3 = 5; // ok
    sp3 = sp2; // error
    const std::shared_ptr<const int> sp4 = sp1;
    *sp4 = 5; // error
    sp4 = sp2; // error
    
    
    // Const vs Constexpr vs Define
    
    const int x = 5; // this variable can be initialized in runtime (read user input)
    #define Y = 5 // const defined for preprocessor (non type safe)
    constexpr int y = 5; // this variable MUST be initialized in compile-time (similar to #define Y 5, but type-safe)
    
    
    // "Rustification"
    
    #define let const auto
    #define mut auto
    
    let a = 5;  // cosnt variable
    mut b = 3;  // mutable variable
    let& c = a; // const reference
    mut& d = b; // mutable reference

Standardní kontejnery v C++

std::string

Je to kontejner, pro obyčejný text. Obsahuje základní funkce pro práci s textem.

Může sice obsahovat UTF-8, ale nepočítá jeden znak UTF-8 jako primitivu, ale jako sekvenci několika bajtů.

std::vector<typ>

Je to dynamické pole, které obsahuje prvky specifikovaného datového typu.

Jedotlivé prvky jsou indexovány od nuly

  std::vector<int> vektor{
    1,3,5,7,11,13,17,
  };
  
  vektor[5] = 4;                              // zápis do položky
  cout << vektor[0] * vektor[2] << endl;      // čtení z položky
  
  if (auto p = vektor.find(13); p != vektor.end()) {  // hledání prvku
    cout << *p << endl;                       // prvek se nachází ve vektoru
  }

std::unordered_map<klíč,typ>

Je to adresovatelný kontejner, kde každá hodnota má specifikovaný klíč, pod kterým ji lze najít. Kontejner zajišťuje oproti std::map co nejrychlejší adresaci za pomocí hashe.

  std::unordered_map<std::string, double> mapa{
    { "PI", 3.14159},
    { "E", 2.71828},
  };
  
  mapa.emplace_back
  mapa["FN"] = 598722.4879;                         // definice Baštincova čísla  (zápis do položky)
  cout << mapa["E"] * mapa["E"] << endl;            // čtení z položky
  
  if (auto p = mapa.find("ZY"); p != mapa.end()) {  // hledání prvku
    cout << p->second << endl;                      // ZY se nachází v mapě
  }

Standardní algoritmy v C++

Návrhové vzory (design patterns)

Builder

Observer

Mediator

State

Robotic Operating System 2

Název "Robot Operating System" poněkud klame svým zněním. Nejedná se o samostaný operační sýstém, nýbrž spíše o middle-ware, tedy softwarový nástroj (knihovnu), který pomáha propojit dílčí programy do komplexnejšího celku. V praxi si to můžeme představit tak, že máme jednoduchou aplikaci pro robot jezdící po čáre, kterou realizujeme pomocí 3 navzájem spolupracujících programů (příklad funguje jako ilustrační; takový robot samozřejmě můžeme naprogramovat pomocí jednoho programu; ilustrujeme tím ale komplexnější problém). První program vyčítá data ze snímače a provádí jednoduchou filtraci dat. Druhý program je mozkem celého řešení a rozhoduje o pohybu robotu. Třetí program pak přijímá řídicí pokyny a na jejich základě ovládá motory.

Rviz

Obr: Schéma fungování pomyslého line-tracking robotu na platformě Rasperry Pi s použitím ROSu.

V případě absence ROSu bychom museli vymyslet způsob jak spolu budou tyto tři programy komunikovat. Mohli bychom sdílet paměť, pipovat, posílat si IP zprávy, používat DBus, atd. Všechny tyto techniky fungují, ale vyžadují určitý programátorský um. My se těmito nízkouúrovňovými problémy nechceme zabývat a proto použijeme ROS.

V praxi si pak můžeme říct, že ROS komunikuje mezi procesy právě pomocí posílání UDP paketů. To umožňuje také komunikovat procesům, které běží na různých počítačích. Tomu říkáme distribuovaný systém.

Základ ROSu je postaven na 3 stavebních kamenech.

  • ROS Node
  • ROS Topic
  • ROS Message

ROS Node - Nodem je myšlený každý program do kterého přídáme klientskou knihovnu ROSu. Naučíme tedy program používat funkce ROSu. ROS Node je pak schopen "automaticky" objevit další instance (programy), které jsou spuštěny na stejné síťi a navázat s nimi komunikaci.

ROS Topic - Doména, ve které se posílá specifický okruh ROS Messagů.

ROS Message - Jedna instance odeslané zprávy. V rámci ROSu je možné posílat jenom zprávy, které jsou striktně zadefinovány a mají svůj jasně daný formát. Často obsahují také časovou značku, kdy byly odeslány.

Dále si zadefinujme dva typy postavení ROS Nodů při komunikaci.

Subscriber - ROS Node, který přijímá všechny zprávy v rámci daného ROS Topicu.

Publisher - ROS Node, který vytváří a odesíla zprávy v rámci daného ROS Topicu.

Náš robot-sledující-čáru příklad si pak můžem ilustrovat takto:

Rviz

Napíšeme zmíněné 3 programy. Jeden pro čtení dat ze snímače, druhý pro rozhodování jak se pohybovat a třetí pro ovládání motorů. První program (Node) vystaví svůj topic "SensorData" jako publisher. Druhý se přihlásí k odebírání zpráv jako subscriber v témuž topicu. V tuto chvíli dojde k navázání spojení a všechny zprávy publikované na tomto topicu budou směrovány k subsriberovi. Když pak první program přečte data ze snímače, vyfiltruje je a vytvoří z nich message, kterou odešle. Obdobným způsobem se vymění data i mezi druhým a třetím programem, pouze pod hlavičkou jiného topicuu.

Nyní máme vytvořené všechny tři programy. Ty spolu komunikují, ale robot přesto nefunguje podle přestav. Tušíme, že chyba je v tom, jak druhý program převádí data ze snímače na pohyb kol. Proto si napíšeme 4. program, který bude poslouchat veškerou komunikaci a bude ji logovat do souboru. Náš nový program tedy bude subscriberem pro oba dříve zavedené topicy "SensorData" a "MotorControl". V okamžiku kdy tento program zapneme, tak se ohlásí publisherům (Nodům, které data publikují) a od tohoto okamžiku všechny zprávy odeslané v topicích "SensorData" a "MotorControl" budou posílány také našemu logovacímu programu. Ten zprávy přijme a jejich obsah včetně časové značky vytiskne do souboru. Když se pak do souboru podíváme, zjistíme, že plánovací program vytváří akční zásah vždy s opačným znaménkem, proto přídáme "-" do výpočtu akčního zásahu a vše začne fungovat.

Instalace ROS2

Tento návod je pouze českým přepisem oficiální dokumentace (Instalace pro Ubuntu)[https://docs.ros.org/en/humble/Installation/Ubuntu-Install-Debians.html]. Primárně prosím používejte oficiální verzi. Tento návod je pouze doprovodný.

Instalace je doporučená na distribuci Ubuntu 22.04 LTS (long term stable). Instalovat budeme verzi ROSu z roku 2022, Humble.

Povolíme přístup do Ubuntu Universe repository

sudo apt install software-properties-common
sudo add-apt-repository universe

Přidáme do Linuxu repozitáře (servery) ze kterých je možné stáhnout ROS.

sudo apt update && sudo apt install curl
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null

Necháme baličkovací systém načíst nově přidaná data.

sudo apt update
sudo apt upgrade

Samotný ROS nainstalujeme tímto příkazem. Trvá cca 10 min.

sudo apt install ros-humble-desktop

V neposlední řadě nainstalujeme build system zvalý colcon

sudo apt install python3-colcon-common-extensions

A na závěr si do souboru ~/.bashrc přídáme záznam o načítání ROSu do proměnného prostředí, kdykoliv zapneme terminál.

echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
source ~/.bashrc

ROS je nyní nainstalován. Pokud vše proběhlo v pořádku, jste nyní schopni provést příkaz

ros2

Tvorba vlastního nodu

Vytvoříme si jednoduchou aplikaci, kde jeden node bude odesílat zprávu s pořadovým číslem a časovou značkou a druhý node zprávu přijme, vypíše a zjistí, s jakým zpožděním zpráva došla.

Nejprve si vytvoříme tzv. workspace pro náš projekt. Workspacem se myslí speciálně uspořádaná složka.

cd ~/
mkdir -p ros_ws/src
cd ros_ws/src

C++ Node

Dále si vygenerujeme nový balíček (package) - Nutno spouštět ve složce ~/ros_ws/src.

ros2 pkg create --build-type ament_cmake cpp_publisher

Příkaz nám říká, že budeme volat program pkg create a chceme po něm, aby nám vytvořil balíček cpp_publisher.

Pokud se Vám stane, že předchozí příkaz neprojde z důvodu nedostatečných práv, vraťte se o složku zpět (cd ~/ros_ws) a upravte přístupová práva - pak zkuste balíček vytvořit znovu

sudo chmod 777 -R .

Nyní se náš balíček skládá z několika následujícíh souborů

~/ros_ws/src/cpp_publisher/
    include/
    src/
    CMakeLists.txt
    package.xml

Do adresářů include a src budeme ukládat naše zdrojové kódy a soubory CMakeLists.txt a package.xml slouží ke kompilaci balíčku.

CMakeLists.txt a package.xml obsahují velké množství předpřipravených direktiv, které slouží složitějším příkladům. Pro naše potřeby si můžeme tyto dva soubory smazat.

cd ~/ros_ws/src/cpp_publisher/
rm CMakeLists.txt
rm package.xml

Pomocí programu nano nebo vim si oba soubory znovu vytvoříme a přidáme následující obsah.

nano CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(cpp_publisher)

## Set CMAKE standard and flags
SET(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic")
set(CMAKE_CXX_FLAGS_DEBUG " ${CMAKE_CXX_FLAGS_DEBUG} -g")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")

## Find catkin and any catkin packages
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

## Build talker and listener
include_directories(
        include
        ${rclcpp_INCLUDE_DIRS}
        ${std_msgs_INCLUDE_DIRS}
        )

add_executable(publisher src/main.cpp)
ament_target_dependencies(publisher rclcpp std_msgs)

install(TARGETS publisher DESTINATION lib/${PROJECT_NAME})

ament_package()

a

nano package.xml
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>cpp_publisher</name>
  <version>0.0.0</version>
  <description>The cpp_publisher package</description>

  <maintainer email="my@email.todo">cpp_publisher</maintainer>

  <license>TODO</license>

  <buildtool_depend>ament_cmake</buildtool_depend>
  <depend>rclcpp</depend>
  <depend>std_msgs</depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

Nyní si můžeme vytvořit soubor main.cpp ve složce src a do něj napíšeme vlastní program

nano src/main.cpp
#include "rclcpp/rclcpp.hpp"
#include <std_msgs/msg/header.hpp>

class TimestampPublisher : public rclcpp::Node {
public:
        TimestampPublisher(): Node("timestamp_publisher") {
                publisher_ = this->create_publisher<std_msgs::msg::Header>("timestamp_topic", 10);
                using namespace std::chrono_literals;
                timer_ = this->create_wall_timer(10ms, std::bind(&TimestampPublisher::timer_callback, this));
        }

private:
        void timer_callback() {
                auto message = std_msgs::msg::Header();

                message.stamp = this->get_clock()->now();
                message.frame_id = "origin";

                publisher_->publish(message);
        }

        rclcpp::TimerBase::SharedPtr timer_;
        rclcpp::Publisher<std_msgs::msg::Header>::SharedPtr publisher_;
};

int main(int argc, char **argv) {
        rclcpp::init(argc, argv);
        rclcpp::spin(std::make_shared<TimestampPublisher>());
        rclcpp::shutdown();
        
        return 0;
}

Nyní se vrátíme do kořene našeho workspacu a zavoláme příkaz pro build celého workspacu.

cd ~/ros_ws
colcon build

Pokud se nevypíše žádná chyba, máme hotový publisher, který je uložený v ~/ros_ws/src/cpp_publisher/install.

Aby si Linux načetl nově zkompilované programy z našeho ros_ws přidámi si tento workspace do systémového prostředí (environmentu).

source ~/ros_ws/src/cpp_publisher/install/setup.bash

Abychom tuto akci již nemuseli opakovat přidáme si tento řádek také do ~/.bashrc

echo "source ~/ros_ws/src/cpp_publisher/install/setup.bash" >> ~/.bashrc

Nyní si dvě okna terminálu. V jednom spustíme námi vytvořený publisher

ros2 run cpp_publisher publisher

A ve druhém si poslechneme zprávy na topicu /timestamp_topic

ros2 topic echo /timestamp_topic

Pokud vidíte v terminále výpis zpráv, vše pracuje, jak má.

Python Node

Vytvoříme si další balíček pomocí

cd ~/ros_ws/src/
ros2 pkg create --build-type ament_python python_subscriber

a upravíme si strukturu balíčku tak, aby vypadala následovně.

~/ros_ws/src/python_subscriber/
    python_subscriber/
        __init__.py
        python_subscriber.py
    resource/
        ...
    test/
        ...
    setup.cfg
    setup.py
    package.xml

Složka python_subscriber bude soužit k uložení hlavního skriptu python_subscriber.py. package.xml obdobně jako pro C++ příklad. setup.py a setup.cfg slouží k instalaci python balíčku do workspacu.

Zmíněné soubory si pak upravíme následovně.

nano package.xml
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>python_subscriber</name>
  <version>0.0.0</version>
  <description>TODO</description>
  <maintainer email="todo@todo.todo">python_subscriber</maintainer>
  <license>TODO</license>

  <exec_depend>rclpy</exec_depend>
  <exec_depend>std_msgs</exec_depend>

  <!-- These test dependencies are optional
  Their purpose is to make sure that the code passes the linters -->
  <test_depend>ament_copyright</test_depend>
  <test_depend>ament_flake8</test_depend>
  <test_depend>ament_pep257</test_depend>
  <test_depend>python3-pytest</test_depend>

  <export>
    <build_type>ament_python</build_type>
  </export>
</package>

pak

nano setup.py
from setuptools import setup

package_name = 'python_subscriber'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='TODO',
    maintainer_email='todo@todo.todo',
    description='TODO',
    license='TODO',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
                'python_subscriber = python_subscriber.python_subscriber:main',
        ],
    },
)

a finálně

nano python_subscriber/python_subscriber.py
import rclpy
from rclpy.node import Node

from std_msgs.msg import Header


class TimestampSubscriber(Node):

    def __init__(self):
        super().__init__('timestamp_subscriber')
        self.subscription = self.create_subscription(
            Header,
            'timestamp_topic',
            self.listener_callback,
            10)
        self.subscription  # prevent unused variable warning

    def listener_callback(self, msg):
        self.get_logger().info('I heard: "%s"' % msg.stamp.nanosec)


def main(args=None):
    rclpy.init(args=args)

    timestamp_subscriber = TimestampSubscriber()

    rclpy.spin(timestamp_subscriber)

    # Destroy the node explicitly
    # (optional - otherwise it will be done automatically
    # when the garbage collector destroys the node object)
    timestamp_subscriber.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Nyní se můžeme vrátit do kořene workspacu a vše zkompilovat.

cd ~/ros_ws/
colcon build

Zaktualizujeme si proměné prostředí.

echo "source ~/ros_ws/src/python_subscriber/install/setup.bash" >> ~/.bashrc
source ~/.bashrc

Pokud máme aktivní cpp_publisher, pak zapnene python_subscriber node v dalším okně pomocí

ros2 run python_subscriber python_subscriber

a vidíme výpis přijímaných zpráv.

Pomocí programu rqt_graph si můžeme prohlédnout aktuální stav propojení nodů.

ros2 run rqt_graph rqt_graph

rqt_graph

Obr: vizualizace komunikace mezi nody pomocí rqt_graph

Rviz

Rviz je vizualizační nástroj, který je dodáván jako součást ROSu. Jedná se o aplikaci, která dokáže poslouchat širokou paletu předdefinovaných ROS zpráv a vizualizovat je v 3D grafickém prostředí.

Obvykle Rviz používáme pro vizualizaci pointcloudů (mračna bodů z LIDARu), obrázků z kamery, vykreslování geometrických primitiv v prostoru, vizualizace occupancy grid map, atd.

Rviz aktivujeme pomocí

rviz2

Vizualizaci konkrétního topicu pak aktivujeme pomocí

Add -> By topic -> [náš topic]

V sekci

Add -> By display type

vidíme všechny podporované typy zpráv (viz online dokumentace ROSu).

rqt_graph Obr: příklad vizualizace pointcloudu a kamery v Rvizu

Nyní si zkusme vytvořit vlastní Node, který bude vykreslovat geometrické primitivum do RVizu. Vyjděme z příkladu cpp_publisher a vytvoříme následujicí program.

cd ~/ros_ws/src
ros2 pkg create --build-type ament_cmake cpp_rviz_publisher

CMakeLists.txt

cmake_minimum_required(VERSION 3.8)
project(cpp_rviz_publisher)

## Set CMAKE standard and flags
SET(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic")
set(CMAKE_CXX_FLAGS_DEBUG " ${CMAKE_CXX_FLAGS_DEBUG} -g")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")

## Find catkin and any catkin packages
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(visualization_msgs REQUIRED)

## Build talker and listener
include_directories(
        include
        ${rclcpp_INCLUDE_DIRS}
        ${std_msgs_INCLUDE_DIRS}
        ${visualization_msgs_INCLUDE_DIRS}
        )

add_executable(rviz_publisher src/main.cpp)
ament_target_dependencies(rviz_publisher rclcpp std_msgs visualization_msgs)

install(TARGETS rviz_publisher DESTINATION lib/${PROJECT_NAME})

ament_package()

package.xml

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>cpp_rviz_publisher</name>
  <version>0.0.0</version>
  <description>The cpp_rviz_publisher package</description>

  <maintainer email="my@email.todo">cpp_rviz_publisher</maintainer>

  <license>TODO</license>

  <buildtool_depend>ament_cmake</buildtool_depend>
  <depend>rclcpp</depend>
  <depend>std_msgs</depend>
  <depend>visualization_msgs</depend>
  
  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

src/main.cpp

#include "rclcpp/rclcpp.hpp"
#include <visualization_msgs/msg/marker.hpp>

class CubePublisher : public rclcpp::Node {
public:
        CubePublisher(): Node("cube_publisher") {
                publisher_ = this->create_publisher<visualization_msgs::msg::Marker>("cube_topic", 10);
                using namespace std::chrono_literals;
                timer_ = this->create_wall_timer(100ms, std::bind(&CubePublisher::timer_callback, this));
        }

private:
        void timer_callback() {
                auto marker = visualization_msgs::msg::Marker();
                marker.header.frame_id = "map";
                marker.header.stamp = this->get_clock()->now();
                marker.ns = "cube";

                marker.id = 0;
                marker.type = visualization_msgs::msg::Marker::CUBE;
                marker.action = visualization_msgs::msg::Marker::ADD;

                marker.pose.position.x = sin(pose_);
                marker.pose.position.y = cos(pose_);
                marker.pose.position.z = 0.1*sin(5*pose_);

                marker.pose.orientation.x = 0.0;
                marker.pose.orientation.y = 0.0;
                marker.pose.orientation.z = 0.0;
                marker.pose.orientation.w = 1.0;

                marker.scale.x = 0.1;
                marker.scale.y = 0.1;
                marker.scale.z = 0.1;

                marker.color.a = 1.0;
                marker.color.r = 0.0;
                marker.color.g = 1.0;
                marker.color.b = 0.0;

                pose_ += 0.01;

                publisher_->publish(marker);
        }

        rclcpp::TimerBase::SharedPtr timer_;
        rclcpp::Publisher<visualization_msgs::msg::Marker>::SharedPtr publisher_;
        float pose_ = 0;
};

int main(int argc, char **argv) {
        rclcpp::init(argc, argv);
        rclcpp::spin(std::make_shared<CubePublisher>());
        rclcpp::shutdown();

        return 0;
}

V Rvizu si pak otevřeme topic /cube topic.

Kam dál?

Tento tutoriál je popisuje pouze malý zlomek všech možných funkcionalit této obšírné platformy.

Oficiální web [1] - http://www.ros.org/

Oficiální tutoriály [2] - http://wiki.ros.org/ROS/Tutorials

Naučit se používat ROS Services [6]

Seznamy několika předdefinovaných ROS Messagů - [3] [4]

Pro reálnou práci se zdrojovými kódy je vhodné použít nějaké IDE. V případě Linuxu vřele doporučuji programy od JetBrains, CLion pro vývoj C++ a Pycharm pro práci s pythonem. Oba programy jsou pro studenty VUT zdarma.

Reference

[1] http://www.ros.org/

[2] http://wiki.ros.org/ROS/Tutorials

[3] http://wiki.ros.org/std_msgs

[4] http://wiki.ros.org/sensor_msgs

[6] http://wiki.ros.org/Services

[7] http://wiki.ros.org/ROS/Installation

Supersmyčka a regulace v ní

const int T = 30;
long long ms = millis() + T;
while(rum) {
    comm.send(sensor.BuildReads());
    comm.send(drive.BuildReads());
    
    while (ms < millis())
      comm.loop();
      
    ms = millis() + T;
    
    sensor.ProcessLineSensors();
    drive.ComputeOdometry();
    
    controller.Control();                                                       
    
    drive.ComputeMotorRamps();        
    comm.send(sensor.BuildWrites());
    comm.send(drive.BuildWrites());
  }

Mediator - Protokolový eventový parser

Controller

Někdy je třeba řídit robot v různých okamžicích různým způsobem (např. robot nejprve musí zkalibrovat senzor čáry, aby mohl vyrazit na soutěžní trať ), přičemž potřebujeme, aby důležité části chodu supersmyčky probíhaly ve správném čase

Ideální je implementace stavového automatu. Jde ji řešit hloupým a nepřehledným způsobem switch-case nebo funkcionálně

Funkcionální přístup

Nejprve musíme deklarovat ukazatel na prováděcí funkci stavu.

Pokud budou všechny stavy obsluhovány jedním objektem, je možné vytvořit ukazatel na metodu aktuálního objektu.

using State = void(StateController::*)();                                       // ukazatel na stavovou funkci
State state;                                                                    // stav automatu
state = &StateController::DoSearchLine;                                         // změna stavu
(this->*state)();                                                               // vyvolání stavové funkce

Pokud však bude obsluha ve více objektech, je nutné použít lambda funkcí a std::function

using State = void();                                                           // ukazatel na stavovou funkci
std::function<State> state;                                                     // stav automatu
state = [=](){ regulator->DoSearchLine(); }                                     // změna stavu
(*state)();                                                                     // vyvolání stavové funkce

Doporučuji první způsob.

Řešení funkcionálním přístupem s pomocí metod aktuálního objektu

Deklarace Controlleru

class StateController {
  using State = void(StateController::*)();                                     // MAGIE: ukazatel na metodu controlleru
public:
  State state{&StateController::DoSearchLine}                                   // Robot po startu zacne hledat caru, prvni stav
  
  void Control();                                                               // funkce která bude volána z main
  
  // jednotlive stavove "funkce"
  void DoSearchLine();
  void DoFollowLine();
  void DoTryFollowMissingLine();
  void DoDanceOnFloor();
  void DoBurnEverything();
  // ...
}

Příklad implementací jednotlivých stavů automatu:

void StateController::Control()
{
  (this->*state)();                                                             // MAGIE: provede volání aktuálně vybrané funkce
}

// Robot hleda caru
void StateController::DoSearchLine() 
{
  if (!sensor.LineMissing)
    state = &StateController::DoFollowLine;
}  

// robot reguluje pozici na care
void StateController::DoFollowLine()
{
    drive.Regulate(ForwardSpeed, sensor.ComputedDistanceFromLine);
    
    if (sensor.LineMissing)
      state = &StateController::DoTryFollowMissingLine;
}

// robot se pokousi znovu nalezt prerusenou caru
void StateController::DoTryFollowMissingLine()
{
    if (!sensor.LineMissing)
       state = &StateController::DoTryFollowMissingLine;
}

A samozřejmě supersmyčka v main:

StateController controller{&drive, &sensor};                                    // konstrukce controlleru nad daty

while(rum) {
    comm.send(sensor.BuildReads());
    comm.send(drive.BuildReads());
    
    while (ms < millis())
      comm.loop();
      
    ms = millis() + T;
    
    sensor.ProcessLineSensors();
    drive.ComputeOdometry();
    
    controller.Control();                                                       // provede jeden krok aktualniho stavu
    
    drive.ComputeMotorRamps();        
    comm.send(sensor.BuildWrites());
    comm.send(drive.BuildWrites());
  }

Přístup switch-case

Deklarace Controlleru

enum State {
    SearchLine,
    FollowLine,
    TryFollowMissingLine,
    DanceOnFloor,
    DoBurnEverything,
  };                                                                            // Deklarace stavu
  
class StateController {
public:
  State state{SearchLine};                                                      // Robot po startu zacne hledat caru, prvni stav
  
  void Control();                                                               // funkce která bude volána z main
}

Příklad implementací jednotlivých stavů automatu:

void StateController::Control()
{
  switch (state) {
  default:
  case SearchLine:
    if (!sensor.LineMissing)
      state = FollowLine;
    break;  
    
  case FollowLine:
    drive.Regulate(ForwardSpeed, sensor.ComputedDistanceFromLine);
    
    if (sensor.LineMissing)
      state = TryFollowMissingLine;
    break;
      
  case TryFollowMissingLine:
    if (!sensor.LineMissing)
      state = FollowLine;
    break;
  }  
}

Poznámka: V příkladech chybí deklarace lokálních proměnných objektu, příklady jsou uvedeny jen jako vzorové, pro pochopení funkce. Konkrétní implementaci si musíte vytvořit sami.

Chytrého nakopni, hloupého kopni, blbého zakopej 4 metry pod zem ...

Procházení komunikačním protokolem ISO/OSI

Doporučená organizace projektu

Rozdělení do tříd (abecedně):

  • Comm - Obsluha kompletní komunikace
  • Configuration - Konfigurace načtená z yaml souboru
  • Controller - Hlavní logika ovládání programu
  • Drive - Obsluha a řízení podvozku
  • Nmea - Implementace převodu NMEA zpráv
  • Sensor - Obsluha měření a identifikace čáry
  • main.cpp - Supersmyčka

Doporučen způsob práce kdy za funkcionalitu jedné třídy zodpovídá jedna osoba (ideálně se jménem napsaným v komentáři na začátku souboru). Nikdo jiný než vybraná osoba nesmí daný soubor commitovat.

Main je kolizní, můžou do něj všichni, ideálně v malých jednořádkových commitech. Main pokud možno modifikovat jen když jste všichni spolu a definujete API (rozdáváte práci)

main.cpp

Řeší:

  • obsluha argumentů příkazové řádky
  • instanciace ostatních tříd
  • hlavní supersmyčka

Závisí na:

  • Comm - instanciace a obsluha v supersmy4ce
  • Drive - instanciace a obsluha
  • Sensor - instanciace a obsluha
  • Configuration - instanciace, načtení
  • Controller - instanciace a volání v supersmyčce

POZOR KOLIZNÍ SOUBOR (commitovat vždy zvlášť, modifikovat pouze velmi malé změny kvůli častým konfliktům)

Nmea

Řeší:

  • převod z frame na zprávu
  • převod ze zprávy na frame

Závisí na:

  • nic

Realizace:

  • Implementace 4. týden
  • Spolehlivost 6. týden

Comm

Řeší:

  • obsluha příjmu a odesílání UDP zpráv
  • Mediátor zpráv do ostatních objektů

Závisí na

  • Nmea - převod protokolu
  • Configuration VOLITELNE - port

Realizace:

  • Implementace 6. týden
  • Spolehlivost 8. týden

Drive

Řeší:

  • Obsluha komunikace s podvozkem
  • Výpočty nad podvozkem
  • Odometrie

Závisí na

  • Nmea - deklarace zpráv
  • Configuration VOLITELNE - poloměr kola, rozteč

Realizace:

  • Implementace 6. týden
  • Spolehlivost 8. týden

Sensor

Řeší:

  • Obsluha komunikace se senzory
  • Parametrizace /detekce pozice čáry

Závisí na

  • Nmea - deklarace zpráv
  • Configuration VOLITELNE - pozice senzoru

Realizace:

  • Implementace 7. týden
  • Spolehlivost 8. týden

Configuration

  • Načítání parametrů z yaml souboru
  • Volitelná třída, lze implementovat za pomocí konstant v kódu
  • výhodné použití yaml-cpp knihovny

Závisí na

nic

Realizace:

  • Implementace 8. týden
  • Spolehlivost 10. týden

Controller

Řeší:

  • Logika jízdy robotu
  • Stavový automat pro:
    • inicializace / kalibrace senzorů
    • jízda po čáře (regulátor)
    • jízda po přerušené čáře
    • jízda po křižovatce

Závisí na

  • Drive - zápis rychlostí robotu
  • Sensor - čtení čáry
  • Configuration VOLITELNE - délka přerušení

Realizace:

  • Implementace 8. týden
  • Spolehlivost 10. týden

Oddělení modulů - Zrychlení kompilace

Při návrhu objektového API se setkáváme s nutností oddělení jednotlivých modulů tak, aby navzájem spolupracovaly (jeden modul využívá druhý), ale současně změna v impleentaci jednoho modulu nevyvolala nutnost rekompilace modulu druhého. Typickým příkladem je vazba nadřazený-podřazený, ale lze takto vytvořit i cyklickou závislost

Velmi špatným řešením v aplikacích bývá vytvoření globální proměnné.

Řešení lze docílit měkkou vazbou přes ukazatel na nedefinovaný objekt (tzv forward).

Příklad: Máme objekt Controller, který ve svých metodách potřebuje používat funkce z objektů Drive a Sensor. Objekty Drive a Sensor jsou singletony (existují v aplikaci v právě jedné instanci, vytvořené nejspíše jako lokální proměnné ve funkci main.

Forward

Aby mohly jednotlivé metody Controlleru pracovat s Drive, musí mít Controller v sobě uložený odkaz na instanci objektu Drive. Nejspíše takto:

#include "Drive.h"

class Controller {
public:
  Drive *drv;
}

Pokud však s třídou Drive pracuje pouze implementace Controlleru, kompilátor potřebuje pouze vědět, že Drive je třída (a nic víc!) a není tak potřeba vkládat celý soubor s definicí API třídy Drive. Lze toto zjednodušit na:

class Drive;    // pouze forward

class Controller {
public:
  Drive *drv;
}

a soubor s deklarací Drive vkládáme až do cpp souboru

#include "Controller.h"
#include "Drive.h"

Controller:: ......

Toto lze použít při splnění podmínek:

  • Všechny přístupy ke třídě Drive jsou přes ukazatel, nikoliv přímo
    • instance objektu potřebuje znát velikost tedy plnou deklaraci !
  • k obsahu drv se nepřistupuje v headeru ale v kódu cpp.
    • Jakékoliv inline funkce používající obsah drv potřebují znát plnou deklaraci !

Neměnný ukazatel

Dále drive by měl být konstantní ukazatel na nekonstantní instanci třídy Drive. Ukazatel nechceme nikdy měnit, ale vlastní objekt měnit můžeme. S tím kam umístíme const bývá začátečnický problém, pro shrnutí:

Drive *drv;               // ukazatel na Drive. Lze změnit ukazatel i Data na které ukazuje.
const Drive *drv;         // ukazatel na konstantní Drive. Lze změnit ukazatel, Data na které ukazuje lze pouze číst.
Drive * const drv;        // konstantní ukazatel na Drive. Lze změnit data, na které ukazuje. Ukazatel lze pouze číst.
const Drive * const drv;  // konstantní ukazatel na konstantní Drive. Data i ukazatel lze pouze číst.

Dále nechceme, aby do ukazatele v inicializaci instance Controlleru někdo vložil nullptr (nedává to v API smysl, bez instance Drive neumí Controller žít). Navíc nechceme použít pointerovou aritmetiku (z principu se jí vyhýbáme, kde to jde). Místo konstantního ukazatele Drive * const ptr použijeme referenci Drive & ptr. Pozor reference je vždy konstantní, takže nám jedno const odpadá !

Drive & drv;              // reference na Drive. Lze změnit Data na které ukazuje. Ukazatel ne.

Vzorové řešení

Objekt Controller tedy deklarujeme v headeru takto:

class Drive;    // pouze forward

class Controller {
public:
  Controller(Drive & aDrive); 
  
  void Method();
  
private:
  Drive & drv;
}

A implementujeme v cpp souboru takto:

#include "Controller.h"
#include "Drive.h"          // metody controlleru už smí používat API Drive

Controller::Controller(Drive & aDrive)
 : drv{aDrive}
{
} 

void Controller::Method()
{
  drv->CallMe();            // Můžeme používat API
}

Cyklická závislost

Stejným způsobem lze definovat, že Drive může přistupovat k prvkům Controlleru. Standardním způsobem deklarace bysme vytvořili cyklickou závislost mezi headery a kód by (logicky) nešel zkompilovat:

Drive.h:

#include "Controller.h"

class Drive {
 Controller & ctrl;
}

Controller.h:

#include "Drive.h"

class Controller {
 Drive & drv;
}

Řešení cyklické závislosti bylo již uvedeno:

class Controller;

class Drive {
 Controller & ctrl;
}

Controller.h:

class Drive;

class Controller {
 Drive & drv;
}