Fyzikální kabinet FyzKAB
Pomůcky „Bastlení“ k systému Vernier Vernier Go!Temp + modul Arduino Vylepšené načítání Go!Temp modulem Arduino

Vylepšené načítání USB teploměru Vernier Go!Temp modulem Arduino

Aneb nový návrat na staré místo činu.

Proč se po letech vracet ke Go!Temp?

Před několika lety jsme se zabývali možností načítání USB teploměru Vernier Go!Temp Modulem Arduino. Problém se nám tehdy podařilo vyřešit na principu úpravu kódu původně určenému pro načítání USB joysticku. Tehdejší řešení bylo funkční, ale upřímně řečeno to bylo takové řešení stylu: „Funguje to, prodáme to!“

Dnes bychom se chtěli k problematice načtení teploty z USB teploměru Vernier Go!Temp trochu vrátit a zkusit zde prezentovat nový program, který nyní vychází z několika poznatků o komunikaci USB HID, které jsme za ta léta nasbírali.

Důležitá oprava z předchozích verzí (Ošetření záporných teplot):
V předešlých článcích a verzích kódu byla v datové struktuře rawTemp použita beznaménková proměnná uint16_t. Čidlo Vernier Go!Temp však v HID režimu vrací 16bitovou hodnotu se znaménkem (v jednotkách 1/128 °C). Beznaménkový datový typ uint16_t tak při poklesu teploty pod 0 °C způsobil přetečení na nesmyslně velkou kladnou hodnotu.
V tomto (i předešlých článcích zpětně) je již chyba opravena změnou na znaménkový typ int16_t rawTemp.

Nebudeme zde již prezentovat celé řešení zobrazovací jednotky, zaměříme se jen na načtení teploty z teploměru Go!Temp. Pochopitelně, pokud níže uvedený program doplníme obsluhou OLED displeje nebo jiné zobrazovací jednotky, získáme tak základ pro nový typ zobrazovací jednotky k tomuto USB teploměru. To již ale necháme na čtenáři – koneckonců potřebné informace k tomu může nalézt v předchozích článcích.

Arduino UNO - Vernier Go!Temp

Pro potřeby tohoto článku používáme modul Arduino UNO doplněný rozšířením USB Shield, ale uvedený kód lze podobně jako předešlé kódy použít i na modulu Arduino Pro Mini s příslušným USB Shieldem.

V čem je nový přístup lepší?

Jak jsme naznačili v úvodu, původní kód byl z hlediska načítání USB zařízení jen určitou funkční úpravou programu pro USB joystick. Způsob komunikace Ardino–Go!Temp měl díky tomu několik nedostatků:

  • Komunikace s USB shieldem byla zbytečně komplikovaná
  • USB teploměr Vernier Go!Temp běžel v režimu, jako když je připojen k USB počítače, ale měřicí aplikace jej zatím nenalezla.
  • Díky neúplné aktivaci teploměru nebyla odesílaná data korigována vnitřními kalibračními konstantami (naše domněnka)
  • Program byl řešen systémem vzájemně vnořených podmínek s několika stavovými proměnnými, které suplovaly stavy dostupné z knihovny USB Shieldu.

Výše uvedené nedostatky jsme se pokusili nyní vyřešit následující úpravou:

  • Zjednodušení struktury načítání dat a vytvoření jednodušší struktury HID parseru.
  • Po připojení USB teploměru a jeho identifikaci dochází k odeslání inicializační sekvence, která převede teploměr Go!Temp do měřicího stavu (zelená barva stavové LED).
  • Získaná data jsou nyní načítána v jednotkách, které činí 1/128 stupně Celsia (což je patrně důsledek inicializace teploměru, respektive přesnější korekce původních dat).
  • Program je nyní řešen na principu stavového automatu (FSM), což je pro tyto programy mnohem vhodnější.

Nový zdrojový kód

USB a HID komunikace

Pro přístup k USB Shiledu se využívá objekt řídicího řadiče obvodu MAX3421E a pro komunikaci s USB teploměrem Vernier GoTemp, který komunikuje ve standardu HID, i objekt univerzálního HID obladače:

USB Usb;                 // Objekt řídicího řadiče MAX3421E
HIDUniversal Hid(&Usb);  // Univerzální HID ovladač

Pro načítání dat z USB teploměru byla zavedena nová datová struktura:

struct GoTempData {
    uint8_t ready;         // 1. bajt: Stavový příznak (0x01 = nová platná data)
    uint8_t sampleNum;     // 2. bajt: Pořadové číslo naměřeného vzorku (0–255)
    int16_t rawTemp;       // 3. a 4. bajt: 16bitová RAW hodnota z ADC převodníku
};

Stále se jedná o trojici dat, kdy první bajt představuje stavový příznak, určující, zda jsou nová data platná (0x01). Druhý bajt udává pořadové číslo (0–255), které se dá využít pro identifikaci daného vzorku nebo jej lze využít ke kontrole, zda teploměr stále odesílá data. Poslední (16bitová) hodnota odpovídá výstupní hodnotě z AD převodníku teploměru. Jelikož čidlo Vernier Go!Temp v HID režimu vrací 16bitovou hodnotu se znaménkem v jednotkách 1/128, musí být tato proměnná deklarována int16_t. Jinak by teploty pod 0 byly ukládány jako vysoká kladná hodnota. Z hodnoty proměnné rawTemp budeme dále získávat hodnotu teploty ve stupních Celsia a ukládat do proměnné temperatureC.

Pro předávání dat mezi HID parserem a hlavní smyčkou loop(), ve které běží hlavní část programu, využijeme proměnnou currentReading, která bude deklarována právě jako výše uvedená struktura. Tato proměnná je nastaveno volatile, neboť do ní paralelně přistupuje HID parser, pracující na úrovni přerušení.

Pro vytěžení potřebných dat z HID paketů je v programu definována třída GoTempReportParser:

class GoTempReportParser : public HIDReportParser {
public:
    virtual void Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf) override {
    // Bezpečnostní kontrola délky paketu a platnosti ukazatele
    if (buf == NULL || len < sizeof(GoTempData)) return;

        // Bajt buf[0] == 0x01 označuje platný datový paket s naměřenou teplotou
        if (buf[0] == 0x01) {
            currentReading.ready = buf[0];
            currentReading.sampleNum = buf[1];
            // Složení 16bitového RAW čísla ze dvou 8bitových bajtů (Little-Endian)
            currentReading.rawTemp = (uint16_t)(buf[2] | ((uint16_t)buf[3] << 8));

            // Přepočet RAW hodnoty na stupně Celsia (°C)
            temperatureC = currentReading.rawTemp / 128.0;
        }
    }
};

Konkrétní instanci této třídy pro nastavení USB a HID komunikace pak představuje proměnná Parser:

GoTempReportParser Parser;   // Instance dekodéru

Výchozí nastavení USB a HID komunikace najdeme v úvodní funkci setup(). Nejdřív jde o samotnou inicializaci USB Shieldu:

// Inicializace USB Host Shieldu
Serial.println(F("[INIT] Inicializuji USB Host Shield..."));
if (Usb.Init() == -1) {
    Serial.println(F("[CHYBA] USB Host Shield se nespustil!"));
    while(1); // HW chyba shieldu -> zastavení programu
}

Pokud dojde k chybě, nemá cenu dále pokračovat, program tedy končí v nekonečné smyčce.

Druhým krokem je registrace HID parseru:

// Registrace HID parseru na rozhraní 0
if (!Hid.SetReportParser(0, &Parser)) {
    Serial.println(F("[CHYBA] Nepodarilo se nastavit HID Report Parser."));
} else {
    Serial.println(F("[INIT] USB Shield i HID parser pripraveny."));
}

Po úspěšném nastavení USB A HID komunikace již lze se dotazovat USB Shieldu na stavy USB rozhraní. K tomu dochází pravidelným voláním metody Usb.Task(), která běží asynchronně na pozadí a voláním HID parseru naplňuje globální pracovní proměnné: currentReading a temperatureC.

Vyhodnocování stavů – stavový automat

Jednotlivé aktuální stavy programu jsou uložené ve výčtovém typu ConnectionState:

enum ConnectionState {
    STATE_WAIT_FOR_DEVICE,   // Čeká se na fyzické připojení USB zařízení
    STATE_CHECK_DEVICE,      // Zařízení je připojeno, provádí se ověření VID/PID
    STATE_INIT_GOTEMP,       // Zařízení je Go!Temp, posílá se inicializační příkaz
    STATE_MEASURING,         // Teploměr je spuštěn a probíhá měření
    STATE_REJECTED_DEVICE    // Připojeno jiné USB zařízení (čeká se na odpojení)
};

Program rozlišuje stav STATE_WAIT_FOR_DEVICE, kdy čeká na připojení USB zařízení. Je to výchozí stav celého programu, do kterého se vracíme, pokud není připojeno zařízení – například, pokud v hlavní smyčce program shledá, že tento stav není nastaven a zároveň není připojeno USB zařízení, vyhodnotí to jako odpojení USB zařízení.

Stav STATE_CHECK_DEVICE nastává v okamžiku připojení USB zařízení. Je načtena dvojice údajů idVendor a idProduct, která charakterizuje výrobce a konkrétní USB zařízení. Pokud tato kombinace odpovídá firmě Vernier (buf.idVendor == GOTEMP_VENDOR_ID) a USB teploměru Go!Temp (buf.idProduct == GOTEMP_PRODUCT_ID), přechází program do stavu STATE_INIT_GOTEMP, jinak přechází do stavu STATE_REJECTED_DEVICE.

Ve stavu STATE_INIT_GOTEMP dojde k odeslání příkazu „SetReport“ (sekvence 0x1D, 0x80, 0x10…), který nastaví teploměr Go!Temp do stavu měření – stavoví LED se rozsvítí zeleně).

uint8_t result = Hid.SetReport(0, 0, 2, 0, 8, (uint8_t*)startReport);

Jestliže teploměr toto nastavení potvrdí (if (result == 0)) přechází teploměr do měřicího stavu STATE_MEASURING, počká cca 0,5 s a pokusí se o odeslání příkazu „SetReport“ znova.

Stav STATE_MEASURING je již stavem, ve kterém jsou zachytávána data z HID parseru a vypisována na sériový výstup (do sériového monitoru). K výpisu dat (čísla vzorku, data z ADC a odpovídající teplotě) pochopitelně dochází jen tehdy, pokud jsou data platná (1 bajt paketu je nenulový)

if (currentReading.ready != 0) { … }

Poslední stav (nazvaný STATE_REJECTED_DEVICE) je stavem ryze vyčkávacím, ve kterém program setrvává po zjištění jiného USB zařízení, než je USB teploměr Vernier Go!Temp. Program tak čeká, až bude toto zařízení odpojeno.

Následující obrázek ukazuje průběh stavů po startu programu, připojení USB flash paměti, která simuluje připojení jiného USB zařízení, než je teploměr Go!Temp. Následuje odpojení tohoto zařízení a připojení USB Teploměru Vernier Go!Temp. Následuje úspěšná identifikace, inicializace teploměru a následný proces měření.

vystup programu

Program samozřejmě musí testovat přítomnost USB zařízení i v průběhu měření, neboť nic nebrání náhlému odpojení USB teploměru Go!Temp, případně připojení opět jiného USB zařízení.

Stav připojení USB zařízení je testován v hlavní smyčce

uint8_t usbTaskState = Usb.getUsbTaskState();

// Test fyzického odpojení USB zařízení
if (usbTaskState != USB_STATE_RUNNING) {
    if (currentState != STATE_WAIT_FOR_DEVICE) {
        resetConnectionState("Fyzicke odpojeni USB kabelu.");
    }
    return;
}

Dojde-li k odpojení, tedy metoda Usb.getUsbTaskState() nevrací stav USB_STATE_RUNNING, a stav programu není ve stavu vyčkávání na USB (STATE_WAIT_FOR_DEVICE), muselo dojít k odpojení. Program prostřednictvím funkce resetConnectionState() přechází do stavu STATE_WAIT_FOR_DEVICE.

Situaci náhlého vytažení teploměru při úspěšném měření (a připojení jiného USB zařízení) zachycuje následující obrázek.

vytazeni USB, pak vlozeny jiny

Drobnou reminiscencí předešlých verzí programu je proměnná lastSampleTime, ve které se ukládá okamžik posledního vzorku. U tohoto času je kontrolováno, zda poslední přijatá hodnota teploty není starší než stanovený čas WATCHDOG_TIMEOUT (nastaveno na 2 s):

// Watchdog: Pokud po nastavenou dobu nepřijdou data, hlásíme odpojení
if (millis() - lastSampleTime > WATCHDOG_TIMEOUT) {
resetConnectionState("Vyprsel watchdog timeout (teplomer neposila data).");
}
break;

Tato část kódu (společně s potřebnými proměnnými) je v programu vlastně zbytečná. Jedná se jen o jakousi pojistku, která hlídá provoz programu a komunikaci s USB teploměrem. Tato část kódu by měla zareagovat na situaci, kdy USB komunikace měla fungovat (tedy bude vracet stav USB_STATE_RUNNING), ale data z nějakého důvodu nebudou z teploměru Go!Temp přicházet – například jakési „zaseknutí“ softwaru v teploměru. Mělo by v tomto případu dojít prostřednictvím funkce resetConnectionState() k přechodu programu do stavu STATE_WAIT_FOR_DEVICE a jeho detekci a novému zaslání úvodního příkazu „SetReport“, což by jej mohlo resetovat.

Pravdou je, že jsme při zkušebním provozu a dalším testování zatím žádnou takovou situaci nezaznamenali. Je tedy možné, že tato část kódu je v programu zbytečná a je zde jen kvůli naší přílišné upjatosti a zbytečné snaze ošetřit chybu, ke které nedochází – nebo, jak říká docent Chocholoušek, dochází jedenkrát za deset let.

Výpis kódu programu:
/*
=============================================================================
PROGRAM: Ukázkový program pro USB teploměr Vernier Go!Temp
=============================================================================
Hardware:
  - Arduino UNO (nebo kompatibilní deska)
  - USB Host Shield v2.0 (čip MAX3421E)
  - USB teploměr Vernier Go!Temp
=============================================================================
*/

#include            // Knihovna pro USB Host Shield (MAX3421E)
#include  // Třída pro univerzální HID zařízení

// -----------------------------------------------------------------------------
// GLOBÁLNÍ OBJEKTY A KONSTANTY
// -----------------------------------------------------------------------------
USB Usb;                     // Objekt řídicího řadiče MAX3421E
HIDUniversal Hid(&Usb);      // Univerzální HID ovladač

// Identifikátory výrobce a zařízení Vernier Go!Temp
const uint16_t GOTEMP_VENDOR_ID  = 2295; // 0x08F7 - Vernier Software & Technology
const uint16_t GOTEMP_PRODUCT_ID = 2;    // 0x0002 - Go!Temp

// Inicializační příkaz pro spuštění čidla, interní korekce a rozsvícení zelené LED
const uint8_t startReport[8] = {0x1D, 0x80, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00};

// Časový limit (v ms) pro detekci výpadku přenosu dat z teploměru
const unsigned long WATCHDOG_TIMEOUT = 2000;

// -----------------------------------------------------------------------------
// DATOVÁ STRUKTURA PRO MAPOVÁNÍ USB HID REPORTU
// -----------------------------------------------------------------------------
struct GoTempData {
    uint8_t ready;       // 1. bajt: Stavový příznak (0x01 = nová platná data)
    uint8_t sampleNum;   // 2. bajt: Pořadové číslo naměřeného vzorku (0–255)
    int16_t rawTemp;    // 3. a 4. bajt: 16bitová RAW hodnota z ADC převodníku
};

// Sdílené globální proměnné pro předávání dat mezi parserem a smyčkou loop()
volatile GoTempData currentReading;
float temperatureC = 0.0;

// -----------------------------------------------------------------------------
// DEKODÉR USB HID PAKETŮ (REPORT PARSER)
// -----------------------------------------------------------------------------
class GoTempReportParser : public HIDReportParser {
public:
    virtual void Parse(USBHID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf) override {
        // Bezpečnostní kontrola délky paketu a platnosti ukazatele
        if (buf == NULL || len < sizeof(GoTempData)) return;

        // Bajt buf[0] == 0x01 označuje platný datový paket s naměřenou teplotou
        if (buf[0] == 0x01) {
            currentReading.ready     = buf[0];
            currentReading.sampleNum = buf[1];
            // Složení 16bitového RAW čísla ze dvou 8bitových bajtů (Little-Endian)
            currentReading.rawTemp   = (int16_t)(buf[2] | ((int16_t)buf[3] << 8));
            
            // Přepočet RAW hodnoty na stupně Celsia (°C)
            temperatureC = currentReading.rawTemp / 128.0;
        }
    }
};

GoTempReportParser Parser;  // Instance dekodéru

// -----------------------------------------------------------------------------
// STAVY A PROMĚNNÉ PRO ŘÍZENÍ SPOJENÍ
// -----------------------------------------------------------------------------
enum ConnectionState {
    STATE_WAIT_FOR_DEVICE,     // Čeká se na fyzické připojení USB zařízení
    STATE_CHECK_DEVICE,        // Zařízení je připojeno, provádí se ověření VID/PID
    STATE_INIT_GOTEMP,         // Zařízení je Go!Temp, posílá se inicializační příkaz
    STATE_MEASURING,           // Teploměr je spuštěn a probíhá měření
    STATE_REJECTED_DEVICE      // Připojeno jiné USB zařízení (čeká se na odpojení)
};

ConnectionState currentState = STATE_WAIT_FOR_DEVICE;
unsigned long lastSampleTime = 0;

// -----------------------------------------------------------------------------
// POMOCNÁ FUNKCE PRO RESETOVÁNÍ STAVU PŘI ODPOJENÍ ZAŘÍZENÍ
// -----------------------------------------------------------------------------
void resetConnectionState(const char* reason) {
    if (currentState != STATE_WAIT_FOR_DEVICE) {
        Serial.print(F("\n[USB] Odpojeno / Zmena stavu: "));
        Serial.println(reason);
        Serial.println(F("[USB] Cekam na pripojeni USB zarizeni..."));
    }
    currentState = STATE_WAIT_FOR_DEVICE;
    currentReading.ready = 0;
}

// -----------------------------------------------------------------------------
// INICIALIZAČNÍ FUNKCE SETUP
// -----------------------------------------------------------------------------
void setup() {
    Serial.begin(115200);
    while (!Serial); // Čekání na otevření sériového monitoru

    Serial.println(F("=================================================="));
    Serial.println(F(" Arduino UNO + USB Host Shield: Vernier Go!Temp"));
    Serial.println(F("=================================================="));

    // Inicializace USB Host Shieldu
    Serial.println(F("[INIT] Inicializuji USB Host Shield..."));
    if (Usb.Init() == -1) {
        Serial.println(F("[CHYBA] USB Host Shield se nespustil!"));
        while (1); // HW chyba shieldu -> zastavení programu
    }

    // Registrace HID parseru na rozhraní 0
    if (!Hid.SetReportParser(0, &Parser)) {
        Serial.println(F("[CHYBA] Nepodarilo se nastavit HID Report Parser."));
    } else {
        Serial.println(F("[INIT] USB Shield i HID parser pripraveny."));
    }

    Serial.println(F("[USB] Cekam na pripojeni USB zarizeni..."));
}

// -----------------------------------------------------------------------------
// HLAVNÍ SMYČKA LOOP
// -----------------------------------------------------------------------------
void loop() {
    // Pravidelná obsluha USB sběrnice (vyvolá Parse() při příjmu dat)
    Usb.Task();

    uint8_t usbTaskState = Usb.getUsbTaskState();

    // Test fyzického odpojení USB zařízení
    if (usbTaskState != USB_STATE_RUNNING) {
        if (currentState != STATE_WAIT_FOR_DEVICE) {
            resetConnectionState("Fyzicke odpojeni USB kabelu.");
        }
    return;
    }

    // Stavový automat pro správu připojení a měření
    switch (currentState) {
        
        // Po fyzickém připojení přepneme do ověření typu zařízení
        case STATE_WAIT_FOR_DEVICE:
            currentState = STATE_CHECK_DEVICE;
            break;

        // Test připojení USB – kontrola, zda se jedná o Vernier Go!Temp
        case STATE_CHECK_DEVICE: {
            USB_DEVICE_DESCRIPTOR buf;
            uint8_t rcode = Usb.getDevDescr(1, 0, sizeof(USB_DEVICE_DESCRIPTOR), (uint8_t *)&buf);

            if (rcode == 0) {
                Serial.print(F("[USB] Detekovano zarizeni -> Vendor ID: "));
                Serial.print(buf.idVendor);
                Serial.print(F(" (0x"));
                Serial.print(buf.idVendor, HEX);
                Serial.print(F("), Product ID: "));
                Serial.print(buf.idProduct);
                Serial.print(F(" (0x"));
                Serial.print(buf.idProduct, HEX);
                Serial.println(F(")"));

                // Kontrola VID (2295 = Vernier) a PID (2 = Go!Temp)
                if (buf.idVendor == GOTEMP_VENDOR_ID && buf.idProduct == GOTEMP_PRODUCT_ID) {
                    Serial.println(F("[USB] -> Nalezen spravny teplomer Vernier Go!Temp."));
                    currentState = STATE_INIT_GOTEMP;
                } else {
                    Serial.println(F("[CHYBA] -> Pripojene USB zarizeni NENI teplomer Go!Temp!"));
                    Serial.println(F("[USB] Cekam na pripojeni spravneho zarizeni..."));
                    currentState = STATE_REJECTED_DEVICE;
                }
            }
            break;
        }

        // Inicializace Go!Temp – odeslání příkazu (startReport)
        case STATE_INIT_GOTEMP: {
            Serial.println(F("[GOTEMP] Odesilam inicializacni prikaz (SetReport)..."));

            // Odeslání reportu přes HID rozhraní
            uint8_t result = Hid.SetReport(0, 0, 2, 0, 8, (uint8_t*)startReport);

            if (result == 0) {
                Serial.println(F("[GOTEMP] -> Prikaz odeslan, potvrzeno! Zelena LED sviti."));
                Serial.println(F("[GOTEMP] Zahajuji mereni teploty...\n"));
                lastSampleTime = millis();
                currentState = STATE_MEASURING;
            } else {
                Serial.print(F("[CHYBA] Selhalo odeslani inicializacniho prikazu. Kod: "));
                Serial.println(result);
                delay(500); // Čekání před opakovaným pokusem
            }
            break;
        }

        // Probíhá aktivní měření a výpis na sériový port
        case STATE_MEASURING: {
            // Kontrola, zda parser zpracoval nový vzorek
            if (currentReading.ready != 0) {
                Serial.print(F("Vzorek #: "));
                Serial.print(currentReading.sampleNum);
                Serial.print(F(" | RAW: "));
                Serial.print(currentReading.rawTemp);
                Serial.print(F(" | Teplota: "));
                Serial.print(temperatureC, 2);
                Serial.println(F(" °C"));

                // Vynulování příznaku a aktualizace časovače watchdogu
                currentReading.ready = 0;
                lastSampleTime = millis();
            }

            // Watchdog: Pokud po nastavenou dobu nepřijdou data, hlásíme odpojení
            if (millis() - lastSampleTime > WATCHDOG_TIMEOUT) {
                resetConnectionState("Vyprsel watchdog timeout (teplomer neposila data).");
            }
            break;
            
        }

        // Připojeno cizí USB zařízení – čekáme na jeho odpojení
        case STATE_REJECTED_DEVICE:
            // Neaktivní stav – čeká se, až uživatel odpojí cizí USB zařízení
        break;
    }
}

Závěr

Dnešním článkem jsme snad opustili dřívější mírně provizorní řešení odvozené z joystickového ovladače. Přechod k čitelnému stavovému automatu (FSM) a přímé HID komunikaci přinesl nejen stabilnější provoz, ale především korektní inicializaci teploměru Vernier Go!Temp a plně korigovaná data s přesností na 1/128 °C. Vznikl tak solidní základ, na kterém můžete ihned stavět vlastní projekty – kupříkladu autonomní meteostanici, bateriový datalogger s OLED displejem nebo jen neobyčejný laboratorní teploměr.

Plánujete-li rozšíření modulu Arduino s připojeným USB teploměrem Go!Temp o Ethernet, Wi-Fi či Bluetooth, myslete na to, že při použití standardních knihoven u klasického Arduina UNO (ATmega328P) rychle narazíte na limity paměti RAM. Na druhou stranu to může být skvělá příležitost pro bastlířskou výzvu. Spíš bychom ale doporučili zaměřit se na jiné moduly a vyzkoušet si tak portaci na výkonnější desky jako jsou Arduino Mega, ESP32 nebo RP2040 s USB Host rozhraním.

Autor článku: Miroslav Panoš

Článek je součástí následující série článků: