├── HTTPserver.cpp ├── HTTPserver.h ├── README.md └── examples ├── Display_received_parameters └── Display_received_parameters.ino ├── Process_Cookie_information └── Process_Cookie_information.ino ├── Process_GET_arguments └── Process_GET_arguments.ino └── Process_POST_arguments └── Process_POST_arguments.ino /HTTPserver.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Arduino tiny web server. 4 | 5 | Copyright 2015 Nick Gammon. 6 | 7 | Version: 1.3 8 | 9 | Change history 10 | -------------- 11 | 1.1 - Fixed header values to not be percent-encoded, fixed cookie issues. 12 | Also various bugfixes. 13 | 1.2 - Added buffering of writes. 14 | 1.3 - Removed trailing space from header and cookie values 15 | 16 | 17 | http://www.gammon.com.au/forum/?id=12942 18 | 19 | PERMISSION TO DISTRIBUTE 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software 22 | and associated documentation files (the "Software"), to deal in the Software without restriction, 23 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 24 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 25 | subject to the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be included in 28 | all copies or substantial portions of the Software. 29 | 30 | 31 | LIMITATION OF LIABILITY 32 | 33 | The software is provided "as is", without warranty of any kind, express or implied, 34 | including but not limited to the warranties of merchantability, fitness for a particular 35 | purpose and noninfringement. In no event shall the authors or copyright holders be liable 36 | for any claim, damages or other liability, whether in an action of contract, 37 | tort or otherwise, arising from, out of or in connection with the software 38 | or the use or other dealings in the software. 39 | 40 | */ 41 | 42 | #include 43 | #include 44 | 45 | // --------------------------------------------------------------------------- 46 | // clear the key/value buffers ready for a new key/value 47 | // --------------------------------------------------------------------------- 48 | void HTTPserver::clearBuffers () 49 | { 50 | keyBuffer [0] = 0; 51 | valueBuffer [0] = 0; 52 | keyBufferPos = 0; 53 | valueBufferPos = 0; 54 | bodyBufferPos = 0; 55 | encodePhase = ENCODE_NONE; 56 | flags = FLAG_NONE; 57 | } // end of HTTPserver::clearBuffers 58 | 59 | // --------------------------------------------------------------------------- 60 | // switch states (could add state-change debugging in here) 61 | // --------------------------------------------------------------------------- 62 | void HTTPserver::newState (StateType what) 63 | { 64 | state = what; 65 | } // end of HTTPserver::newState 66 | 67 | // --------------------------------------------------------------------------- 68 | // add a character to the key buffer - straight text 69 | // --------------------------------------------------------------------------- 70 | void HTTPserver::addToKeyBuffer (const byte inByte) 71 | { 72 | if (keyBufferPos >= MAX_KEY_LENGTH) 73 | { 74 | flags |= FLAG_KEY_BUFFER_OVERFLOW; 75 | return; 76 | } // end of overflow 77 | keyBuffer [keyBufferPos++] = inByte; 78 | keyBuffer [keyBufferPos] = 0; // trailing null-terminator 79 | } // end of HTTPserver::addToKeyBuffer 80 | 81 | // --------------------------------------------------------------------------- 82 | // add a character to the value buffer - percent-encoded (if wanted) 83 | // --------------------------------------------------------------------------- 84 | void HTTPserver::addToValueBuffer (byte inByte, const bool percentEncoded) 85 | { 86 | if (valueBufferPos >= MAX_VALUE_LENGTH) 87 | { 88 | flags |= FLAG_VALUE_BUFFER_OVERFLOW; 89 | return; 90 | } // end of overflow 91 | 92 | // look for stuff like "foo+bar" (turn the "+" into a space) 93 | // and also "foo%21bar" (turn %21 into one character) 94 | if (percentEncoded) 95 | { 96 | switch (encodePhase) 97 | { 98 | 99 | // if in "normal" mode, turn a "+" into a space, and look for "%" 100 | case ENCODE_NONE: 101 | if (inByte == '+') 102 | inByte = ' '; 103 | else if (inByte == '%') 104 | { 105 | encodePhase = ENCODE_GOT_PERCENT; 106 | return; // no addition to buffer yet 107 | } 108 | break; 109 | 110 | // we had the "%" last time, this should be the first hex digit 111 | case ENCODE_GOT_PERCENT: 112 | if (isxdigit (inByte)) 113 | { 114 | byte c = toupper (inByte) - '0'; 115 | if (c > 9) 116 | c -= 7; // Fix A-F 117 | encodeByte = c << 4; 118 | encodePhase = ENCODE_GOT_FIRST_CHAR; 119 | return; // no addition to buffer yet 120 | } 121 | // not a hex digit, give up 122 | encodePhase = ENCODE_NONE; 123 | flags |= FLAG_ENCODING_ERROR; 124 | break; 125 | 126 | // this should be the second hex digit 127 | case ENCODE_GOT_FIRST_CHAR: 128 | if (isxdigit (inByte)) 129 | { 130 | byte c = toupper (inByte) - '0'; 131 | if (c > 9) 132 | c -= 7; // Fix A-F 133 | inByte = encodeByte | c; 134 | } 135 | else 136 | flags |= FLAG_ENCODING_ERROR; 137 | 138 | // done with encoding it, or not a hex digit 139 | encodePhase = ENCODE_NONE; 140 | 141 | } // end of switch on encodePhase 142 | } // end of percent-encoded 143 | 144 | // add to value buffer, encoding has been dealt with 145 | valueBuffer [valueBufferPos++] = inByte; 146 | valueBuffer [valueBufferPos] = 0; // trailing null-terminator 147 | } // end of HTTPserver::addToValueBuffer 148 | 149 | // --------------------------------------------------------------------------- 150 | // add a character to the body buffer - raw binary 151 | // --------------------------------------------------------------------------- 152 | void HTTPserver::addToBodyBuffer (const byte inByte) 153 | { 154 | if (bodyBufferPos >= BODY_CHUNK_LENGTH) 155 | { 156 | // pass current chunk to the application and empty it 157 | processBodyChunk (bodyBuffer, bodyBufferPos, flags); 158 | bodyBufferPos = 0; 159 | } // end of overflow 160 | bodyBuffer [bodyBufferPos++] = inByte; 161 | } // end of HTTPserver::addToBodyBuffer 162 | 163 | // --------------------------------------------------------------------------- 164 | // handleSpace - we have an incoming space 165 | // --------------------------------------------------------------------------- 166 | 167 | // in the state machine handlers the symbols { } indicate where we think we are 168 | void HTTPserver::handleSpace () 169 | { 170 | switch (state) 171 | { 172 | 173 | // GET{ }/pathname/filename?foo=bar&fubar=true HTTP/1.1 174 | case SKIP_GET_SPACES_1: 175 | // GET /pathname/filename?foo=bar&fubar=true{ }HTTP/1.1 176 | case SKIP_GET_SPACES_2: 177 | // GET /pathname/filename?foo=bar&fubar=true HTTP/1.1{ } 178 | case SKIP_TO_END_OF_LINE: 179 | // Cookie:{ }foo=bar; 180 | case SKIP_COOKIE_SPACES: 181 | break; // ignore these spaces 182 | 183 | // GET{ }/pathname/filename?foo=bar&fubar=true HTTP/1.1 184 | case GET_LINE: 185 | processPostType (keyBuffer, flags); 186 | // see if it is a POST type 187 | postRequest = strcmp (keyBuffer, "POST") == 0; 188 | newState (SKIP_GET_SPACES_1); 189 | clearBuffers (); 190 | break; 191 | 192 | // GET /pathname/filename?foo=bar&fubar=true{ }HTTP/1.1 193 | case GET_PATHNAME: 194 | processPathname (valueBuffer, flags); 195 | newState (SKIP_GET_SPACES_2); 196 | clearBuffers (); 197 | break; 198 | 199 | // GET /pathname/filename?foo=bar&fubar=true{ }HTTP/1.1 200 | case GET_ARGUMENT_NAME: 201 | processGetArgument (keyBuffer, valueBuffer, flags); 202 | newState (SKIP_GET_SPACES_2); 203 | clearBuffers (); 204 | break; 205 | 206 | // GET /pathname/filename?foo{ }HTTP/1.1 207 | case GET_ARGUMENT_VALUE: 208 | processGetArgument (keyBuffer, valueBuffer, flags); 209 | newState (SKIP_GET_SPACES_2); 210 | clearBuffers (); 211 | break; 212 | 213 | // GET /pathname/filename?foo=bar&fubar=true HTTP/1.1{ } 214 | case GET_HTTP_VERSION: 215 | processHttpVersion (keyBuffer, flags); 216 | newState (SKIP_TO_END_OF_LINE); 217 | clearBuffers (); 218 | break; 219 | 220 | // Accept-Encoding: gzip,{ }deflat 221 | case HEADER_VALUE: 222 | case COOKIE_VALUE: 223 | addToValueBuffer (' ', false); 224 | break; 225 | 226 | // Accept-Encoding{ }: 227 | // space shouldn't be there, but we'll ignore it 228 | case HEADER_NAME: 229 | break; 230 | 231 | default: 232 | break; // do nothing 233 | 234 | } // end of switch on state 235 | 236 | } // end of HTTPserver::handleSpace 237 | 238 | // --------------------------------------------------------------------------- 239 | // handleNewline - we have an incoming newline 240 | // --------------------------------------------------------------------------- 241 | void HTTPserver::handleNewline () 242 | { 243 | 244 | // pretend there was a trailing space and wrap up the previous line 245 | if (state != SKIP_TO_END_OF_LINE && 246 | state != SKIP_INITIAL_LINES && 247 | state != HEADER_VALUE && // don't have trailing space on header value 248 | state != COOKIE_VALUE) // nor on cookie value 249 | handleSpace (); 250 | 251 | switch (state) 252 | { 253 | // default is the start of a new header line 254 | default: 255 | clearBuffers (); 256 | newState (START_LINE); 257 | break; 258 | 259 | // ignore blank lines before the GET/POST line 260 | case SKIP_INITIAL_LINES: 261 | break; 262 | 263 | // a blank line on its own signals switching to the POST key/values or binary body 264 | case START_LINE: 265 | clearBuffers (); 266 | newState (binaryBody ? BODY : POST_NAME); 267 | break; 268 | 269 | // wrap up this POST key/value and start a new one 270 | case POST_NAME: 271 | case POST_VALUE: 272 | if (keyBufferPos > 0) 273 | processPostArgument (keyBuffer, valueBuffer, flags); 274 | newState (POST_NAME); 275 | clearBuffers (); 276 | break; 277 | 278 | // end of a header value, start looking for a new header 279 | case HEADER_VALUE: 280 | processHeaderArgument (keyBuffer, valueBuffer, flags); 281 | // remember the content length for the POST data 282 | if (strcasecmp (keyBuffer, "Content-Length") == 0) 283 | contentLength = atol (valueBuffer); 284 | if (strcasecmp (keyBuffer, "Content-Type") == 0 && strcasecmp (valueBuffer, "application/octet-stream") == 0) 285 | binaryBody = true; 286 | clearBuffers (); 287 | newState (START_LINE); 288 | break; 289 | 290 | case COOKIE_VALUE: 291 | processCookie (keyBuffer, valueBuffer, flags); 292 | newState (START_LINE); 293 | clearBuffers (); 294 | break; 295 | 296 | } // end of switch on state 297 | 298 | } // end of HTTPserver::handleNewline 299 | 300 | // --------------------------------------------------------------------------- 301 | // handleText - we have an incoming character other than a space or newline 302 | // --------------------------------------------------------------------------- 303 | 304 | // in the state machine handlers the symbols { } indicate where we think we are 305 | void HTTPserver::handleText (const byte inByte) 306 | { 307 | switch (state) 308 | { 309 | // blank lines before GET line 310 | case SKIP_INITIAL_LINES: 311 | newState (GET_LINE); 312 | addToKeyBuffer (inByte); 313 | break; 314 | 315 | // {GET} /whatever/foo.htm HTTP/1.1 316 | case GET_LINE: 317 | // GET /whatever/foo.htm {HTTP/1.1} 318 | case GET_HTTP_VERSION: 319 | addToKeyBuffer (inByte); 320 | break; 321 | 322 | // {Connection}: keep-alive 323 | case HEADER_NAME: 324 | if (inByte == ':') 325 | { 326 | if (strcasecmp (keyBuffer, "Cookie") == 0) 327 | { 328 | newState (SKIP_COOKIE_SPACES); 329 | clearBuffers (); 330 | } 331 | else 332 | newState (SKIP_HEADER_SPACES); 333 | } 334 | else 335 | addToKeyBuffer (inByte); 336 | break; 337 | 338 | // Connection: {k}eep-alive 339 | case SKIP_HEADER_SPACES: 340 | newState (HEADER_VALUE); 341 | addToValueBuffer (inByte, false); 342 | break; 343 | 344 | // Connection: {keep-alive} 345 | case HEADER_VALUE: 346 | addToValueBuffer (inByte, false); 347 | break; 348 | 349 | // Cookie: foo=bar;{ }whatever=something; 350 | case SKIP_COOKIE_SPACES: 351 | newState (COOKIE_NAME); 352 | addToKeyBuffer (inByte); 353 | break; 354 | 355 | // Cookie: {foo}=bar; 356 | case COOKIE_NAME: 357 | if (inByte == '=') 358 | newState (COOKIE_VALUE); 359 | else 360 | addToKeyBuffer (inByte); 361 | break; 362 | 363 | // Cookie: foo={bar}; 364 | case COOKIE_VALUE: 365 | if (inByte == ';' || inByte == ',') 366 | { 367 | processCookie (keyBuffer, valueBuffer, flags); 368 | newState (SKIP_COOKIE_SPACES); 369 | clearBuffers (); 370 | } 371 | else 372 | addToValueBuffer (inByte, false); 373 | break; 374 | 375 | // {foo}=bar&answer=42 376 | case POST_NAME: 377 | if (inByte == '&') 378 | { 379 | processPostArgument (keyBuffer, valueBuffer, flags); 380 | newState (POST_NAME); 381 | clearBuffers (); 382 | } 383 | else if (inByte == '=') 384 | newState (POST_VALUE); 385 | else 386 | addToKeyBuffer (inByte); 387 | break; 388 | 389 | // foo={bar}&answer=42 390 | case POST_VALUE: 391 | if (inByte == '&') 392 | { 393 | processPostArgument (keyBuffer, valueBuffer, flags); 394 | newState (POST_NAME); 395 | clearBuffers (); 396 | } 397 | else 398 | addToValueBuffer (inByte, true); 399 | break; 400 | 401 | // GET {/whatever/foo.htm} HTTP/1.1 402 | case SKIP_GET_SPACES_1: 403 | newState (GET_PATHNAME); 404 | addToValueBuffer (inByte, true); 405 | break; 406 | 407 | // GET /pathname/filename?{foo}=bar&fubar=true 408 | case GET_ARGUMENT_NAME: 409 | if (inByte == '&') 410 | { 411 | processGetArgument (keyBuffer, valueBuffer, flags); 412 | newState (GET_ARGUMENT_NAME); 413 | clearBuffers (); 414 | } 415 | else if (inByte == '=') 416 | newState (GET_ARGUMENT_VALUE); 417 | else 418 | addToKeyBuffer (inByte); 419 | break; 420 | 421 | // GET /pathname/filename?foo={bar}&fubar=true 422 | case GET_ARGUMENT_VALUE: 423 | if (inByte == '&') 424 | { 425 | processGetArgument (keyBuffer, valueBuffer, flags); 426 | newState (GET_ARGUMENT_NAME); 427 | clearBuffers (); 428 | } 429 | else 430 | addToValueBuffer (inByte, true); 431 | break; 432 | 433 | // GET {/pathname/filename}?foo=bar&fubar=true 434 | case GET_PATHNAME: 435 | if (inByte == '?') 436 | { 437 | processPathname (valueBuffer, flags); 438 | newState (GET_ARGUMENT_NAME); 439 | clearBuffers (); 440 | } 441 | else 442 | addToValueBuffer (inByte, true); 443 | break; 444 | 445 | // GET /whatever/foo.htm {HTTP/1.1} 446 | case SKIP_GET_SPACES_2: 447 | newState (GET_HTTP_VERSION); 448 | addToKeyBuffer (inByte); 449 | break; 450 | 451 | // {C}onnection: keep-alive 452 | case START_LINE: 453 | newState (HEADER_NAME); 454 | addToKeyBuffer (inByte); 455 | break; 456 | 457 | // we think line is done, skip whatever we find 458 | case SKIP_TO_END_OF_LINE: 459 | break; // ignore it 460 | } // end of switch on state 461 | 462 | } // end of HTTPserver::handleText 463 | 464 | // --------------------------------------------------------------------------- 465 | // processIncomingByte - our main sketch has received a byte from the client 466 | // --------------------------------------------------------------------------- 467 | void HTTPserver::processIncomingByte (const byte inByte) 468 | { 469 | 470 | // count received bytes in POST section or binary body 471 | if (state == POST_NAME || state == POST_VALUE || state == BODY) 472 | receivedLength++; 473 | 474 | if (state == BODY) 475 | { 476 | addToBodyBuffer (inByte); 477 | 478 | // if all received, stop now 479 | if (receivedLength >= contentLength) 480 | { 481 | // wrap up last partial binary chunk (always at least 1 byte here by definition) 482 | processBodyChunk (bodyBuffer, bodyBufferPos, flags); 483 | clearBuffers (); 484 | done = true; 485 | } 486 | 487 | // don't process data specially inside a binary body 488 | return; 489 | } 490 | 491 | switch (inByte) 492 | { 493 | case '\r': 494 | break; // ignore carriage-return 495 | 496 | case ' ': 497 | case '\t': 498 | handleSpace (); // generally switches states 499 | break; 500 | 501 | case '\n': 502 | handleNewline (); // generally switches states 503 | break; 504 | 505 | default: 506 | handleText (inByte); // collect text 507 | break; 508 | } // end of switch on inByte 509 | 510 | // see if count of content bytes is up 511 | if (state == POST_NAME || state == POST_VALUE) 512 | { 513 | // if all received, stop now 514 | if (receivedLength >= contentLength) 515 | { 516 | // handle final POST item 517 | if (keyBufferPos > 0) 518 | handleNewline (); 519 | done = true; 520 | return; 521 | } // end of Content-Length reached 522 | 523 | // not a POST request? don't look for more data 524 | if (!postRequest) 525 | done = true; 526 | } // end of up to the POST states 527 | 528 | } // end of HTTPserver::processIncomingByte 529 | 530 | // --------------------------------------------------------------------------- 531 | // begin - reset state machine to the start 532 | // --------------------------------------------------------------------------- 533 | void HTTPserver::begin (Print * output_) 534 | { 535 | // reset everything to initial state 536 | state = SKIP_INITIAL_LINES; 537 | encodePhase = ENCODE_NONE; 538 | flags = FLAG_NONE; 539 | postRequest = false; 540 | binaryBody = false; 541 | contentLength = 0; 542 | receivedLength = 0; 543 | sendBufferPos = 0; 544 | output = output_; 545 | clearBuffers (); 546 | done = false; 547 | } // end of HTTPserver::begin 548 | 549 | // --------------------------------------------------------------------------- 550 | // write - for outputting via print, println etc. 551 | // --------------------------------------------------------------------------- 552 | size_t HTTPserver::write (uint8_t c) 553 | { 554 | // forget it, if they supplied no output device 555 | if (!output) 556 | return 0; 557 | 558 | // only buffer writes up, if a non-zero buffer length 559 | if (SEND_BUFFER_LENGTH > 0) 560 | { 561 | sendBuffer [sendBufferPos++] = c; 562 | // if full, flush it 563 | if (sendBufferPos >= SEND_BUFFER_LENGTH) 564 | flush (); 565 | } 566 | else 567 | output->write (c); // otherwise write a byte at a time 568 | 569 | return 1; 570 | } // end of HTTPserver::write 571 | 572 | void HTTPserver::flush () 573 | { 574 | if (sendBufferPos > 0) 575 | { 576 | output->write (sendBuffer, sendBufferPos); 577 | sendBufferPos = 0; 578 | } // end of anything in buffer 579 | } // end of HTTPserver::flush 580 | 581 | // --------------------------------------------------------------------------- 582 | // fixHTML - convert special characters such as < > and & 583 | // --------------------------------------------------------------------------- 584 | void HTTPserver::fixHTML (const char * message) 585 | { 586 | char c; 587 | while ((c = *message++)) 588 | { 589 | switch (c) 590 | { 591 | case '<': print ("<"); break; 592 | case '>': print (">"); break; 593 | case '&': print ("&"); break; 594 | case '"': print ("""); break; 595 | default: write (c); break; 596 | } // end of switch 597 | } // end of while 598 | } // end of HTTPserver::fixHTML 599 | 600 | // --------------------------------------------------------------------------- 601 | // urlEncode - convert special characters such as spaces into percent-encoded 602 | // --------------------------------------------------------------------------- 603 | void HTTPserver::urlEncode (const char * message) 604 | { 605 | char c; 606 | while ((c = *message++)) 607 | { 608 | if (!isalpha (c) && !isdigit(c)) 609 | { 610 | // compact conversion to hex 611 | write ('%'); 612 | char x = ((c >> 4) & 0xF) | '0'; 613 | if (x > '9') 614 | x += 7; 615 | write (x); 616 | x = (c & 0xF) | '0'; 617 | if (x > '9') 618 | x += 7; 619 | write (x); 620 | } 621 | else 622 | write (c); 623 | } // end of while 624 | } // end of HTTPserver::urlEncode 625 | 626 | // --------------------------------------------------------------------------- 627 | // setCookie - cookies only permit certain characters 628 | // --------------------------------------------------------------------------- 629 | void HTTPserver::setCookie (const char * name, const char * value, const char * extra) 630 | { 631 | print (F("Set-Cookie: ")); 632 | // send the name which excludes spaces, ';', ',' or '=' 633 | for (const char * p = name; *p; p++) 634 | if (*p >= '!' && *p <= '~' && *p != ';' && *p != ';' && *p != '=') 635 | write (*p); 636 | write ('='); 637 | // send the value which excludes ';' or ',' 638 | for (const char * p = value; *p; p++) 639 | if (*p >= ' ' && *p <= '~' && *p != ';' && *p != ';') 640 | write (*p); 641 | // terminate value with semicolon and space 642 | print (F("; ")); 643 | 644 | // extra stuff like: 645 | // Path=/accounts; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly 646 | if (extra) 647 | print (extra); 648 | // end of header line 649 | println (); 650 | 651 | } // end of HTTPserver::setCookie 652 | 653 | -------------------------------------------------------------------------------- /HTTPserver.h: -------------------------------------------------------------------------------- 1 | // HTTPserver class 2 | class HTTPserver : public Print 3 | { 4 | // derived classes can know these lengths 5 | protected: 6 | static const size_t MAX_KEY_LENGTH = 40; // maximum size for a key 7 | static const size_t MAX_VALUE_LENGTH = 100; // maximum size for a value 8 | static const size_t BODY_CHUNK_LENGTH = 16; // maximum size for a binary body chunk 9 | static const size_t SEND_BUFFER_LENGTH = 64; // how much to buffer sends 10 | 11 | private: 12 | char keyBuffer [MAX_KEY_LENGTH + 1]; // store here 13 | size_t keyBufferPos; // how much data we have collected 14 | 15 | char valueBuffer [MAX_VALUE_LENGTH + 1]; // store here 16 | size_t valueBufferPos; // how much data we have collected 17 | 18 | byte bodyBuffer [BODY_CHUNK_LENGTH]; // store here 19 | size_t bodyBufferPos; // how much data we have collected 20 | 21 | char sendBuffer [SEND_BUFFER_LENGTH]; // for buffering output 22 | size_t sendBufferPos; // how much in buffer 23 | 24 | // state machine: possible states 25 | enum StateType { 26 | SKIP_INITIAL_LINES, // skip blank lines before the GET line 27 | GET_LINE, // eg. GET /myblog/nick.html?foo=42 HTTP/1.1 (might be POST) 28 | SKIP_GET_SPACES_1, // spaces after GET 29 | GET_PATHNAME, // eg. /foo/myblog.html 30 | GET_ARGUMENT_NAME, // eg. foo 31 | GET_ARGUMENT_VALUE, // eg. 42 32 | SKIP_GET_SPACES_2, // spaces after arguments 33 | GET_HTTP_VERSION, // eg. HTTP/1.1 34 | SKIP_TO_END_OF_LINE, 35 | START_LINE, // start of another header line 36 | HEADER_NAME, // eg. Accept: 37 | SKIP_HEADER_SPACES, // spaces after header name 38 | HEADER_VALUE, // eg. text/html 39 | SKIP_COOKIE_SPACES, // spaces before cookie name 40 | COOKIE_NAME, // eg. theme 41 | COOKIE_VALUE, // eg. light 42 | POST_NAME, // eg. action 43 | POST_VALUE, // eg. add 44 | BODY, // eg. octet-stream binary blob 45 | }; 46 | // current state 47 | StateType state; 48 | 49 | // percent-encoding: possible states 50 | enum EncodePhaseType { 51 | ENCODE_NONE, 52 | ENCODE_GOT_PERCENT, 53 | ENCODE_GOT_FIRST_CHAR, 54 | }; 55 | // percent-encoding: current state 56 | EncodePhaseType encodePhase; 57 | byte encodeByte; // encoded byte being assembled (first nybble) 58 | 59 | // derived classes can inspect the flags and check if it was a POST 60 | protected: 61 | 62 | // extra information about the key/value passed to a callback function 63 | // (bitmask - more than one might be set) 64 | enum { 65 | FLAG_NONE = 0x00, // no problems 66 | FLAG_KEY_BUFFER_OVERFLOW = 0x01, // the key was truncated 67 | FLAG_VALUE_BUFFER_OVERFLOW = 0x02, // the value was truncated 68 | FLAG_ENCODING_ERROR = 0x04, // %xx encoding error 69 | }; 70 | 71 | bool postRequest; // true if a POST type 72 | 73 | private: 74 | 75 | byte flags; // see above enum 76 | 77 | unsigned long contentLength; // how long the POST data is 78 | unsigned long receivedLength; // how much POST data we currently have 79 | Print * output; // where to write output to 80 | 81 | // private methods (just used internally) 82 | 83 | // state change 84 | void newState (StateType what); 85 | // buffer handlers 86 | void addToKeyBuffer (const byte inByte); 87 | void addToValueBuffer (byte inByte, const bool percentEncoded); 88 | void addToBodyBuffer (const byte inByte); 89 | void clearBuffers (); 90 | // state handlers 91 | void handleNewline (); 92 | void handleSpace (); 93 | void handleText (const byte inByte); 94 | 95 | public: 96 | 97 | // constructor 98 | HTTPserver () { begin (NULL); } 99 | 100 | // re-initialize states 101 | void begin (Print * output_); 102 | 103 | // handle one incoming byte from the client 104 | void processIncomingByte (const byte inByte); 105 | 106 | // empty sending buffer 107 | void flush (); // for emptying send buffer 108 | 109 | // give application read access to expected/current content length 110 | unsigned long getContentLength () { return contentLength; } 111 | unsigned long getReceivedLength () { return receivedLength; } 112 | 113 | // set to stop further processing (eg. on error) 114 | bool done; 115 | 116 | // true if "Content-Type" header is "application/octet-stream" OR application can set for other relevant type(s) 117 | bool binaryBody; 118 | 119 | protected: 120 | 121 | // user handlers - override to do something with them 122 | virtual void processPostType (const char * key, const byte flags) { } 123 | virtual void processPathname (const char * key, const byte flags) { } 124 | virtual void processHttpVersion (const char * key, const byte flags) { } 125 | virtual void processGetArgument (const char * key, const char * value, const byte flags) { } 126 | virtual void processHeaderArgument (const char * key, const char * value, const byte flags) { } 127 | virtual void processCookie (const char * key, const char * value, const byte flags) { } 128 | virtual void processPostArgument (const char * key, const char * value, const byte flags) { } 129 | virtual void processBodyChunk (const byte * data, const size_t length, const byte flags) { } 130 | 131 | // for outputting back to client 132 | size_t write(uint8_t c); 133 | 134 | public: 135 | // fix up <, >, & into < > and & 136 | void fixHTML (const char * message); 137 | // fix up non alphanumeric into %-encoding 138 | void urlEncode (const char * message); 139 | // output a Set-Cookie header line 140 | void setCookie (const char * name, const char * value, const char * extra = NULL); 141 | 142 | using Print::write; 143 | }; // end of HTTPserver 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny web server for Arduino or similar 2 | 3 | This is the code and example sketches for a library which interprets incoming HTTP messages using a "state machine": 4 | 5 | * Minimal memory (RAM) requirements (about 256 bytes) 6 | * Small code size (around 2.5 KB) 7 | * No use of the String class, or dynamic memory allocation (to avoid heap fragmentation) 8 | * Incoming HTTP (client) requests decoded "on the fly" by a state machine 9 | * Doesn't care what Ethernet library you are using - you call your Ethernet library, and send a byte at at time to the HTTP library. 10 | * Compact and fast 11 | * Handles all of: 12 | 13 | * Request type (eg. POST, GET, etc.) 14 | * Path (eg. /server/foo.htm) 15 | * GET parameters (eg. /server/foo.htm?device=clock&mode=UTC) 16 | * Header values (eg. Accept-Language: en, mi) 17 | * Cookies (as sent by the web browser) 18 | * POST data (ie. the contents of forms) 19 | 20 | See: [Forum posting with description](http://www.gammon.com.au/forum/?id=12942) 21 | 22 | ## How to use 23 | 24 | Download the library from GitHub, and unzip it into your *libraries* folder inside your Arduino *sketches* folder. 25 | 26 | --- 27 | 28 | Include the library in your sketch: 29 | 30 | #include 31 | 32 | --- 33 | 34 | Derive an instance of the *HTTPserver* class with custom handlers (just the ones you want): 35 | 36 | class myServerClass : public HTTPserver 37 | { 38 | virtual void processPostType (const char * key, const byte flags); 39 | virtual void processPathname (const char * key, const byte flags); 40 | virtual void processHttpVersion (const char * key, const byte flags); 41 | virtual void processGetArgument (const char * key, const char * value, const byte flags); 42 | virtual void processHeaderArgument (const char * key, const char * value, const byte flags); 43 | virtual void processCookie (const char * key, const char * value, const byte flags); 44 | virtual void processPostArgument (const char * key, const char * value, const byte flags); 45 | }; // end of myServerClass 46 | 47 | --- 48 | 49 | Make an instance of this new class: 50 | 51 | myServerClass myServer; 52 | 53 | --- 54 | 55 | Now implement the handlers that you declared above, most have key/value arguments. For example: 56 | 57 | void myServerClass::processGetArgument (const char * key, const char * value, const byte flags) 58 | { 59 | if (strcmp (key, "light") == 0 && strcmp (value, "on") == 0) 60 | digitalWrite (light_switch, HIGH); 61 | } // end of myServerClass::processGetArgument 62 | 63 | --- 64 | 65 | When you get an incoming connection call the begin() method to reset the state machine to the start: 66 | 67 | myServer.begin (&client); 68 | 69 | 70 | In the begin() call we pass down the address of the Ethernet client, so that the derived classes can do print() and println() calls to send data back to the web client. If this isn't possible pass down NULL. 71 | 72 | --- 73 | 74 | Now while the client remains connected, call *processIncomingByte* for each byte read from the web client, this will be processed by the state machine, and the appropriate callback routine (which you supplied earlier) will be called when required. The state machine sets the *done* flag when it determines that there should be no more incoming data. 75 | 76 | while (client.connected() && !myServer.done) 77 | { 78 | while (client.available () > 0 && !myServer.done) 79 | myServer.processIncomingByte (client.read ()); 80 | 81 | // do other stuff here 82 | 83 | } // end of while client connected 84 | 85 | --- 86 | 87 | ## Output buffering 88 | 89 | Version 1.2 of this library now buffers output. This considerably speeds up writes done with the F() macro, eg. 90 | 91 | void myServerClass::processCookie (const char * key, const char * value, const byte flags) 92 | { 93 | print (F("Cookie: ")); 94 | print (key); 95 | print (F(" = ")); 96 | println (value); 97 | } // end of processCookie 98 | 99 | Previously each byte in those strings would be sent in a separate packet, now they are placed into a 64-byte buffer, and sent when the buffer fills up. When you are done sending you need to flush the final bytes like this: 100 | 101 | myServer.flush (); 102 | 103 | This buffering is only done if you write **via the derived class**, not directly to the client. For example: 104 | 105 | myServer.println(F("")); 106 | myServer.println(F("")); 107 | -------------------------------------------------------------------------------- /examples/Display_received_parameters/Display_received_parameters.ino: -------------------------------------------------------------------------------- 1 | // Tiny web server demo 2 | // Author: Nick Gammon 3 | // Date: 20 July 2015 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | // Enter a MAC address and IP address for your controller below. 10 | byte mac[] = { 0x90, 0xA2, 0xDA, 0x00, 0x2D, 0xA1 }; 11 | 12 | // The IP address will be dependent on your local network: 13 | byte ip[] = { 10, 0, 0, 241 }; 14 | 15 | // the router's gateway address: 16 | byte gateway[] = { 10, 0, 0, 1 }; 17 | 18 | // the subnet mask 19 | byte subnet[] = { 255, 255, 255, 0 }; 20 | 21 | // Initialize the Ethernet server library 22 | EthernetServer server(80); 23 | 24 | // derive an instance of the HTTPserver class with custom handlers 25 | class myServerClass : public HTTPserver 26 | { 27 | virtual void processPostType (const char * key, const byte flags); 28 | virtual void processPathname (const char * key, const byte flags); 29 | virtual void processHttpVersion (const char * key, const byte flags); 30 | virtual void processGetArgument (const char * key, const char * value, const byte flags); 31 | virtual void processHeaderArgument (const char * key, const char * value, const byte flags); 32 | virtual void processCookie (const char * key, const char * value, const byte flags); 33 | virtual void processPostArgument (const char * key, const char * value, const byte flags); 34 | }; // end of myServerClass 35 | 36 | myServerClass myServer; 37 | 38 | // ----------------------------------------------- 39 | // User handlers 40 | // ----------------------------------------------- 41 | 42 | void myServerClass::processPostType (const char * key, const byte flags) 43 | { 44 | println(F("HTTP/1.1 200 OK")); 45 | println(F("Content-Type: text/plain\n" 46 | "Connection: close\n" 47 | "Server: HTTPserver/1.0.0 (Arduino)")); 48 | println(); // end of headers 49 | 50 | print (F("GET/POST type: ")); 51 | println (key); 52 | } // end of processPostType 53 | 54 | void myServerClass::processPathname (const char * key, const byte flags) 55 | { 56 | print (F("Pathname: ")); 57 | println (key); 58 | } // end of processPathname 59 | 60 | void myServerClass::processHttpVersion (const char * key, const byte flags) 61 | { 62 | print (F("HTTP version: ")); 63 | println (key); 64 | } // end of processHttpVersion 65 | 66 | void myServerClass::processGetArgument (const char * key, const char * value, const byte flags) 67 | { 68 | print (F("Get argument: ")); 69 | print (key); 70 | print (F(" = ")); 71 | println (value); 72 | } // end of processGetArgument 73 | 74 | void myServerClass::processHeaderArgument (const char * key, const char * value, const byte flags) 75 | { 76 | print (F("Header argument: ")); 77 | print (key); 78 | print (F(" = ")); 79 | println (value); 80 | } // end of processHeaderArgument 81 | 82 | void myServerClass::processCookie (const char * key, const char * value, const byte flags) 83 | { 84 | print (F("Cookie: ")); 85 | print (key); 86 | print (F(" = ")); 87 | println (value); 88 | } // end of processCookie 89 | 90 | void myServerClass::processPostArgument (const char * key, const char * value, const byte flags) 91 | { 92 | print (F("Post argument: ")); 93 | print (key); 94 | print (F(" = ")); 95 | println (value); 96 | } // end of processPostArgument 97 | 98 | // ----------------------------------------------- 99 | // End of user handlers 100 | // ----------------------------------------------- 101 | 102 | void setup () 103 | { 104 | // start the Ethernet connection and the server: 105 | Ethernet.begin(mac, ip, gateway, subnet); 106 | } // end of setup 107 | 108 | void loop () 109 | { 110 | // listen for incoming clients 111 | EthernetClient client = server.available(); 112 | if (!client) 113 | return; 114 | 115 | myServer.begin (&client); 116 | while (client.connected() && !myServer.done) 117 | { 118 | while (client.available () > 0 && !myServer.done) 119 | myServer.processIncomingByte (client.read ()); 120 | 121 | // do other stuff here 122 | 123 | } // end of while client connected 124 | 125 | myServer.println(F("OK, done.")); 126 | myServer.flush (); 127 | 128 | // give the web browser time to receive the data 129 | delay(1); 130 | // close the connection: 131 | client.stop(); 132 | 133 | } // end of loop 134 | 135 | -------------------------------------------------------------------------------- /examples/Process_Cookie_information/Process_Cookie_information.ino: -------------------------------------------------------------------------------- 1 | // Tiny web server demo 2 | // Author: Nick Gammon 3 | // Date: 20 July 2015 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | // Enter a MAC address and IP address for your controller below. 10 | byte mac[] = { 0x90, 0xA2, 0xDA, 0x00, 0x2D, 0xA1 }; 11 | 12 | // The IP address will be dependent on your local network: 13 | byte ip[] = { 10, 0, 0, 241 }; 14 | 15 | // the router's gateway address: 16 | byte gateway[] = { 10, 0, 0, 1 }; 17 | 18 | // the subnet mask 19 | byte subnet[] = { 255, 255, 255, 0 }; 20 | 21 | // Initialize the Ethernet server library 22 | EthernetServer server(80); 23 | 24 | // derive an instance of the HTTPserver class with custom handlers 25 | class myServerClass : public HTTPserver 26 | { 27 | virtual void processPostType (const char * key, const byte flags); 28 | virtual void processCookie (const char * key, const char * value, const byte flags); 29 | virtual void processPostArgument (const char * key, const char * value, const byte flags); 30 | 31 | public: 32 | 33 | static const int MAX_NAME_LENGTH = 30; 34 | // put the cookie value here 35 | char theirName [MAX_NAME_LENGTH + 1]; // allow for trailing 0x00 36 | }; // end of myServerClass 37 | 38 | myServerClass myServer; 39 | 40 | // ----------------------------------------------- 41 | // User handlers 42 | // ----------------------------------------------- 43 | 44 | 45 | void myServerClass::processPostType (const char * key, const byte flags) 46 | { 47 | println(F("HTTP/1.1 200 OK")); 48 | println(F("Content-Type: text/html\n" 49 | "Connection: close\n" 50 | "Server: HTTPserver/1.0.0 (Arduino)")); 51 | theirName [0] = 0; // no name yet 52 | } // end of processPostType 53 | 54 | 55 | void myServerClass::processCookie (const char * key, const char * value, const byte flags) 56 | { 57 | // if we get a "name" cookie, save the name 58 | if (strcmp (key, "name") == 0) 59 | { 60 | strncpy (theirName, value, MAX_NAME_LENGTH); 61 | theirName [MAX_NAME_LENGTH] = 0; 62 | } 63 | } // end of processCookie 64 | 65 | void myServerClass::processPostArgument (const char * key, const char * value, const byte flags) 66 | { 67 | // if they sent a form with a new name, set the cookie to it 68 | if (strcmp (key, "name") == 0) 69 | { 70 | strncpy (theirName, value, MAX_NAME_LENGTH); 71 | theirName [MAX_NAME_LENGTH] = 0; 72 | setCookie ("name", value); 73 | } 74 | } // end of myServerClass::processPostArgument 75 | 76 | // ----------------------------------------------- 77 | // End of user handlers 78 | // ----------------------------------------------- 79 | 80 | void setup () 81 | { 82 | // start the Ethernet connection and the server: 83 | Ethernet.begin(mac, ip, gateway, subnet); 84 | server.begin(); 85 | } // end of setup 86 | 87 | void loop () 88 | { 89 | // listen for incoming clients 90 | EthernetClient client = server.available(); 91 | if (!client) 92 | { 93 | // do other processing here 94 | return; 95 | } 96 | 97 | myServer.begin (&client); 98 | while (client.connected() && !myServer.done) 99 | { 100 | while (client.available () > 0 && !myServer.done) 101 | myServer.processIncomingByte (client.read ()); 102 | 103 | // do other stuff here 104 | 105 | } // end of while client connected 106 | 107 | // end of headers 108 | myServer.println(); 109 | // start HTML stuff 110 | myServer.println (F("\n" 111 | "\n" 112 | "\n" 113 | "Arduino test\n" 114 | "\n" 115 | "\n")); 116 | 117 | myServer.print (F("

Your name is registered as: ")); 118 | myServer.fixHTML (myServer.theirName); 119 | myServer.println (); 120 | 121 | myServer.println (F("

")); 122 | myServer.print (F("

Your name: ")); 129 | myServer.println (F("

")); 130 | myServer.println (F("

")); 131 | 132 | myServer.println(F("
OK, done.")); 133 | 134 | myServer.println (F("\n" 135 | "")); 136 | 137 | myServer.flush (); 138 | 139 | // give the web browser time to receive the data 140 | delay(1); 141 | // close the connection: 142 | client.stop(); 143 | 144 | } // end of loop 145 | -------------------------------------------------------------------------------- /examples/Process_GET_arguments/Process_GET_arguments.ino: -------------------------------------------------------------------------------- 1 | // Tiny web server demo 2 | // Author: Nick Gammon 3 | // Date: 20 July 2015 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | const int LOW_PIN = 3; 10 | const int HIGH_PIN = 5; 11 | 12 | // Enter a MAC address and IP address for your controller below. 13 | byte mac[] = { 0x90, 0xA2, 0xDA, 0x00, 0x2D, 0xA1 }; 14 | 15 | // The IP address will be dependent on your local network: 16 | byte ip[] = { 10, 0, 0, 241 }; 17 | 18 | // the router's gateway address: 19 | byte gateway[] = { 10, 0, 0, 1 }; 20 | 21 | // the subnet mask 22 | byte subnet[] = { 255, 255, 255, 0 }; 23 | 24 | // Initialize the Ethernet server library 25 | EthernetServer server(80); 26 | 27 | // derive an instance of the HTTPserver class with custom handlers 28 | class myServerClass : public HTTPserver 29 | { 30 | virtual void processPostType (const char * key, const byte flags); 31 | virtual void processGetArgument (const char * key, const char * value, const byte flags); 32 | }; // end of myServerClass 33 | 34 | myServerClass myServer; 35 | 36 | // ----------------------------------------------- 37 | // User handlers 38 | // ----------------------------------------------- 39 | 40 | void myServerClass::processPostType (const char * key, const byte flags) 41 | { 42 | println(F("HTTP/1.1 200 OK")); 43 | println(F("Content-Type: text/html\n" 44 | "Connection: close\n" 45 | "Server: HTTPserver/1.0.0 (Arduino)")); 46 | println(); // end of headers 47 | println (F("\n" 48 | "\n" 49 | "\n" 50 | "Arduino test\n" 51 | "\n" 52 | "\n")); 53 | 54 | } // end of processPostType 55 | 56 | void myServerClass::processGetArgument (const char * key, const char * value, const byte flags) 57 | { 58 | 59 | int which = atoi (value); 60 | if (which >= LOW_PIN && which <= HIGH_PIN) 61 | { 62 | if (strcmp (key, "on") == 0) 63 | { 64 | digitalWrite (which, HIGH); 65 | print (F("

Turning on pin ")); 66 | println (which); 67 | } 68 | else if (strcmp (key, "off") == 0) 69 | { 70 | digitalWrite (which, LOW); 71 | print (F("

Turning OFF pin ")); 72 | println (which); 73 | } 74 | else 75 | { 76 | print (F("

Bad action: ")); 77 | fixHTML (value); 78 | println (F("")); 79 | } 80 | } 81 | else 82 | { 83 | print (F("

Bad pin number: ")); 84 | fixHTML (value); 85 | println (F("")); 86 | } 87 | 88 | } // end of processGetArgument 89 | 90 | // ----------------------------------------------- 91 | // End of user handlers 92 | // ----------------------------------------------- 93 | 94 | void setup () 95 | { 96 | // start the Ethernet connection and the server: 97 | Ethernet.begin(mac, ip, gateway, subnet); 98 | 99 | for (int i = LOW_PIN; i <= HIGH_PIN; i++) 100 | pinMode (i, OUTPUT); 101 | } // end of setup 102 | 103 | void loop () 104 | { 105 | // listen for incoming clients 106 | EthernetClient client = server.available(); 107 | if (!client) 108 | { 109 | // do other processing here 110 | return; 111 | } 112 | 113 | myServer.begin (&client); 114 | while (client.connected() && !myServer.done) 115 | { 116 | while (client.available () > 0 && !myServer.done) 117 | myServer.processIncomingByte (client.read ()); 118 | 119 | // do other stuff here 120 | 121 | } // end of while client connected 122 | 123 | myServer.println(F("


OK, done.")); 124 | 125 | myServer.println (F("\n" 126 | "")); 127 | 128 | myServer.flush (); 129 | 130 | // give the web browser time to receive the data 131 | delay(1); 132 | // close the connection: 133 | client.stop(); 134 | 135 | } // end of loop 136 | -------------------------------------------------------------------------------- /examples/Process_POST_arguments/Process_POST_arguments.ino: -------------------------------------------------------------------------------- 1 | // Tiny web server demo 2 | // Author: Nick Gammon 3 | // Date: 20 July 2015 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | const int LOW_PIN = 3; 10 | const int HIGH_PIN = 5; 11 | 12 | // Enter a MAC address and IP address for your controller below. 13 | byte mac[] = { 0x90, 0xA2, 0xDA, 0x00, 0x2D, 0xA1 }; 14 | 15 | // The IP address will be dependent on your local network: 16 | byte ip[] = { 10, 0, 0, 241 }; 17 | 18 | // the router's gateway address: 19 | byte gateway[] = { 10, 0, 0, 1 }; 20 | 21 | // the subnet mask 22 | byte subnet[] = { 255, 255, 255, 0 }; 23 | 24 | // Initialize the Ethernet server library 25 | EthernetServer server(80); 26 | 27 | // derive an instance of the HTTPserver class with custom handlers 28 | class myServerClass : public HTTPserver 29 | { 30 | virtual void processPostType (const char * key, const byte flags); 31 | virtual void processPostArgument (const char * key, const char * value, const byte flags); 32 | }; // end of myServerClass 33 | 34 | myServerClass myServer; 35 | 36 | // ----------------------------------------------- 37 | // User handlers 38 | // ----------------------------------------------- 39 | 40 | void myServerClass::processPostType (const char * key, const byte flags) 41 | { 42 | println(F("HTTP/1.1 200 OK")); 43 | println(F("Content-Type: text/html\n" 44 | "Connection: close\n" 45 | "Server: HTTPserver/1.0.0 (Arduino)")); 46 | println(); // end of headers 47 | println (F("\n" 48 | "\n" 49 | "\n" 50 | "Arduino test\n" 51 | "\n" 52 | "\n")); 53 | 54 | } // end of processPostType 55 | 56 | void myServerClass::processPostArgument (const char * key, const char * value, const byte flags) 57 | { 58 | if (memcmp (key, "led_", 4) == 0 && isdigit (key [4]) ) 59 | { 60 | int which = atoi (&key [4]); 61 | if (which >= LOW_PIN && which <= HIGH_PIN) 62 | digitalWrite (which, HIGH); 63 | } 64 | } // end of processPostArgument 65 | 66 | // ----------------------------------------------- 67 | // End of user handlers 68 | // ----------------------------------------------- 69 | 70 | void setup () 71 | { 72 | // start the Ethernet connection and the server: 73 | Ethernet.begin(mac, ip, gateway, subnet); 74 | 75 | for (int i = LOW_PIN; i <= HIGH_PIN; i++) 76 | pinMode (i, OUTPUT); 77 | } // end of setup 78 | 79 | void loop () 80 | { 81 | // listen for incoming clients 82 | EthernetClient client = server.available(); 83 | if (!client) 84 | { 85 | // do other processing here 86 | return; 87 | } 88 | 89 | // default LEDs to off 90 | for (int i = LOW_PIN; i <= HIGH_PIN; i++) 91 | digitalWrite (i, LOW); 92 | 93 | myServer.begin (&client); 94 | while (client.connected() && !myServer.done) 95 | { 96 | while (client.available () > 0 && !myServer.done) 97 | myServer.processIncomingByte (client.read ()); 98 | 99 | // do other stuff here 100 | 101 | } // end of while client connected 102 | 103 | // send the form 104 | myServer.println (F("

")); 105 | for (int i = LOW_PIN; i <= HIGH_PIN; i++) 106 | { 107 | myServer.print (F("
LED: ")); 108 | myServer.print (i); 109 | myServer.print (F(" ")); 116 | } 117 | // submit button 118 | myServer.println (F("

")); 119 | myServer.println (F("

")); 120 | 121 | myServer.println(F("
OK, done.")); 122 | 123 | myServer.println (F("\n" 124 | "")); 125 | 126 | myServer.flush (); 127 | 128 | // give the web browser time to receive the data 129 | delay(1); 130 | // close the connection: 131 | client.stop(); 132 | 133 | } // end of loop 134 | --------------------------------------------------------------------------------