Poniżej opisany projekt przedstawia “wrap” biblioteki EtherCard https://github.com/njh/EtherCard.
Biblioteka EtherCard bardzo ładnie rozwiązuje konfigurację modułu ethernet opartego na układzie ENC28J60, pozwala ona m.in. na zestawienie połączenia ze statycznym IP lub uzyskanym poprzez DHCP.
Dzięki bibliotece EtherCard w łatwy sposób można zrobić aplikację serwer/klient obsługującą powiadomienia lub dostarczającą dane telemetryczne, a nawet prostą aplikację IoT.
Brakowało mi jednak przejrzystości w sketch-u i prostoty obsługi żądań HTTP. Bazując na przykładach udostępnionych przez EtherCard i ogólnodostępnej wiedzy, dopisałem wrap tej biblioteki i nazwałem ją EtherDevice.
Spis treści:
- Opis nagłówka
- Przykładowe użycie
- Sposób połączenia modułu ethernet i arduino
- Sketch z komentarzami
- Sketch bez komentarzy
- Wady/Zalety
- Załączniki
1. Opis nagłówka
Na początku postanowiłem zamknąć fragmenty kodu, które pierwotnie zmniejszały czytelność sketch-a właściwego.
Utworzyłem klasę EtherDevice dziedziczącą publicznie po EtherCard, a w niej zawarłem poniżej opisane metody.
Inicializacja z adresem IP przydzielanym przez DHCP:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//DHCP IP init // mac - MAC adres urządzenia // slaveSelect - określa pin dla chip select układu gdy płyta Arduino inna niż UNO int init(byte mac[], int slaveSelect) { if (begin(sizeof Ethernet::buffer, mac, slaveSelect) == 0) return 1;//Failed to access Ethernet controller if (!dhcpSetup()) return 2;//DHCP failed return 0; } |
Inicializacja z adresem IP ustawionym statycznie:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//STATIC IP init // mac - MAC adres urządzenia // myip - adres IP, wymaga dodania w sketch-u: // static byte myip[] = { 192,168,1,200 };<br /> // gateway - adres bramy domyślnej, wymaga dodania w sketch-u:<br /> // static byte gwip[] = { 192,168,1,1 }; // slaveSelect - określa pin dla chip select układu gdy płyta Arduino inna niż UNO int init(byte mac[], byte ip[], byte gateway[], int slaveSelect) { if (begin(sizeof Ethernet::buffer, mac, slaveSelect) == 0) return 1;//Failed to access Ethernet controller staticSetup(ip, gateway); return 0; } |
Metoda ipconfig:
1 2 3 4 5 6 7 |
// Wypisuje poprzez Serial konfigurację IP urządzenia void ipconfig() { printIp("IP: ", ether.myip); printIp("GW: ", ether.gwip); printIp("DNS: ", ether.dnsip); } |
Metoda statycznej odpowiedzi – staticHttpServerReply:
1 2 3 4 5 6 7 8 |
// Wysyła statycznie zdefiniowaną odpowiedź na żądanie HTTP // mem - stały zdefiniowany łańcuch void staticHttpServerReply(const char mem[]) { bfill = tcpOffset(); bfill.emit_p(mem); httpServerReply(bfill.position()); } |
Powyższa metoda ta wymaga zdefiniowanych stałych łańcuchów, zdefiniowałem typowe odpowiedzi HTTP (200, 401, 404, 405, 406), przydatne w dalszej części usprawnienia biblioteki EtherCard:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
const char HTTP200[] PROGMEM = "HTTP/1.1 200 OK\r\n" "Server: Arduino Ethernet Devices\r\n" "Cache-Control: no-store\r\n" "Connection: Keep-Alive\r\n" "Content-Type: text/html; charset=utf-8\r\n\r\n"; const char HTTP401[] PROGMEM = "HTTP/1.0 401 Unauthorized\r\n" "Content-Type: text/html\r\n\r\n" "<h1>401 Unauthorized</h1>"; const char HTTP404[] PROGMEM = "HTTP/1.0 404 Not Found\r\n" "Content-Type: text/html\r\n\r\n" "<h1>404 Not Found</h1>"; const char HTTP405[] PROGMEM = "HTTP/1.0 405 Method Not Allowed\r\n" "Content-Type: text/html\r\n\r\n" "<h1>405 Method Not Allowed</h1>"; const char HTTP406[] PROGMEM = "HTTP/1.0 406 Not Acceptable\r\n" "Content-Type: text/html\r\n\r\n" "<h1>406 Not Acceptable</h1>"; |
Następnie napisałem metodę, ułatwiającą parsowanie argumentów żądania HTTP, np:
po wpisaniu w przeglądarce http: //ip_urządzenia/?arg_1=val_1&arg_2=val_2& … &arg_C=val_C
jest C niewygodnych do sparsowania argumentów oddzielonych znakiem “&”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
// Zwraca liczbę argumentów przychodzących w żądaniu HTTP // i zapisuje argumenty pod wskaźnikiem argv // argc - dopuszczalna liczba argumentów // argv wskaźnik tablicy struktur argumentów int getTask(int argc, arg argv[]) { word offset = packetLoop(packetReceive()); if ( offset ) { char *request = (char *) Ethernet::buffer + offset; if (strncmp("GET /", request, 5) != 0) { // 405 Unsupported HTTP request staticHttpServerReply(HTTP405); return -1; } else { request += 5; if (request[0] == ' ') { //Jeżeli nie ma żadnych argumentów for(int idx=0; idx<argc; idx++) { argv[idx].argName = ""; argv[idx].argValue = ""; } return 0; } else { if (!strncmp("?",request,1)) { //Wycinanie rządania od znaku "?" + 1, żeby tego znaku "?" nie było w ciągu, //do znaku " " o indeksie endIdx. int endIdx = strcspn (request," "); request += strcspn (request,"?") + 1; int idx = 0; int args_valid = 0; int args_cnt = 0; for( idx=0; idx<endIdx; idx++ ) { if(request[idx]=='&') args_cnt++; if(request[idx]=='=') args_valid++; } if( (args_valid == (args_cnt+1)) && (args_valid<=argc) ) { for( idx=0; idx<argc; idx++) { argv[idx].argName = ""; argv[idx].argValue = ""; } idx=0; bool tictac=0; for( int c=0; c<endIdx; c++ ) { if( tictac == 0) { if( request[c] == '=') { tictac=!tictac; }else{ argv[idx].argName += request[c]; } }else{ if( request[c] == '&') { tictac=!tictac; idx++; }else{ argv[idx].argValue += request[c]; } } } return args_cnt+1; }else{ // 406 Not Acceptable staticHttpServerReply(HTTP406); return -3; } } else { // 404 Page not found staticHttpServerReply(HTTP404); return -2; } } } } else { return -1; } } |
Powyższa metoda wymaga zdefiniowanej struktury dla argumentów argv:
1 2 3 4 5 6 |
// Struktura przechowywania argumentów żądania struct arg { String argName; String argValue; }; |
Kolejno dopisałem metodę pozwalającą w przyjazny sposób wypisać w Serial wszystkie argumenty żądania, mając już je sparsowane przy użyciu metody getTask:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Wypisuje w Serial wszystkie argumenty przychodzące // argc - aktualna liczba argumentów // argv wskaźnik tablicy struktur argumentów void printArg(int argc, arg argv[]) { for(int i=0;i<argc;i++) { Serial.print("["); Serial.print(argv[i].argName); Serial.print(" = "); Serial.print(argv[i].argValue); Serial.println("]"); } } |
Nie mogło zabraknąć też metody wyszukującej czy dany argument zaistniał w żądaniu i zwracającej jego wartość:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// Zwraca wartość argumentu o nazwie argName jeżeli istnieje // argc - aktualna liczba argumentów // argv wskaźnik tablicy struktur argumentów String getArg(int argc, arg argv[], String argName) { argName.trim(); for(int i=0;i<argc;i++) { argv[i].argName.trim(); if(argv[i].argName.compareTo(argName)==0) { argv[i].argValue.trim(); return argv[i].argValue; } } return ""; } |
Dodatkowo napisałem metodę sprawdzającą i porównującą czy istnieje dany argument o danej wartości:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Sprawdza czy argument o podanej nazwie argName zawiera argValue // argc - aktualna liczba argumentów // argv wskaźnik tablicy struktur argumentów int checkArg(int argc, arg argv[], String argName, String argValue) { argName.trim(); argValue.trim(); if(getArg(argc, argv, argName).compareTo(argValue)==0) { return 0; } return 1; } |
Wewnątrz klasy jest zadeklarowana metoda webPage służąca wygenerowaniu odpowiedzi na żądanie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// Służy wygenerowaniu odpowiedzi na żądanie HTTP // Definicja musi być w pliku .ino (w sketch-u) jako: // word EtherDevice::webPage() // { // // Ustawia offset w BufferFiller // bfill = tcpOffset(); // // // Wewnątrz PSTR(...) możesz używać następujących typów: // // $D = word data type // // $L = long data type // // $S = c string // // $F = progmem string // // $E = byte from the eeprom // bfill.emit_p( // PSTR( // "$F"// Tego $F nie zmieniaj i nie usuwaj - to nagłówek odpowiedzi HTTP OK // " jakiś tekst html $D jakiś tekst html " // " jakiś tekst html $D jakiś tekst html" // "..." // ... // ), // HTTP200,// Tego HTTP200 nie zmieniaj i nie usuwaj - to nagłówek odpowiedzi HTTP OK // ..., // potrzebne zmienne // ... // ); // // // Zwraca aktualny offset BufferFiller // return bfill.position(); // } word webPage(); |
Natomiast definicja powyższej metody jest realizowana w sketch-u, po to by użytkownik opisywanego tutaj wrap-a nie musiał grzebać w kodzie nagłówka.
Cały plik nagłówkowy “EtherDevice.h” prezentuje się następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
/* * Autor: Cezary Wernik * Szczecin 2018 Polska */ #ifndef EtherDevice_h #define EtherDevice_h #include <EtherCard.h> byte Ethernet::buffer[500]; // tcp/ip buffer BufferFiller bfill; const char HTTP200[] PROGMEM = "HTTP/1.1 200 OK\r\n" "Server: Arduino Ethernet Devices\r\n" "Cache-Control: no-store\r\n" "Connection: Keep-Alive\r\n" "Content-Type: text/html; charset=utf-8\r\n\r\n"; const char HTTP401[] PROGMEM = "HTTP/1.0 401 Unauthorized\r\n" "Content-Type: text/html\r\n\r\n" "<h1>401 Unauthorized</h1>"; const char HTTP404[] PROGMEM = "HTTP/1.0 404 Not Found\r\n" "Content-Type: text/html\r\n\r\n" "<h1>404 Not Found</h1>"; const char HTTP405[] PROGMEM = "HTTP/1.0 405 Method Not Allowed\r\n" "Content-Type: text/html\r\n\r\n" "<h1>405 Method Not Allowed</h1>"; const char HTTP406[] PROGMEM = "HTTP/1.0 406 Not Acceptable\r\n" "Content-Type: text/html\r\n\r\n" "<h1>406 Not Acceptable</h1>"; // Struktura przechowywania argumentów żądania struct arg { String argName; String argValue; }; class EtherDevice : public EtherCard { public: //DHCP IP init // mac - MAC adres urządzenia // slaveSelect - określa pin dla chip select układu gdy płyta Arduino inna niż UNO int init(byte mac[], int slaveSelect) { if (begin(sizeof Ethernet::buffer, mac, slaveSelect) == 0) return 1;//Failed to access Ethernet controller if (!dhcpSetup()) return 2;//DHCP failed return 0; } //STATIC IP init // mac - MAC adres urządzenia // myip - adres IP, wymaga dodania w sketch-u: // static byte myip[] = { 192,168,1,200 }; // gateway - adres bramy domyślnej, wymaga dodania w sketch-u: // static byte gwip[] = { 192,168,1,1 }; // slaveSelect - określa pin dla chip select układu gdy płyta Arduino inna niż UNO int init(byte mac[], byte ip[], byte gateway[], int slaveSelect) { if (begin(sizeof Ethernet::buffer, mac, slaveSelect) == 0) return 1;//Failed to access Ethernet controller staticSetup(ip, gateway); return 0; } // Wypisuje poprzez Serial konfigurację IP urządzenia void ipconfig() { printIp("IP: ", ether.myip); printIp("GW: ", ether.gwip); printIp("DNS: ", ether.dnsip); } // Wysyła statycznie zdefiniowaną odpowiedź na żądanie HTTP // mem - stały zdefiniowany łańcuch void staticHttpServerReply(const char mem[]) { bfill = tcpOffset(); bfill.emit_p(mem); httpServerReply(bfill.position()); } // Wypisuje w Serial wszystkie argumenty przychodzące // argc - aktualna liczba argumentów // argv wskaźnik tablicy struktur argumentów void printArg(int argc, arg argv[]) { for(int i=0;i<argc;i++) { Serial.print("["); Serial.print(argv[i].argName); Serial.print(" = "); Serial.print(argv[i].argValue); Serial.println("]"); } } // Zwraca wartość argumentu o nazwie argName jeżeli istnieje // argc - aktualna liczba argumentów // argv wskaźnik tablicy struktur argumentów String getArg(int argc, arg argv[], String argName) { argName.trim(); for(int i=0;i<argc;i++) { argv[i].argName.trim(); if(argv[i].argName.compareTo(argName)==0) { argv[i].argValue.trim(); return argv[i].argValue; } } return ""; } // Sprawdza czy argument o podanej nazwie argName zawiera argValue // argc - aktualna liczba argumentów // argv wskaźnik tablicy struktur argumentów int checkArg(int argc, arg argv[], String argName, String argValue) { argName.trim(); argValue.trim(); if(getArg(argc, argv, argName).compareTo(argValue)==0) { return 0; } return 1; } // Zwraca liczbę argumentów przychodzących w żądaniu HTTP // i zapisuje argumenty pod wskaźnikiem argv // argc - dopuszczalna liczba argumentów // argv wskaźnik tablicy struktur argumentów int getTask(int argc, arg argv[]) { word offset = packetLoop(packetReceive()); if ( offset ) { char *request = (char *) Ethernet::buffer + offset; if (strncmp("GET /", request, 5) != 0) { // 405 Unsupported HTTP request staticHttpServerReply(HTTP405); return -1; } else { request += 5; if (request[0] == ' ') { //Jeżeli nie ma żadnych argumentów for(int idx=0; idx<argc; idx++) { argv[idx].argName = ""; argv[idx].argValue = ""; } return 0; } else { if (!strncmp("?",request,1)) { //Wycinanie rządania od znaku "?" + 1, żeby tego znaku "?" nie było w ciągu, //do znaku " " o indeksie endIdx. int endIdx = strcspn (request," "); request += strcspn (request,"?") + 1; int idx = 0; int args_valid = 0; int args_cnt = 0; for( idx=0; idx<endIdx; idx++ ) { if(request[idx]=='&') args_cnt++; if(request[idx]=='=') args_valid++; } if( (args_valid == (args_cnt+1)) && (args_valid<=argc) ) { for( idx=0; idx<argc; idx++) { argv[idx].argName = ""; argv[idx].argValue = ""; } idx=0; bool tictac=0; for( int c=0; c<endIdx; c++ ) { if( tictac == 0) { if( request[c] == '=') { tictac=!tictac; }else{ argv[idx].argName += request[c]; } }else{ if( request[c] == '&') { tictac=!tictac; idx++; }else{ argv[idx].argValue += request[c]; } } } return args_cnt+1; }else{ // 406 Not Acceptable staticHttpServerReply(HTTP406); return -3; } } else { // 404 Page not found staticHttpServerReply(HTTP404); return -2; } } } } else { return -1; } } // Służy wygenerowaniu odpowiedzi na żądanie HTTP // Definicja musi być w pliku .ino (w sketch-u) jako: // word EtherDevice::webPage() // { // // Ustawia offset w BufferFiller // bfill = tcpOffset(); // // // Wewnątrz PSTR(...) możesz używać następujących typów: // // $D = word data type // // $L = long data type // // $S = c string // // $F = progmem string // // $E = byte from the eeprom // bfill.emit_p( // PSTR( // "$F"// Tego $F nie zmieniaj i nie usuwaj - to nagłówek odpowiedzi HTTP OK // " jakiś tekst html $D jakiś tekst html " // " jakiś tekst html $D jakiś tekst html" // "..." // ... // ), // HTTP200,// Tego HTTP200 nie zmieniaj i nie usuwaj - to nagłówek odpowiedzi HTTP OK // ..., // potrzebne zmienne // ... // ); // // // Zwraca aktualny offset BufferFiller // return bfill.position(); // } word webPage(); }; extern EtherDevice eth; #endif |
2. Przykładowe użycie
Poniżej przedstawiony zostanie schemat połączeń oraz sketch z komentarzami i bez, aby ukazać zaletę wrap-a, jaką jest przejrzystość sketcha przy nieco rozbudowanej funkcjonalności.
Założenia do przykładu użycia:
- W przykładzie przyjęte jest założenie, że ktoś kto nie zna “tajnego klucza”, nie może ani odczytać nic z Arduino za pomocą protokołu HTTP, ani nic zmienić w jego ustawieniach.
- Jako przykład wyobraźmy sobie wentylator sterowny PWM, wraz z termometrem podpiętym do pinu A0 Arduino – taka “wentylacja IoT”.
- W kodzie temperaturę reprezentuje zmienna “int temperatura”, obroty wiatraka zmienna “int pwm_wentylatora”.
- Chcielibyśmy po wpisaniu w przeglądarce IP urządzenia i odpowiedniego klucza zobaczyć aktualną temperaturę (0-100 stopni) i obecne obroty wiatraka (0-255) podane w formacie “temperatur|pwm_wentylatora”.
- Wpisanie w przeglądarce samego adresu IP powinno zwrócić komunikat 401 Unauthorized.
- Dopiero wpisanie adresu IP opatrzonego w secretKey
np. http://192.168.0.10/?secretKey=7rHvCGd59C
wyświetli stan temperatury odczytanej z pinu analogowego A0 i aktualnie ustawione PWM wentylatora. - Natomiast dopisanie do adresu cmd=changepwm i pwm=255 ustawi zmienną pwm_wentylatora na 255.
http://192.168.0.10/?secretKey=7rHvCGd59C&cmd=changepwm&pwm=255 - Wyzerowanie pwm_wentylatora mogłoby być poprzez dopisanie cmd=resetpwm do adresu IP
np. http://192.168.0.10/?secretKey=7rHvCGd59C&cmd=resetpwm
Wybrany sposób łączności z urządzeniem jest ze względu na łatwość pobrania stanu urządzenia i jedno czesne sterowanie z innego urządzenia (np. NanoPi) za pomocą linuksowego polecenia wget. Z myślą o Arduino jako element większego systemu IoT.
2.1. Sposób połączenia modułu ethernet i Arduino
Schemat połączeń jest taki sam jak oryginalnie w bibliotece EtherCard https://github.com/njh/EtherCard
Dla Arduino UNO:
ENC28J60 | Arduino Uno | Adnotacje |
---|---|---|
VCC | 3.3V | |
GND | GND | |
SCK | Pin 13 | |
MISO | Pin 12 | |
MOSI | Pin 11 | |
CS | Pin 10 | Modyfikowalny przez eth.init() |
ENC28J60 | Arduino Mega | Adnotacje |
---|---|---|
VCC | 3.3V | |
GND | GND | |
SCK | Pin 52 | |
MISO | Pin 50 | |
MOSI | Pin 51 | |
CS | Pin 53 | Modyfikowalny przez eth.init() |
Odpowiednio zgodnie do przykładu użycia, do pinu A0 czujnik temperatury np. analogowy czujnik temperatury LM35 i do wybranego pinu PWM sterowanie wentylatora.
2.2. Sketch z komentarzami
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
/* * Autor: Cezary Wernik * Szczecin 2018 Polska */ // Dołączenie wrap-a biblioteki EtherCard #include "EtherDevice.h" // Adres MAC urządzenia, // w tym miejscu wstaw unikalny adres. static byte mac[] = {0x74, 0x69, 0x69, 0x2D, 0x30, 0x31 }; // Tajny klucz do odczytu "telemetrii" i wykonywania akcji, // w tym miejscu wstaw tajny klucz. String secretKey = "7rHvCGd59C"; // Deklaracja ilu maksymalnie argumentów urządzenie powinno się spodziewać. // Przykład żądania HTTP: // http: //192.168.0.10/?arg_1=val_1&arg_2=val_2& ... &arg_C=val_C // W tym miejscu wpisz ile wynosi spodziewane C. static const int argc=3; // Argumentów wejściowych może przyjść c <= C; // Obsługa tego przykładu: // // Wpisanie w przeglądarce samego adresu IP powinno zwrócić // komunikat 401 Unauthorized. // // Dopiero wpisanie adres IP opatrzonego w secretKey // http ://192.168.0.10/?secretKey=7rHvCGd59C // wyświetli stan temperatury odczytanej z pinu analogowego A0 i aktualnie ustawione PWM wentylatora. // // Natomiast dopisanie do adresu cmd=changepwm i pwm=255 ustawi zmienną pwm_wentylatora na 255. // http ://192.168.0.10/?secretKey=7rHvCGd59C&cmd=changepwm&pwm=255 // // Można też wyzerować pwm_wentylatora dopisując cmd=resetpwm // http ://192.168.0.10/?secretKey=7rHvCGd59C&cmd=resetpwm // wyzeruje to zmienną pwm_wentylatora. // Deklaracja przestrzeni na wejściowe argumenty. arg argv[argc]; // Struktura argumentów wygląda następująco: // struct arg // { // String argName; // String argValue; // }; // Zmienne do obsługi czujnika. int temperatura = 0; int pwm_wentylatora = 0; void setup() { Serial.begin( 57600 ); Serial.println( "Arduino ethernet device" ); // Dopóki urządzenie ethernet nie zainicjuje się po DHCP, // wstrzymaj dalsze wykonywanie programu. while( eth.init( mac, SS ) != 0 ); // Wyświetl w konsoli konfigurację IP uzyskaną z DHCP. eth.ipconfig(); } void loop() { // Pseudo odczyt temperatury. temperatura = map(analogRead(A0),0,1023,0,100); // Obsługa wentylatora w/g zmiennej pwm_wentylatora /* ... tu wyobraź sobie kod obsługi wentylatora :) ... */ // Sprawdzenie przychodzących żądań, // jeżeli są to zwraca ilość argumentów wejściowych. int c = eth.getTask(argc, argv); if(c>=0) { // Funkcja w warunku sprawdza czy w argumentach wejściowych // jest argument o nazwie "secretKey", jeżeli jest to sprawdza czy // zgodny ze zmienną secretKey zdefiniowaną na początku sketch-a. if(eth.checkArg(c, argv, "secretKey", secretKey)==0) { // Opcjonalnie wypisuje w Serial argumenty wejściowe. eth.printArg(c, argv); // Metoda checkArg sprawdza czy argument o podanej nazwie "cmd" zawiera "changepwm" if(eth.checkArg(c, argv, "cmd", "changepwm")==0) { // Funkcja getArg zwraca wartość argumentu wejściowego o podanej nazwie "pwm". String pwm = eth.getArg(c, argv, "pwm"); if(pwm.length()>0)//Sprawdzam czy zwrócona wartość argumentu jest niepusta { //Sprawdzam czy rzutowana do int-a wartość argumentu jest w zakresie 0-255. if(pwm.toInt()>=0 && pwm.toInt()<=255) pwm_wentylatora = pwm.toInt(); //Modyfikuję PWM } } if(eth.checkArg(c, argv, "cmd", "resetpwm")==0) pwm_wentylatora = 0; // Ta funkcja generuje dynamiczną odpowiedź HTTP, // zgodną z definicją funkcji webPage() umieszczoną na końcu pliku. eth.httpServerReply(eth.webPage()); }else{ // Ta funkcja generuje statyczną odpowiedź HTTP - 401 Unauthorized // gdy "secretKey" jest niezgodne lub gdy go nie ma. eth.staticHttpServerReply(HTTP401); } } } // Funkcja webPage służy do zdefiniowania bufora, // który zostanie przesłany jako odpowiedź żądania HTTP. // Modyfikuj tylko bfill.emit_p(...). // Definicja tej funkcji powinna znaleźć się na końcu tego pliku, // ponieważ musi mieć zasięg do deklaracji wszystkich używanych zmiennych globalnych. word EtherDevice::webPage() { // Ustawia offset w BufferFiller bfill = tcpOffset(); // Wewnątrz PSTR(...) możesz używać następujących typów: // $D = word data type // $L = long data type // $S = c string // $F = progmem string // $E = byte from the eeprom bfill.emit_p( PSTR( "$F"// Tego $F nie zmieniaj i nie usuwaj - to nagłówek odpowiedzi HTTP OK "$D|" "$D" ), HTTP200,// Tego HTTP200 nie zmieniaj i nie usuwaj - to nagłówek odpowiedzi HTTP OK temperatura, pwm_wentylatora ); // Zwraca aktualny offset BufferFiller return bfill.position(); } |
2.3. Sketch bez komentarzy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
/* * Autor: Cezary Wernik * Szczecin 2018 Polska */ #include "EtherDevice.h" static byte mac[] = {0x74, 0x69, 0x69, 0x2D, 0x30, 0x31 }; String secretKey = "7rHvCGd59C"; static const int argc=3; arg argv[argc]; // Zmienne do obsługi czujnika. int temperatura = 0; int pwm_wentylatora = 0; void setup() { Serial.begin( 57600 ); Serial.println( "Arduino ethernet device" ); while( eth.init( mac, SS ) != 0 ); eth.ipconfig(); } void loop() { // Pseudo odczyt temperatury. temperatura = map(analogRead(A0),0,1023,0,100); // Obsługa wentylatora w/g zmiennej pwm_wentylatora /* ... tu wyobraź sobie kod obsługi wentylatora :) ... */ int c = eth.getTask(argc, argv); if(c>=0) { if(eth.checkArg(c, argv, "secretKey", secretKey)==0) { if(eth.checkArg(c, argv, "cmd", "changepwm")==0) { String pwm = eth.getArg(c, argv, "pwm"); if(pwm.length()>0) { if(pwm.toInt()>=0 && pwm.toInt()<=255) pwm_wentylatora = pwm.toInt(); } } if(eth.checkArg(c, argv, "cmd", "resetpwm")==0) pwm_wentylatora = 0; eth.httpServerReply(eth.webPage()); }else{ eth.staticHttpServerReply(HTTP401); } } } word EtherDevice::webPage() { bfill = tcpOffset(); bfill.emit_p( PSTR( "$F" "$D|" "$D" ), HTTP200, temperatura, pwm_wentylatora ); return bfill.position(); } |
Jak widać sketch bez komentarzy jest już bardzo czytelny a zawiera logikę główną i obsługę ethernet.
3. Zalety/Wady
Do zalet należy:
- ułatwiona obsługa modułu ethernet do poziomu “instant”;
- przejrzystość kodu, w sketch-u można się skupić na obsłudze zadań/czujników;
- gotowe parsowanie argumentów przychodzących w żądaniu;
- kilka gotowych odpowiedzi (HTTP 401, 404, 405, 406) na błędnie wpisany w pasku adresu IP z argumentami.
Do wad należy:
- zabezpieczenie secretKey jest na laików bo i tak wszystko po http leci plain-text, więc ktoś z Wireshark-iem od razu znajdzie klucz, ale jest to możliwe do załatania/usprawnienia;
- jak na programowanie mikrokontrolera trochę za bardzo obiektowo;
- z wielu metod HTTP obsłużone tylko część GET https://pl.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Metody_HTTP;
- waga sketch-a:
- podobne funkcjonalnie sketch-e z przykładów EtherCard zajmują średnio (na dzień dobry) około 30% pamięci programu i około 40% pamięci dynamicznej;
- sketch po owrapowaniu już jako EtherDevice zajmuje 38% pamięci programu i 48% pamięci dynamicznej.
4. Załączniki
Do wpisu załączam paczuszkę z sketch-em (.ino) i plikiem nagłówkowym (.h).
Bardzo dokładny i ładny opis. Ocena 5*.
Dziękuję :)
Mimo małego projekciku, starałem się go dobrze opisać, aby każdy mógł zrozumieć, użycie moich dopisanych metod do biblioteki EtherCard.
Sam zrobiłem kilka projektów z ENC28J60 i mimo że działają już ponad dwa lata – to nie jest to najlepszy wybór – są ciągłe problemy ze stabilnością modułu. Ostatnio wypróbowałem USR-ES1 na chipie W5500 i bibliotekę Ethernet3 https://github.com/sstaub/Ethernet3 – jest to dużo lepsze rozwiązanie niż ENC28J60.
Witam,
jak osiągnąć “narastanie” / “opadanie” PWM na sztyno o okresie np 1 sek ? lub – co było by najmilsze, jako parametr, coś na wzór z kit AVT1825 (niestety to nie jest superkonstrukcja, wiesza się)