Wstęp
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:
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.