Miesięczne archiwum: Maj 2011

P4A – PHP dla Arduino cz. 2

W poprzednim wpisie skończyliśmy w miejscu w którym Webduino mogło nam już serwować dowolne pliki z karty SD. Teraz musimy wybrane pliki przepuścić przez nasze PHP :) i rezultat przesłać do klienta.

Dla uproszczenia całego procesu, zakładamy, że każdy plik który ma zostać poddany obróbce jest nam znany. Tzn rejestrujemy każdy taki plik (URL) za pomocą addCommand.

Następnie jak to ma działać? Idea jest taka, że mamy swoje funkcje w kodzie szkicu i których wynik działania ma zostać wklejony w wybrane miejsca kodu HTML. Czyli chcemy mieć plik HTML z takim kawałkiem kodu:

<p>
Wynik odczytu czujnika 1: MAGIA1<br/>
Wynik odczytu czujnika 2: MAGIA2<br/>
</p>

Na skutek działania naszego parsera chcemy MAGIA1 i MAGIA2 mieć zastąpione przez wynik działania funkcji w szkicu.

Najlepiej zacząć od magii!

Czyli jak zapisać w HTMLu że ma nasz parser wsadzić w to miejsce inny tekst. Dla uproszczenia przyjmujemy następującą formułę: #{X} zostanie zastąpione przez wywołanie odpowiedniej funkcji. X to jest jednoliterowy mnemonik określający którą funkcję chcemy wywołać.

Funkcje muszą mieć określoną definicję i nie mogą przyjmować argumentów. Dlaczego? Przyjmowanie argumentów komplikuje parser i na tym etapie nie jest chyba potrzebne.

Wybraliśmy sposób zapisu w HTML. Teraz, jak nasza funkcja ma przekazać wynik działania? Otóż zakładamy że przykładowa funkcja ma mieć następującą definicję:

void timeReport(char *buf) { itoa(millis()/1000, buf, 10); };

Funkcja ma nie zwracać danych (void) a przyjmować wskaźnik na bufor tekstowy. W tym buforze ma umieścić wynik swojego działania, który następnie zostanie wstawiony w odpowiednie miejsce HTML. Funkcja sama musi pilnować, żeby nie przepełnić tego bufora. Jego rozmiar jest definiowany przez P4A_MAX_FUNCTION_RESPONSE_LENGTH.

Jak widać powyższa funkcja zwraca liczbę pełnych sekund które minęły od uruchomienia lub resetu Arduino.

Jak parsować plik? Dzięki prostemu znacznikowi naszej magii jest to względnie proste. Czytamy plik znak po znaku. Jeżeli natkniemy się na # wówczas czytamy następny znak i sprawdzamy czy jest to klamra { tworzące w sumie sekwencję otwierającą naszej magii. Póki nie natkniemy się na # znaki nas nieinteresujące wysyłamy do bufora, który zostanie w końcu wysłany do przeglądarki.

Jeżeli następny znak po # nie jest klamrą wysyłamy do przeglądarki oba znaki # i następny – w tym miejscu nie mamy nic do robienia, czekamy na następny #.

Gdy jednak drugi znak to była klamra, wówczas czytamy kolejny znak – jest to nasz mnemonik! Wywołujemy odpowiednią funkcję w zależności od mnemonika, wysyłamy wynik do przeglądarki.

Następnie czytamy plik aż do zamykającej klamry i skanujemy dalej plik szukając kolejnego #.

Pozostaje nam kwestia przypisania funkcji do mnemoników. Posłuży nam do tego

Tablica wskaźników do funkcji

Pomówimy teraz o trochę bardziej zaawansowanym temacie, czyli o wskaźnikach do funkcji. Otóż jak się dobrze zastanowić, funkcja po skompilowaniu jest adresem pod którym znajduje się kod ją realizujący oraz pewien kontrakt określający jak przekazywane są dane do funkcji i jak z niej są zwracane.

Jeżeli kontrakt jest taki sam w wypadku wielu funkcji (czyli lista typów argumentów oraz zwracana wartość), funkcje takie można zapisać w postaci samego wskaźnika i trzymać w tablicy. Wówczas możemy wywołać taką funkcję używając zapisanego wskaźnika, nie musimy znać jej nazwy w kodzie.

To właśnie nam posłuży jako mechanizm tłumaczący mnemoniki na wywoływane funkcje. Teraz wiemy czemu wszystkie nasze funkcje muszą mieć taki sam interfejs/kontrakt (jak ustaliliśmy będzie do void jeżeli chodzi o zwracane dane oraz char * jako argument) – dzięki temu możemy je trzymać w jednej tablicy, której indeks będzie literą. Ale po kolei:

void (*_fcts['z'-'a'])(char *);

Definiujemy tablicę wskaźników na funkcje. void z przodu określa typ zwracany przez funkcję, to co w nawiasie na końcu oznacza jakich argumentów spodziewa się funkcja. W środku znajduje się deklaracja tablicy. Jej rozmiar 'z'-'a' może wydawać się dziwny, ale w takim kontekście znaki przez kompilator traktowane są jako bajty. Czyli od kodu ‚z’ odejmujemy kod ‚a’ – różnica to liczba liter. Dzięki temu mamy tablicę mogącą pomieścić tyle wskaźników na funkcje ile jest małych liter w alfabecie łacińskim (lub raczej w standardzie ASCII).

Jeśli będziemy mieli jakiś kod litery wystarczy od niej odjąć kod litery a a dostaniemy indeks z tablicy. Zresztą zobaczmy:

				if (_fcts[c[0]-'a'] == NULL) {
					bufferedSend(server,"n/a");
					continue;
				} else {
					//Call function from table
					_fcts[ c[0]-'a'](buf);
					//Write response to client
					bufferedSend( server, buf );
				}

c[0] zawiera znak naszego mnemonika. Jeżeli tablica nie ma wartości (tzn ma wartość NULL) pod indeksem c[0] - 'a' wówczas w HTML jest wstawiane n/a – nasz sposób na sygnalizowanie złych mnemoników.

'a' w ASCII ma kod 97. Jeżeli nasz mnemonik to też 'a' 97-97 = 0, czyli pierwszy element tablicy. Jeżeli mnemonik to 'b' to 'b'-'a' = 98 – 97 = 1 czyli drugi element tablicy, itd. Przydałoby się sprawdzać czy mnemonik jest we właściwym zakresie, bo inaczej możemy próbować wywołać funkcję z losowym adresem (jeżeli indeks tablicy jest poza zakresem, wówczas pamięć RAM spoza tablicy zostanie odczytana i procesor spróbuje zinterpretować wartość spod tego adresu jako adres funkcji do wywołania – gwarantowane zawieszenie się programu).

W powyższym kodzie widać też, jak należy wywoływać funkcje w tablicy:

_fcts[ c[0]-'a'](buf);

Do analizy pliku wykorzystujemy zmienną status, dzięki której wiem w jakim stanie nasz parser się znajduje. Może to być:

  • P4A_PARSE_REGULAR – stan w którym szukamy znaku #
  • P4A_PARSE_HASH – stan w którym czekamy na klamrę otwierającą
  • P4A_PARSE_COMMAND – stan w którym szukamy mnemonika

Dzięki tym stanom łatwiej kontrolować co robi nasz parser. W zależności od bieżącego stanu oraz kolejnego znaku można podejmować decyzje co dalej. Wysyłać dane do przeglądarki czy wywoływać funkcję na podstawie mnemonika.

Dla zainteresowanych całość funkcji parseP4A:

//Reads HTML file, parses looking for our macro and sends back to client
int parseP4A( char * filename, WebServer &server ) {
	//simple status
	short int STATUS = P4A_PARSE_REGULAR;
	char c[2];
	c[1] = 0;

	//buffer to hold response from functions - there is no boundary checking, so
	//function has to not overwrite data
  char buf[P4A_MAX_FUNCTION_RESPONSE_LENGTH];

  if (! file.open(&root, filename, O_READ)) {
		return -1;
  }
  while ((file.read(c,1) ) > 0) {
		if (STATUS == P4A_PARSE_REGULAR && c[0] == '#')
		{
			//hash was found we need to inspect next character
			STATUS = P4A_PARSE_HASH;
			continue;
		}

		if (STATUS == P4A_PARSE_HASH) {
			if (c[0] == '{') {
				//go to command mode
				STATUS = P4A_PARSE_COMMAND;
				continue;
			} else {
				//fallback to regular mode, but first send pending hash
				STATUS = P4A_PARSE_REGULAR;
				bufferedSend(server, "#");
			}

		}

		if (STATUS == P4A_PARSE_COMMAND) {
			if(c[0] == '}') {
				STATUS = P4A_PARSE_REGULAR;
				continue;
			};
		  if (c[0] >= 'a' && c[0] <='z') {
				//Command found
				if (_fcts[c[0]-'a'] == NULL) {
					bufferedSend(server,"n/a");
					continue;
				} else {
					//Call function from table
					_fcts[ c[0]-'a'](buf);
					//Write response to client
					bufferedSend( server, buf );
				}
			}

		}

		if (STATUS == P4A_PARSE_REGULAR)
			bufferedSend(server, c);
  }

  //force buffer flushing
  flushBuffer(server);
	file.close();
  return 0;
}

P4A czyli PHP for Arduino w akcji

Załóżmy, że chcemy zrobić ładny wirtualny barometr, ale pokazujący prawdziwe ciśnienie. Skorzystamy z BMP085 – jest to poręczny adapter (breakout board) czujnika ciśnienia i temperatury  produkowany przez SparkFun. Rezultat ma być następujący:

Wygląd wirtualnego barometru

Wygląd wirtualnego barometru

Wskazówka ma pokazywać wartość odczytaną z czujnika, a symbol prognozowanej pogody ma się zmieniać w zależności od wartości ciśnienia.

Jak sobie poradzimy z tym, że jak pisaliśmy w poprzednim odcinku, serwer WWW na Arduino, nie jest najlepszym rozwiązaniem, gdy trzeba serwować wiele plików jednocześnie (obrazki!)? Ano, skoro całość i tak ma być dostępna z sieci, czemu nie udostępnić statycznych zasobów z serwera sieci? Na swoje potrzeby mam taki serwer i wszystkie potrzebne elementy graficzne są na nim umieszczone. Czyli tarcza barometru, obraz wskazówki i symbole pogody.

Na Arduino znajduje się sam plik HTML, na karcie SD. W szkicu umieszczamy funkcję wywołującą parser dla tego pliku:

void index(WebServer &server, WebServer::ConnectionType type, char *, bool){
  server.httpSuccess();
  if (!parseP4A("BARO.HTM", server)) {
		Serial.println("P4A: -1");
	}
};

parseP4A to funkcja, która parsuje podany plik i wysyła rezultat korzystając z obiektu serwera Webduino. Pozostaje zarejestrować naszą funkcję jako domyślną komendę:

webserver.setDefaultCommand(&index);

Sam HTML wykorzystuję JaveScript i obiekt canvas do narysowania samej tarczy. Robi to funkcja draw, która jako argument przyjmuje ciśnienie w hektopaskalach. Gdy strona jest gotowa do wyświetlenia (tzn załadowała się) przez atrybut onload wywołujemy funkcję draw, ciśnienie jest wstawiane przez nasze P4A:

<body onload="draw(#{p});"

Mnemonik p trzeba skojarzyć z właściwą funkcją. W szkicu, w setup ustawiamy funkcję dla p:

  _fcts['p'-'a'] = pressureReport;

A samo pressureReport ma wygląd:

void pressureReport(char *buf) {
	bmp085_read_temperature_and_pressure (&temperature, &pressure);
	itoa(pressure/100.0, buf, 10);
	Serial.print ("temp & press: ");
	Serial.print (temperature/10.0);
	Serial.print(" ");
	Serial.println (pressure/100.0);

};

Serial jest używany do sprawdzania czy wartości są jak się spodziewamy i nie ma wpływu na działanie barometru. bmp085_read_temperature_and_pressure to funkcja z kodu obsługi BMP085 zaczerpnięta z fińskiego bloga.

Całość kodu do ściągnięcia tutaj. Jest to szkic napędzający nasz serwer, plus plik HTML i grafiki. Tarcza barometru, grafika i kod HTML/JS autorstwa Sprae.

Instalacja

Ściągnąć, rozpakować w sketchbook. Zawartość podkatalogu html wrzucić w główny katalog na karcie SD, wsadzić ją w shielda. Szkic poprawić podając właściwy MAC i IP adres. Po otwarciu strony głównej powinniśmy zobaczyć barometr, o ile posiadamy BMP085 ;)

Kilka uwag na koniec

Kod jest wersją beta :) tzn – działa na tyle ile moje testy to potwierdzają, może być (i na pewno jest) kilka błędów o których nie mam pojęcia :)

Kod należałoby uporządkować – np funkcje związane z buforowanym wyjściem powinny zostać przeniesione do kodu Wbeduino. Planuję to zrobić i wszystkie zmiany jakie w Webduino zostały wprowadzone wysłać do developerów Webduino – może coś z tego znajdzie się bezpośrednio w bibliotece.

Jestem ciekaw waszych opinii. Czy coś takiego jak P4A ma w ogóle sens i może być przydatne?

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.