P4A – PHP dla Arduino cz. 1

Gdy poznamy już trochę Arduino w głowie każdego prędzej czy później pojawi się pomysł na projekt, który wymaga aby Arduino mogło połączyć się z siecią. Czy to będzie automatyka domowa dostępna przez sieć, czy zestaw czujników raportujący odczyty do bazy danych – trzeba jakoś połączyć Arduino do Internetu. I tu z pomocą przychodzi nam Ethernet Shield.

Najpierw trochę historii. Ethernet Shield był początkowo kompatybilny z małym Arduino. Dlaczego nie z Mega? Otóż do komunikacji z układem W5100 będącym sercem shielda wykorzystywany jest protokół SPI – na cyfrowych wejściach nr 10, 11, 12 i 13. W Arduino Mega SPI jest na innych wejściach. Można to obejść wyginając nóżki shielda i podłączając je do właściwych wejść cyfrowych kabelkiem.

Ale to dotyczy starszych shieldów. Obecnie sprzedawane przez Nettigo Ethernet Shieldy są kompatybilne zarówno z małymi Arduino (UNO, Duemilanove) jaki i Mega (te z procesorami ATmega1280 i ATmega2560) . Po czym poznać takiego shielda? Jeżeli jest na nim gniazdo kart microSD – znaczy to, że to jest nowsza wersja.

Ethernet Shield z gniazdem kart microSD
Ethernet Shield z gniazdem kart microSD

 

 

Wspomniany już został W5100 – układ scalony koreańskiej firmy WIZnet, napędzający Ethernet Shielda. Różni się tym od wielu innych kontrolerów Ethernet, że stos TCP/IP jest zaimplementowany bezpośrednio w układzie scalonym.  Co to znaczy dla użytkownika? Że biblioteka, którą musisz wykorzystać aby komunikować się ze światem potrzebuje mniej pamięci RAM i zajmuje mniej pamięci flash w porównaniu z układami nie mającymi na sobie stosu TCP/IP.

Jak korzystać z Ethernet Shielda?

Jest wiele przykładów w sieci jak tworzyć strony WWW wyświetlające dane z Arduino. Jednak, jeżeli masz już większe  doświadczenie z programowaniem Arduino to pewnie wiesz, że wszystkie łańcuchy znakowe, nawet te zdefiniowane w kodzie zajmują RAM, którego w ATmedze zawsze mało.

Weźmy oficjalny przykład z Arduino Tutoriala. Kod:

 client.println("HTTP/1.1 200 OK");
 client.println("Content-Type: text/html");
 client.println();

Zajmie nam 40 bajtów RAMu (15 znaków w HTTP… + kończące zero i 23 w Conte… + kończące zero). Łatwo sobie wyobrazić co to znaczy gdy mamy 2kB do dyspozycji w ogóle. Strona HTML nie może być zbyt rozbudowana.

Istnieje pewne rozwiązanie, które może nam pomóc – czyli przechowywanie stringów w pamięci Flash. Pozwala to zmniejszyć użycie pamięci RAM, ale często kosztem dodatkowego kodu. Dostęp do stringów tak definiowanych wymaga użycia specjalnego makra i kompilator nie pozwoli nam korzystać z tego makra w wywołaniu funkcji oczekującej char * jako argumentu. Na dodatek – każda zmiana w kodzie HTML, który chcemy wysłać oznacza, że trzeba zmodyfikować szkic i wgrać go w Arduino.

Zaraz, przecież Ethernet Shield od dwóch wydań ma na sobie gniazda na karty microSD – nie można jakoś wykorzystać przestrzeni jaką dają karty SD? Można, ale trzeba się trochę postarać.

Najpierw – Ethernet Shield musi zostać skonfigurowany do pracy w sieci – oznacza to ustawienie adresu MAC oraz IP. Można to zrobić tak:

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
byte ip[] = { 192,168,1, 177 };

Ethernet.begin(mac, ip);

Adres MAC najlepiej, zamiast wymyślać samemu odczytać z naklejki jaka znajduje się na spodzie shielda. Adres IP zależy od konfiguracji sieci. Powyższa sekwencja Ethernet.begin będzie działać tylko w sieci lokalnej – tzn gdy wszystkie adresy IP znajdują się w jednej podsieci. Jeżeli shield ma się łączyć z hostami w innych sieciach (zarówno jako klient lub gdy ma służyć jako serwer) musicie podać jeszcze jeden argument – tablice z 4 liczbami – adres IP domyślnej bramki (default gateway). Więcej w opisie biblioteki Ethernet.

Większość klocków mamy prawie gotowych. Biblioteka Ethernet, wchodząca w skład Arduino IDE, nie jest tym co nam najbardziej będzie pasować. Ułatwia ona tworzenie m.in. serwera TCP, ale do serwera HTTP jeszcze trzeba kawałek. Dlatego przyda się nam Webduino – na bazie biblioteki Ethernet, ktoś za nas wykonał spory kawałek pracy budując serwer HTTP.

Normalnie ściągnęlibyśmy ze strony Downloads bibliotekę i rozpakowalibyśmy w sketchbook/libraries. Na nasze potrzeby będziemy modyfikować trochę Webduino,  będzie do ściągnięcia na końcu tego mini-tutoriala, więc na razie nic nie trzeba instalować.

Webduino – jak zaczać?

Na początek musimy wiedzieć, że za pomocą

void addCommand(const char *verb, Command *cmd);

możemy zarejestrować dowolną funkcję wywoływaną gdy URL będzie pasował do verb. Przykład:

webserver.addCommand("blob.htm", &blob);

Jeżeli URL będzie się zaczynał (tzn porcja po adresie Arduino) od blob.htm, wówczas wywołana zostanie funkcja blob, która musi przyjmować argumenty zgodnie ze zdefiniowanym typem Command:

typedef void Command(WebServer &server, ConnectionType type,

                       char *url_tail, bool tail_complete);

server to obiekt WebServer, dla którego nastąpiło wywołanie metody, type to rodzaj połaczenia (INVALID, GET, HEAD, POST), url_tail to jest to co zostało w URLu po dopasowanym blob.htm. Jeżeli URL został obcięty ze względu na niewielki bufor używany przez Webduino (ehh ta pamięć), to ostatni parametr tail_complete bedzie miał wartość false.

OK, ale to mało wygodne musieć rejestrować każdą funkcję, zwłaszcza, że chcemy serwować dane z karty SD, której zawartości nie znamy. Przyda się nam teraz setFailureCommand, która pozwoli zarejestrować funkcję w naszym kodzie wywoływaną, gdy nie nastąpiło żadne inne dopasowanie do zarejestrowanych funkcji przez addCommand.

Czyli jeżeli URL nie pasuje do żadnej zgłoszonej wcześniej funkcji, wówczas zostanie wywołana funkcja podana do setFailureCommand.

Teraz wystarczy w niej sprawdzić czy url_tail jest nazwą pliku na karcie SD (ponieważ nie nastąpiło żadne dopasowanie, url_tail zawiera pełny URL włącznie z pierwszym znakiem / za adresem Arduino). Gdy pliku nie ma – wyświetlamy HTTP 404, jeżeli jest – wystarczy go wysłać do klienta.

Jak odczytać plik z karty SD?

Podobnie jak z serwerem HTTP nie musimy robić wszystkiego sami. Dobrą pomocą do stworzenia tego szkicu był tutorial przygotowany przez Limor Fried czyli – LadyAda. Korzysta on z biblioteki SdFatLib, która ma wsparcie dla systemów plików FAT16 i FAT32 (czyli tego co zwykle na kartach SD i SDHC jest).

Kod inicjalizacji i obsługi plików w zasadzie został wzięty z tego tutoriala. Omówię tutaj funkcję fetchSD, która została zarejestrowana przez setFailureCommand. Jej zadaniem jest znaleźć plik na karcie i wysłać go do przeglądarki:

P(CT_PNG) = "image/png\n";
P(CT_JPG) = "image/jpeg\n";
P(CT_HTML) = "text/html\n";
P(CT_CSS) = "text/css\n";
P(CT_PLAIN) = "text/plain\n";
P(CT) = "Content-type: ";
P(OK) = "HTTP/1.0 200 OK\n";
void fetchSD(WebServer &server, WebServer::ConnectionType type, char *urltail, bool){
	char buf[32];
	int16_t  readed;

	++urltail;
	char *dot_index; //Where dot is located
	if (! file.open(&root, urltail, O_READ)) {
		//Real 404
		webserver.httpNotFound();
  } else {
	if (dot_index = strstr(urltail, ".")) {
		++dot_index;
		server.printP(OK);
		server.printP(CT);
		if (!strcmp(dot_index, "htm")) {
				server.printP(CT_HTML);

		} else if (!strcmp(dot_index, "css")) {
				server.printP(CT_CSS);

		} else if (!strcmp(dot_index, "jpg")) {
				server.printP(CT_JPG);

		} else {
				server.printP(CT_PLAIN);
		}
		server.print(CRLF);
	}
	readed = file.read(buf,30);
	while( readed > 0) {
		buf[readed] = 0;
		bufferedSend(server,buf,readed);
		readed = file.read(buf,30);
	}
	flushBuffer(server);
	file.close();
	}
}

Na samym początku mamy zarejestrowanych kilka stałych znakowych przechowywanych w pamięci flash.

P(CT_PNG) = "image/png\n";

Makro P jest częścią Webduino, które służy do zapisywania stringów na pamięci flash a nie w RAM. A stałe te to są nazwy różnych formatów danych, tzw MIME Type, jakie chcemy obsługiwać. O co chodzi? Przeglądarka nie wie jakie dane zostaną wysłane przez serwer. Czy to będzie HTML czy obrazek dowiaduje się ona właśnie z nagłówka Content-Type, o którym za chwilę.

Następnie 'pozbywamy’ się wiodącego ukośnika: ++urltail;, potem próbujemy otworzyć plik na karcie SD – jeżeli nie udaje się – to wyświetlamy błąd HTTP 404 (Not Found):

	if (! file.open(&root, urltail, O_READ)) {
		//Real 404
		webserver.httpNotFound();
  } else {

jeżeli się udało otworzyć plik, to w else spróbujemy go odczytać i wysłać do klienta.

Teraz kilka uwag. Po pierwsze – SdFatLib obsługuje tylko krótkie nazwy w formacie 8.3. Jeżeli spróbujesz użyć dłuższych nazw (które FAT32 dopuszcza) to pamiętaj, że nazwa widziana przez SdFatLib będzie inna od tej, którą zobaczysz po podmonotwaniu karty w twoim komputerze. I jeżeli zrobisz plik index.html (cztery znaki w rozszerzeniu), wówczas nazwa będzie dla SdFatLib ind~1.htm. Cóż, nawet jeżeli teraz w komputerze zmienisz nazwę na index.htm, wpis w katalog będzie w rozszerzonej formie. Musisz skasować plik i utworzyć go na nowo z nazwą w formacie 8.3.

Druga uwaga jest taka – z oczywistych względów (FISI :) ) nie będziemy się przejmować katalogami i zakładamy że wszystkie pliki są w głównym katalogu. Może w późniejszych wersjach kodu dodam obsługę trochę bardziej skomplikowanych struktur.

OK, wracamy do kodu fetchSD. Skoro udało się nam otworzyć plik, to szukamy kropki w nazwie pliku i jeżeli ją znajdziemy to sprawdzamy czy pozostała część (w domyśle – rozszerzenie) będzie pasować do znanych nam rozszerzeń. Bo nie wystarczy nam wysłać danych do klienta HTTP – musimy wysłać nagłówek z informacją o właściwym Content-Type (mówiliśmy o tym już wcześniej), inaczej dane mogą zostać błędnie zinterpretowane przez przeglądarkę.

Słowo o tym jak wygląda odpowiedź serwera WWW. Podzielona ona jest na dwie części. Pierwsza to tak zwane nagłówki. Przeglądarka jako nagłówek traktuje wszystko to co na początku, aż natrafi na pustą linię tekstu (linie są oddzielane znakami CR LF). Reszta to właściwa odpowiedź. Jak co ona zostanie zinterpretowana, będzie zależało od nagłówków. Serwer może pomóc przeglądarce przez ustawienie nagłówka określającego typ danych:

Content-type: text/html

Do pierwszego dwukropka jest nazwa nagłówka (tutaj Content-Type) potem wartość nagłówka. Tutaj używane są tak zwane typy MIME. I tak może to być np image/png dla obrazka PNG, image/jpg dla JPG czy text/html dla pliku HTML.

Obowiązkowym nagłówkiem jest status – czyli czy żądanie klienta zostało obsłużone, czy wystąpił błąd a może przekierowanie. HTTP/1.0 200 OK znaczy – jest dobrze, będzie żądna treść. Najpierw jest protokół i jego wersja (HTTP w wersji 1.0) a następnie sam kod 200 – w świecie HTTP znaczy to że jest dobrze. Inne częste kody to 404 – nie znaleziono zasobu (sławne Not Found), 301 i 302 – przekierowania.

Wiedząc to, staramy się rozpoznać rozszerzenie pliku i wysłać odpowiedni nagłówek:

	if (dot_index = strstr(urltail, ".")) {
		++dot_index;
		server.printP(OK);
		server.printP(CT);
		if (!strcmp(dot_index, "htm")) {
				server.printP(CT_HTML);

		} else if (!strcmp(dot_index, "css")) {
				server.printP(CT_CSS);

		} else if (!strcmp(dot_index, "jpg")) {
				server.printP(CT_JPG);

		} else {
				server.printP(CT_PLAIN);
		}
		server.print(CRLF);

Funkcje z dużym P na końcu oczekują nie char * ale const prog_uchar *.

Mamy już wysłany nagłówek HTTP (zakończony pustą linią server.print(CRLF)), więc wyślemy same dane:

	readed = file.read(buf,30);
	while( readed > 0) {
		buf[readed] = 0;
		bufferedSend(server,buf,readed);
		readed = file.read(buf,30);
	}
	flushBuffer(server);
	file.close();

Czytamy po 30 bajtów, wysyłamy do klienta przez funkcję buforującą wysyłane dane. Po co? Otóż jeżeli użyjemy najprostszego rozwiązania i będziemy wysyłać dane znak po znaku, wówczas każdy znak będzie w odrębnym pakiecie TCP. Bardzo (naprawdę, uwierz, naprawdę) nieefektywne rozwiązanie. Po prostu server.write wysyła od razu dane.

Dlatego napisałem funkcję bufferedSend, która jako argumenty bierze obiekt serwera WWW, wskaźnik na bufor z danymi i rozmiar bufora. Czemu nie korzystamy z funkcji określających rozmiar bufora znakowego takich jak strlen? Bo działać to może tylko gdy dane są tekstowe. Jeżeli dane są binarne (obrazki) to znacznik końca łańcucha może pojawić się w legalnym strumieniu danych.

W C i C++ znakiem końca łańcucha jest znak 0 (nie cyfra, tylko bajt o wartości 0). Jeżeli w naszym strumieniu danych mogą pojawić się zera, wszystkie funkcje związane z łańcuchami znakowymi, a oferowane przez standardową bibliotekę nie przydadzą się nam

Z tego powodu musimy wprost określić ilość danych wysyłanych do bufora.

I w zasadzie to tyle. Mamy na Arduino serwer WWW wysyłający dane z karty SD.

Czy to ma sens?

Wystarczy kilka testów z bardziej złożoną stroną WWW (nie jeden plik HTML ale do tego jakieś CSS i obrazki), żeby przekonać się, że rozwiązanie to ma swoje ograniczenia. Arduino jest jednowątkowe, więc każdy element z naszego serwera WWW jest ściągany po kolei. Oznacza to, że z punktu widzenia użytkownika strona się wolno ładuje.

Więc po co to? Arduino może prezentować dane zbierane z czujników w przyjaźniejszej formie jeżeli nie będzie ograniczeniem ilość pamięci RAM potrzebnej bardziej rozbudowanej stronie WWW. Trzymając kod HTML na karcie SD pozbywamy się tego ograniczenia. Ale jak w HTMLu trzymanym na karcie SD umieścić dane zebrane z czujników przez Arduino?

Potrzebujemy czegoś, co pozwoli nam wstrzyknąć dane do HTMLa pomiędzy 'odczytem’ a 'wysłaniem’. Czyli coś jak:

PHP dla Arduino

OK, to jest na wyrost :) potrzebujemy czegoś co bardziej przypomina szablony niż pełne PHP, ale na początku PHP też nie powalało funkcjonalnością :)

O tym jak zrobić taki parser (i pełny kod szkicu) – w następnym odcinku. Stay tuned.