Internet | Pojďme programovat elektroniku | Arduino

Pojďme programovat elektroniku: Připojíme micro SD kartu do internetu

  • Práci s Wi-Fi čipy ESP8266 a ESP32 jsme si už vyzkoušeli
  • Dnes internet naučíme základní Arduino Uno
  • 2 kB RAM nám vystačí na celý server

Když jsme si na sklonku března představili první rozšiřující shield pro Arduino Uno, namísto originálního Una jsme k bateriové desce připojili hybridní mikropočítač, který byl vedle klasického ATmega328P vyzbrojený ještě WI-Fi čipem ESP8266.

Naše Arduino tedy díky dvěma lithiovým článkům mohlo běžet na baterii a zároveň přinejmenším několik hodin komunikovat s internetem. Dnes se k internetu vrátíme, namísto Wi-Fi si však vyzkoušíme rozšiřující shield s ethernetovým čipem.

V minulosti jsme si vyzkoušeli tyto rozšiřující shieldy pro Arduino Uno:

Deska s ethernetem

12. září roku 1962 pronesl americký prezident John Fitzgerald Kennedy jeden z nejslavnějších projevů historie, jehož součástí byla následující pasáž:

„We choose to go to the Moon! We choose to go to the Moon... We choose to go to the Moon in this decade and do the other things, not because they are easy, but because they are hard.“

d6131c49-d178-4f05-8edf-604d51a6d139
Ethernetový shield pro Arduino s čipem WIZnet W5100 a pouzdrem na microSD

Kdyby se JFK dožil dnešních dnů, po večerech bastlil a kupoval rozšiřující shieldy pro Arduino, nejspíše by to samé pronesl i v okamžiku, když by ve žluté bublinkové obálce z Aliexpressu objevil destičku s čipem iEthernet W5100 od jihokorejského WIZnetu.

107a023a-28e6-4d4b-a297-d6ff6a77332a
WIZnet W5100 je základní čip s podporou přenosových protokolů 10BASE-T/100BASE-TX

Cena základního ethernetového shieldu s tímto čipem se dnes na zahraničních e-shopech pohybuje okolo stokoruny (na českých pár stovek, ale zase s rychlým doručením) a vedle konektoru RJ-45 na něm najdete také pouzdro pro microSD.

3d167461-df79-4615-82db-3a97cb951bb0
Shield má i řadu signalizačních LED, které indikuji aktivní linku (LINK), 100BASE-TX (100M), full-duplex (FULLD), kolize (COLL) a samotný přenos oběma směry (RX/TX)

Čip W5100 je už dnes překonaný (dražší desky používají často jeho nástupce W5500), pro základní komunikaci Arduina s okolním světem ale stačí. Rozumí síťovým protokolům TCP, IPv4, UDP, ARP nebo IGMP a co se týče samotného přenosu, pak 10Mb a 100Mb protokolům 10BASE-T/100BASE-TX.

S řídícím čipem Arduina komunikuje skrze sériovou sběrnici SPI

Když rozšiřující destičku zacvaknete do základního mikropočítače Arduino Uno, můžete jej bez další instalace okamžitě používat, součástí vývojového prostředí Arduina je totiž i předinstalovaná knihovna Ethernet s hromadou příkladů.

6013d013-2c45-4102-a2ad-8423f2af9608
Jaké piny na Ardunio Uno okupují standardní datové sběrnice SPI a I2C

To samé platí o zmíněné SD kartě, kterou oživíte taktéž pomocí předinstalované knihovny SD, případně si z GitHubu doinstalujete její propracovanější podobu SdFat. Oba kusy hardwaru s řídícím čipem komunikují skrze sběrnici SPI, jejíž základní komunikační linky na Arduino Uno zaberou piny:

  • 13 – pulzní signál SCK udávající rychlost komunikace
  • 12 – datová linka MISO (Master In, Slave Out)
  • 11 – datová linka MOSI (Master Out, Slave In)
  • 10 – identifikátor připojeného zařízení SS/CS (Slave Select/Chip Select)

Tyto piny tedy za běhu nesmíte používat jako univerzální piny GPIO. Pamatujte na to zejména v případě, pokud byste chtěli zapínat a vypínat LED na desce, ta je totiž připojená právě k pinu 13.

4873a114-4988-441c-a784-dbf2219a3a422b402379-0869-446e-a36e-1a03d4e7208b08ba985c-faee-4298-b597-61230feccd11
Ethernetový shield je primárně určený pro Arduino Uno, lze jej ale připojit i k ostatním stavebnicím Arduino s kompatibilním rozložením pinů GPIO a napájení

Jelikož je SPI sériová sběrnice, na kterou může být připojena hromada periferií, řídící čip ATmega328P, který je mozkem desky Arduino Uno, je musí při komunikaci nějak rozlišit. Zatímco neméně populární sběrnice I2C identifikuje zařízení pomocí speciální digitální adresy, v případě SPI slouží k identifikaci právě pin označovaný jako SS nebo CS.

540271c0-d907-4188-9259-026ab1a23c9f
A konečně shield zacvaknutý do Arduino Uno pod ním
6c9726bc-691b-47ec-a9b4-0751aa6a4e4be981b243-021e-4694-adc9-835d5a5a8659083f8bfb-cf2a-4dbd-a205-447dad802d0e
Plnému zacvaknutí brání velký USB port Arduino Uno. Lepší bude varianta s micro USB.

Výchozí SS na pinu 10 je připojený k ethernetovému čipu W5100 a SD karta zacvaknutá v pouzdře čtečky pak bude používat jako svůj SS pin číslo 4. Takže si to zrekapitulujme. Při zapojení desky a používání ethernetu a SD nepoužívejte piny 13, 12, 11, 10 a také A0 a A1 jako univerzální piny GPIO. Zbytek je nadále k dispozici.

Připojíme SD kartu do internetu jako primitivní souborový server

Knihovna Ethernet umožňuje spojení do sítě s ručně zadanou IP adresou, nebo skrze DHCP server vašeho routeru. Druhou cestu použijeme i v dnešní ukázce. Kdy si program hned na začátku vyžádá od DHCP IP a na tradičním TCP portu 80 spustí drobný HTTP server.

Kód k získání IP adresy od DHCP serveru vašeho routeru:

// Volitelna MAC, kterou bude cip pouzivat
uint8_t mac[] = {0, 1, 2, 3, 4, 5}; 

// Ethernetovy cip pouziva SS pin 10
Ethernet.init(10);

// Samotne pripojeni k siti LAN
Serial.println(F("Prihlasuji se do site..."));
if (Ethernet.begin(mac)) {
  Serial.print(F("DHCP IP adresa: "));
  Serial.println(Ethernet.localIP());
}

Když pak IP adresu Arduina Una načtete třeba ve webovém prohlížeči, zobrazí se vám primitivní HTML stránka s obsahem kořenového adresáře SD karty. Klepnete-li na některý ze souborů, v případě textových souborů, HTML stránek, obrázků a MP3 nahrávek se rovnou zobrazí a přehrají v prohlížeči, anebo stáhnou do počítače.

def4264f-cb34-443c-8819-80a206647736
Úvodní připojení k sítí LAN a získání IP adresy od DHCP serveru

SD karta se hodí přesně pro tyto případy, když se chcete k Arduinu připojit přes síť. Na SD kartě mohou být uložené třeba rozměrné HTML soubory, Javascript, obrázky, kaskádové styly a ze samotného řídícího čipu se třeba ve formátu JSON stáhnou jen data s údaji ze senzorů. V prohlížeči se pak složí a zobrazí hezká stránka se vším všudy a v UTF-8 s českými znaky, což by jinak bylo složité.

Připojení k internetu u desky Arduino Uno, která má jen 2kB RAM? Bitva o každý volný bajt!

Dobrá, ale proč jsem tedy v úvodu zmiňoval Kennedyho a onen výrok „not because they are easy, but because they are hard?“ Protože naráz ovládnout ethernet, SD kartu a ještě spustit webový server na drobném čipu ATmega328P s 2kB RAM je opravdu čiré bláznovství.

Ještě to raději jednou zopakuji. K dispozici máte jen 2 048 B RAM, což je o řád méně než v případě populárních Wi-Fi čipů ESP8266. A už jen tím, že použijete zmíněné vestavěné knihovny pro oživení potřebných periferií, dvě třetiny z této paměti nadobro spotřebujete.

Pro váš uživatelský kód tedy zbude nejvýše pár set bajtů operační paměti, což bude vyžadovat hromadu optimalizací. V případě jakýchkoliv textových konstant vypisovaných třeba do sériové linky se tedy nabízí makro F(), které dá překladači vědět, že tento text se nemá načítat při startu do RAM, ale má se číst přímo z flashové paměti, čímž se ušetří klidně i desítky bajtů operační paměti.

Podívejte se na dvě textové konstanty, které chceme vypsat do sériového terminálu PC. Vypadají stejně, ale je v nich ohromný rozdíl:

Serial.println("Vita vas ethernetovy cip");
Serial.println(F("Vita vas ethernetovy cip"));

Zatímco první řádek textové znaky načte do operační paměti, které máme opravdu málo, druhá varianta s makrem F() tento text načte z flashové paměti firmwaru a ušetří více než 20 B RAM!

Druhou technikou, kterou se postaráme o co nejúspornější kód, bude absence třídy String. Ta je sice naprosto skvělá pro jednoduchou práci s textovými řetězci podobně jako ve vysokoúrovňových jazycích (Javascript, Java, Python aj.), jedná se ale o objekt, který se vytváří v dynamické části paměti RAM (heap) a při nevhodném použití ji může celou zahltit.

Když přitom dojde RAM, často to nemusíte ani poznat. Čip se může začít ve smyčce resetovat, některé části přestanou fungovat a tak dále. Když jsem tedy během vývoje zpočátku nepoužíval makro F(), po startu vždy fungoval jen ethernet, nikdy se mi ale nepodařilo otevřít spojení s SD.

Na vině přitom nebylo třeba slabé napájení (SD i ethernetový čip jsou podobně jako Wi-Fi opravdu žrouti), ale jednoduše skutečnost, že jsem v úvodu vypisoval do sériové linky mnoho textových hlášek (bez použití zmíněného makra F) a pro SD kartu už nezbylo dostatek paměti.

f550880e-d450-4999-9293-67bafaa9bfc5
Pro lokální proměnné programu zbývá 634 B. Musím tedy opravdu šetřit s každým bajtem.

Vývojové prostředí Arduino se na toto snaží myslet, a pokud hrozí, že bude chybět RAM, při kompilaci zobrazí varovné hlášení. Nápovědou může být také souhrn po překladu, kdy překladač vypíše, kolik RAM zbude pro lokální proměnné.

Vytváříme drobný HTTP server, který bude načítat soubory z SD karty

Když konečně vytvoříme firmware, který nezbaští všechny RAM, můžeme si začít hrát s ethernetem. Ten má oproti Wi-Fi tu výhodu, že se nemusíme starat o autentizaci. Čip po připojení k napájení získá od DHCP svoji LAN IP adresu zhruba do pěti sekund a je plně připravený.

Ačkoliv GitHub nabízí nepřeberné množství knihoven pro spuštění HTTP serveru se vším všudy, na drobném čipu ATmega328P ukousnou obrovský díl RAM, a tak si vytvoříme vlastní primitivní HTTP odpovídač.

d4c7f3fa-7db0-470e-9c49-e864e567c3677734029d-1db2-4472-b7a2-380d1169b7bc
Když do prohlížeče zadám IP adresu mikropočítače bez dalších parametrů, program přečte obsah SD karty a vygeneruje HTML kód se seznamem, který odešle zpět prohlížeči. V sériové lince se pro kontrolu vypisuje veškerá komunikace mezi oběma stranami.

Ve funkci setup spustíme poslech serveru na portu 80 a ve smyčce loop se budeme při každém průchodu ptát serveru, jestli se k němu nepřipojil nějaký klient (webový prohlížeč). Pokud ano, začneme od něj přijímat příchozí znaky. Bude se jednat o textovou hlavičku protokolu HTTP, která jednotlivé parametry odděluje zalomením řádku a hned na tom prvním bude samotný požadavek na stránku HTTP GET.

Když tyto znaky budeme pro kontrolu vypisovat do sériové linky, dorazí nám od prohlížeče třeba tento text zakončený prázdným řádkem:

GET /INDEX.HTM HTTP/1.1
Host: 172.17.15.103
Connection: keep-alive
Upgrade-Insecure-Requests: 1
DNT: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.131 Safari/537.36
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Referer: http://172.17.15.103/
Accept-Encoding: gzip, deflate
Accept-Language: cs,en;q=0.9,sk;q=0.8,de;q=0.7

Jelikož se kvůli velmi malé paměti RAM chceme vyvarovat třídy String, pomocí které bychom mohli řádek po řádku uložit elegantním zápisem:

String radek = klient.readStringUntil('\n')

budeme znak po znaku ukládat do pole znaků a vytvoříme si pro něj jednoduchý procesor/parser. Jakmile dorazí znak zalomení řádku, dáme programu vědět, že už máme v poli celý řádek a můžeme jej dále zpracovat.

Nejprve pomocí funkce strstr zjistíme, jestli řádek obsahuje sekvenci znaků „GET /“. Pokud ano, rozdělíme řádek podle mezer na části pomocí funkce strtok. První část bude obsahovat „GET“ a druhá „/INDEX.HTM“.

Náš jednoduchý parser takto přečte řádek po řádku celou dávku dat od prohlížeče, a i když z nich bude zjišťovat jen požadavek GET, principiálně je připravený pro snadné rozšíření a detekci i dalších parametrů HTTP hlavičky.

Co se stane dál? Jelikož už víme, že po nás chce prohlížeč stránku INDEX.HTM, podíváme se, jestli takový soubor máme na SD kartě. Pokud ne, připojenému klientu odešleme krátkou HTTP hlavičku s odpovědí:

HTTP/1.1 404 Not Found
Connection: close

Jakmile ji obdrží webový prohlížeč, dozví se, že mu posíláme standardizovaný HTTP kód číslo 404, který sděluje, že server požadovaná data nenalezl, a prohlížeč zobrazí vlastní chybové hlášení.

0d13b816-6192-4181-ac40-64596b37f403d9b1040c-40fd-4aac-9b9d-903e90b1f6c1
Soubor nesmysl.txt na SD kartě není, a tak server odešle prohlížeči HTTP kód 404. V sériové lince se pro přehlednost vypisuje celá komunikace mezi klientem a serverem.

Pokud soubor na SD kartě nalezneme, odešleme tentokrát HTTP hlavičku třeba ve formě:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Connection: close

Content-type dává prohlížeči vědět, co je to za typ dat. Kdyby se jednalo o zmíněný soubor INDEX.HTM, víme, že má příponu HTM, je to tedy asi textový HTML dokument, a podle toho vytvoříme textový řetězec jako výše. Kdyby to byl naopak JPEG obrázek, řádek by vypadal takto: Content-Type: image/jpeg.

fabe2dfe-e18d-4e7b-b972-ffc49acb7adb5e169392-cd0e-47f7-8969-ef41b484f035
Tentokrát soubor na SD kartě existuje, server tedy klientu vrátí HTTP kód 200 OK a zároveň obsah souboru INDEX.HTM

Jak odeslat soubor pomalu a naopak rychleji

Hned po hlavičce začneme klientu posílat samotná data souboru, k čemuž slouží obslužné metody třídy File, která je součástí knihovny SD. V jeden okamžik můžeme pracovat vždy jen s jedním souborem.

Odeslání souboru by mohlo vypadat třeba takto:

File soubor = SD.open("/krajinka.jpeg");
if (soubor) {
  while (soubor.available()) {
    klient.write(soubor.read());
  }
  soubor.close();
}

Ve smyčce tedy vždy přečtu jeden bajt souboru a hned jej předám klientu, respektive ethernetovému čipu k odeslání. Takto se chová i většina příkladů na webu a je to špatně!

Ne snad, že by to nefungovalo, ale u rozměrnějších souborů to bude velmi pomalé. Kdybychom takovým způsobem měli přenášet do prohlížeče z SD třeba zhruba 250kB obrázek, bude to trvat okolo 70 sekund!

f6a32e08-d6ea-41f5-8455-60e4dc0d85c7 b7f31c75-b132-4401-bc7b-c7bf76db4834
Pomalé přeposílání bajt po bajtu a blok po bloku. Rozdíl? 1,2 minuty vs. 14 sekund.

Mnohem lépe uděláme, když budeme data přenášet po malých blocích. Z SD karty tedy nejprve přečteme třeba 50 B dat a tato data poté přímo pošleme připojenému klientovi. Výsledek? Doba přenosu se zkrátí zhruba na 14 sekund.

Mnohonásobně rychlejší odeslání souboru s 50B bloky:

File soubor = SD.open("krajinka.jpeg");
if (soubor) {
  uint8_t data[50];
  while (soubor.available()) {
    size_t delka = soubor.readBytes(data, 50);
    klient.write(data, delka);
  }
  soubor.close();
}

I to je optikou obvyklých přenosů žalostně málo, ale je třeba si uvědomit, že vše řídí docela primitivní osmibitový čip ATmega328P s 16MHz taktem. Kdybychom ethernetový čip připojili třeba k armovému mikrokontroleru s několikanásobným pracovním taktem, reálná rychlost přenosu se může s knihovnou Ethernet vyšplhat až k 1 Mb/s (viz testy v závěru odkazovaného článku od autora knihovny Ethernet a s pokročilejším čipem W5500).

2 kB RAM je výzva a skvělý trenažér pro začátečníky a studium možností jazyka C

Od ethernetového čipu přímo připojeného k jednoduchému Arduino Uno tedy očekávejte rychlosti vhodné pro přenos drobných textových informací z čidel, načítání zmíněných textových souborů z SD a případně drobné grafiky.

Proč si tedy vůbec takový shield kupovat a celé to nenapsat pro o několik řádů výkonnější čipy ESP8266 a ESP32 s ohromnou pamětí RAM? Protože podobný primitivní hardware, kde na každém kroku hrozí, že vám dojde RAM, je pro začátečníky perfektním trenažerem nízkoúrovňového C/C++, práce s textovými poli, ukazateli a dalšími technikami, jak vše udělat jen s několika málo bajty operační paměti. Spustit jednoduchý webový server nad ESP8266 totiž dokáže i vaše babička.

Nakonec nesmí chybět kód celého projektu

A to je pro dnešek vše. Na závěr nesmí opět chybět komentovaný kód celého programu, který spustí primitivní HTTP GET server a do prohlížeče vypíše libovolný soubor z připojené SD karty.

// Knihovny pro SPI, ethernet a SD
#include <SPI.h>
#include <SD.h>
#include <Ethernet.h>

// Delky znakovych poli pro radek HTTP hlavicky a GET pozadavku
#define RADEK       80
#define URL         40

// Zastupna makra pro typy souboru
#define MIME_HTML   0
#define MIME_GIF    1
#define MIME_JPEG   2
#define MIME_MP3    3
#define MIME_TXT    4
#define MIME_BIN    5

// MAC adresa ethernetoveho cipu
// Muzeme si definovat vlastni
uint8_t mac[] = {0, 1, 2, 3, 4, 5};

// Struktura radku, ktery budeme cist od klientu
// Struktura obsahuje samotne pole znaku, udaj o delce
// a nakonec jeste informaci, jestli je uz radek cely precteny (dorazilo zalomeni radku)
struct {
  char znaky[RADEK];
  size_t delka;
  bool hotovo;
} radek;

// Seznam uzivatelskych funkci, ktere dnes pouzijeme
void vypisAdresar(EthernetClient *klient, File dir);
int volnaPamet();
void vypisVolnouPamet();
void odesliHTTPHlavicku(EthernetClient *klient, uint16_t HTTPKod, uint8_t typ);
uint8_t zjistiTyp(char *soubor);
bool ziskejUrl(EthernetClient *klient, char *url, bool logovat);

// Na TCP portu 80 bude poslocuhat nas HTTP server
EthernetServer server(80);

// Objekt souboru, ktery budeme otevirat na SD karte
File soubor;

// Hlavni funkce setup se zpracuje po pripojeni desky k elektrine
void setup() {
  // Nastartovani seriove linky
  Serial.begin(115200);
  // Nastartovani SD karty (jeji SS je pripojeny na pin 4)
  Serial.print(F("Inicializuji SD kartu: "));
  if (SD.begin(4)) {
    Serial.println(F("OK"));
  }
  else {
    Serial.println(F("nelze se spojit s SD"));
  }

  // Incializace ethernetoveho cipu (jeho SS je pripojeny na vychozi pin 10)
  Serial.println(F("Inicializuji ethernetovy cip"));
  Ethernet.init(10);

  // Ziskani IP adresy z DHCP serveru
  Serial.println(F("Prihlasuji se do site..."));
  if (Ethernet.begin(mac)) {
    Serial.print(F("DHCP IP adresa: "));
    Serial.println(Ethernet.localIP());
  }
  else {
    Serial.println(F("Nelze ziskat DHCP IP adresu"));
  }

  // Spusteni HTTP serveru na portu 80
  Serial.println(F("Startuji HTTP server na TCP portu 80"));
  server.begin();

  // Jelikoz nam dnes pujde o kazdy volny bajt,
  // budeme po kazde dulezite akci vypisovat zbyvajici kapacitu RAM.
  // Pokud zacne klesat, mame nekde problem.
  vypisVolnouPamet();
}

// Smycka loop se opakuje stale dokola a spusti se po zpracovani funkce setup
void loop() {
  // Zeptej se serveru, jestli se pripojil nejaky klient na TCp port 80
  // Treba webovy prohlizec, do ktereho jsme zadali IP adresu desky
  EthernetClient klient = server.available();
  // Pokud se tak stalo...
  if (klient) {
    if (klient.connected()) {
      // Vytvor znakove pole pro URL/nazev souboru a pro jistotu ho vymaz
      // Mohou v nem byt stara data z predchoziho prubehu
      char url[URL];
      memset(&url, 0, sizeof(url));

      // Pomoci naseho znakoveho procesoru/parseru precti postupne od klienta vsechny radky HTTP hlavicky
      // Pokud v nem najdes radek s "GET /neco", "neco" uloz do znakoveho pole url a vrat pravdu
      if (ziskejUrl(&klient, url, true)) {
        Serial.println(F("****************************************"));
        Serial.print(F("Chces po mne soubor: "));
        Serial.println(url);

        // Pokud je url prazdna (jen IP adresa a /, nebo chyba),
        // vytvor HTML se seznamem souboru na SD a odesli ho pripojenemu klientu
        if (strlen(url) <= 1) {
          Serial.println(F("Vypisuji obsah SD karty"));
          odesliHTTPHlavicku(&klient, 200, MIME_HTML);
          File root = SD.open("/");
          vypisAdresar(&klient, root);
          root.close();
        }

        // Pokud url neni prazdna ale neodpovida zadnemu souboru na SD,
        // odpoved prohlizeci kodem 404, aby mohl zobrazit chybove hlaseni
        else if (!SD.exists(url)) {
          Serial.println(F("Soubor neexistuje, posilam kod 404!"));
          odesliHTTPHlavicku(&klient, 404, MIME_BIN);
        }

        // Pokud url neni prazdna a obsahuje nazev existujiciho souboru na SD
        else {
          Serial.println(F("Odesilam soubor klientu"));
          // Zjisti MIME typ souboru podle pripony
          // https://cs.wikipedia.org/wiki/Typ_internetov%C3%A9ho_m%C3%A9dia
          uint8_t typ = zjistiTyp(url);
          Serial.print(F("MIME typ: "));
          Serial.println(typ);
          odesliHTTPHlavicku(&klient, 200, typ);
          // Cti soubor po 50B blocich a odesilej je pripojenemu klientu
          soubor = SD.open(url);
          if (soubor) {
            uint8_t data[50];
            while (soubor.available()) {
              size_t delka = soubor.readBytes(data, 50);
              klient.write(data, delka);
            }
            soubor.close();
          }
          else {
            Serial.println(F("Soubor nelze otevrit"));
          }
        }
        Serial.println(F("****************************************"));
      }
    }
    // Ukonci spojeni s klientem a pro kontrolu opet vypis do seriove linky volnou pamet RAM
    klient.stop();
    vypisVolnouPamet();
  }
}

// Nase pomocna funkce, ktera odesle klientu
// patricnou HTTP hlavicku. Treba:
// HTTP/1.1 200 OK
// Content-Type: text/html; charset=utf-8
// Connection: close
void odesliHTTPHlavicku(EthernetClient *klient, uint16_t HTTPKod, uint8_t typ) {
  klient->print(F("HTTP/1.1 "));
  klient->print(HTTPKod);
  if (HTTPKod == 404) klient->println(F(" Not Found"));
  else if (HTTPKod == 200) {
    klient->println(F(" OK"));
    switch (typ) {
      case MIME_HTML:
        klient->println(F("Content-Type: text/html; charset=utf-8"));
        break;
      case MIME_JPEG:
        klient->println(F("Content-Type: image/jpeg"));
        break;
      case MIME_GIF:
        klient->println(F("Content-Type: image/gif"));
        break;
      case MIME_MP3:
        klient->println(F("Content-Type: audio/mpeg"));
        break;
      case MIME_TXT:
        klient->println(F("Content-Type: text/plain"));
        break;
      case MIME_BIN:
        klient->println(F("Content-Type: application/octet-stream"));
        break;
    }
  }
  else {
    klient->println();
  }
  klient->println(F("Connection: close"));
  klient->println();
}

// Pomocna funkce pro zjisteni MIME typu podle pripony souboru
// Podporuji jen nekolik zakladnich typu JPG, GIF, MP3, HTM a TXT)
// V ostatnich pripadech se prohlizeci odesle obecny typ binanrnich dat
// a prohlizec odkazovany soubor rovno ustahne a nezobrazi
uint8_t zjistiTyp(char *soubor) {
  if (strstr(soubor, "JPG") != NULL) {
    return MIME_JPEG;
  }
  else if (strstr(soubor, "GIF") != NULL) {
    return MIME_GIF;
  }
  else if (strstr(soubor, "MP3") != NULL) {
    return MIME_MP3;
  }
  else if (strstr(soubor, "TXT") != NULL) {
    return MIME_TXT;
  }
  else if (strstr(soubor, "HTM") != NULL) {
    return MIME_HTML;
  }
  else {
    return MIME_BIN;
  }
}

// Znakovy procesor/parser, ktery od klienta precte znak po znaku celou HTTP hlavicku
// a najde v ni HTTP GET pozadavek. Lze snadno rozsirit o hledani dalsich parametru
// pro rozsahlesji podporu protokolu HTTP.
bool ziskejUrl(EthernetClient *klient, char *url, bool logovat) {
  bool navrat = false;
  if (logovat) Serial.println();
  // Dokud pro me na klient nejak data
  while (klient->available()) {
    // Precti jeden bajt a preved jej na znak
    char znak = klient->read();
    if (logovat) Serial.print(znak);

    // Pokud pocet znaku prekonal nami stanovene maximum v uvodu,
    // oznac takovy radek jako hotovy. Zbytek radku se zpracuje samostatne v dalsim prubehu
    if (radek.delka == RADEK) {
      radek.hotovo = true;
    }
    // Pokud dorazil znak zalomeni radku,
    // oznac radek jako hotovy
    else {
      if (znak == 0xA) { // LF
        radek.hotovo = true;
      }
      // Pokud radek jeste neni hotovy, pripadne pokud se nejedna o znak CR
      // uloz znak do pole
      else {
        if (!radek.hotovo) {
          if (znak != 0xD) { // CR
            radek.znaky[radek.delka++] = znak;
          }
        }
      }
    }

    // Pokud je radek hotovy a ma alespon 1 znak
    if (radek.hotovo) {
      if (radek.delka > 0) {
        // Pokud radek obsahuje retezec "GET /"
        if (strstr(radek.znaky, "GET /")  != NULL) {
          // Rozdel radek podle mezery na casti
          // Takze treba "GET /index.htm HTTP/1.1" na "GET", "/index.htm" a "HTTP/1.1"
          // Pokud je druha cast nenulova (radek ma alespon dve mezery)
          // zkopiruj druhou cast (/index.htm) do znakoveho pole url a nastav navratovou hodnotu funkce na pravdu,
          // protoze jsme nasli v radku GET a URL
          char *token = strtok(radek.znaky, " ");
          if (token != NULL) {
            token = strtok(NULL, " ");
          }
          if (token != NULL) {
            navrat = true;
          }
          strcpy(url, token);
        }
      }
      // Vynuluj strukturu s udaji o aktualnim radku,
      // protoze vse zacne znovu, pokud jsme jeste neprecetli vsechny radky
      memset(&radek, 0, sizeof(radek));
    }
  }
  // Az precteme vsechna data od klientu, vrat navratovou hodnotu funkce
  // Pokud jsme nasli GET a URL, bude rovna pravde
  return navrat;
}

// Pomocna funkce pro sjisteni souboru na SD a
// vygenerovani HTML kodu s jejich seznamem,
// ktery se pote posle pripojenemu klientu
void vypisAdresar(EthernetClient *klient, File dir) {
  klient->print(F("<html><head><title>SD karta</title></head><body>"));
  while (true) {
    File soubor = dir.openNextFile();
    if (!soubor) {
      break;
    }
    if (soubor.isDirectory()) {
      klient->print(F("<b>["));
      klient->print(soubor.name());
      klient->print(F("]</b><br />"));
    }
    else {
      klient->print(F("<a href=\""));
      klient->print(soubor.name());
      klient->print(F("\">"));
      klient->print(soubor.name());
      klient->print(F("</a> ("));
      if (soubor.size() > 1000) {
        klient->print((soubor.size() / 1000.0), 2);
        klient->print(F(" kB"));
      }
      else {
        klient->print(soubor.size());
        klient->print(F(" B"));
      }
      klient->print(")<br />");
      klient->println(F("</body></html>"));
    }
    soubor.close();
  }
}

// Pomocna funkce pro zjisteni volen RAM na cipu ATmega328P
int volnaPamet() {
  extern int __heap_start, *__brkval;
  int v;
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}

// Pomocna funkce pro vypsani volne pameti do seriove linky
void vypisVolnouPamet() {
  Serial.print(F("(Volna pamet RAM: "));
  Serial.print(volnaPamet());
  Serial.println(F(" B)"));
}
Diskuze (18) Další článek: Nokia 9 Pureview: Pětifoťák potěší hračičky, cena všechny ostatní

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , , ,