От переводчика. Всё интереснее и интереснее. В этом уроке рассматривается хранение двух файлов на SD карте памяти веб-сервера и создание ссылок в HTML коде веб-страниц на эти файлы, а также загрузка этих страниц при нажатии на ссылки. Фактически мини-прототип сайта, который вы можете самостоятельно развивать и модернизировать.

Создание ссылок в HTML


В HTML ссылки создаются при помощи тега <a>. Текст между открывающим тегом <a> и закрывающим тегом </a> становится «кликабельной» ссылкой.

Значение атрибута href тега <a> должно содержать имя файла веб-страницы на которую делается ссылка, например:

<p>Go to <a href="page2.htm">page 2</a>.</p>

Эта HTML строка создаст текстовый абзац в котором «page 2» станет ссылкой на файл с именем page2.htm.

Файл page2.htm должен присутствовать на карте памяти и находиться в том же каталоге, что и страница, содержащая ссылку на него.

Примеры HTML файлов


В качестве примера в этом уроке будут использоваться два HTML файла. Они должны быть сохранены на microSD карте памяти, а сама карта должна быть вставлена в соответствующий разъём на плате Ethernet Shield.

Главная страница index.htm, которая будет загружаться первой с сервера, содержит следующий HTML код:

<!DOCTYPE html>
<html>
    <head>
        <title>Arduino SD Card Web Page</title>
    </head>
    <body>
        <h1>Arduino SD Card Page with Link</h1>
        <p>Go to <a href="page2.htm">page 2</a>.</p>
    </body>
</html>

Эта страница ссылается на вторую страницу с именем page2.htm:

<!DOCTYPE html>
<html>
    <head>
        <title>Arduino SD Card Web Page 2</title>
    </head>
    <body>
        <h1>Arduino SD Card Page 2</h1>
        <p>Go back to <a href="index.htm">main page</a>.</p>
    </body>
</html>

Здесь страница page2.htm (обратно) ссылается на главную страницу index.htm.

Создайте два файла index.htm и page2.htm с указанным выше содержим и скопируйте их на microSD карту памяти. Затем вставьте карту в разъём на Ethernet Shield.

Предварительно работу этих страниц можно протестировать на компьютере (поместив оба файла в одну папку на жестком диске). Откройте файл index.htm в браузере и щёлкните на ссылку — при этом должен открыться файл page2.htm. При нажатии на ссылку на странице page2.htm браузер должен вернуться к странице index.htm.

В этом видео показано как скопировать файлы страниц на SD карту и как веб-сервер работает с ними. Код скетча и его описание будут даны ниже.


HTTP запросы страниц


Когда веб-браузер запрашивает главную страницу с веб-сервера Arduino, он отправляет HTTP запрос, подобный этому:

GET / HTTP/1.1
Host: 10.0.0.20
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:19.0) Gecko/20100101 Firefox/19.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-ZA,en-GB;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: keep-alive

Мы уже видели такие HTTP запросы в предыдущих частях этого руководства.

При нажатии на ссылку на странице index.htm (это ссылка на страницу page2.htm), браузер отправляет Arduino серверу следующий запрос:

GET /page2.htm HTTP/1.1
Host: 10.0.0.20
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686; rv:19.0) Gecko/20100101 Firefox/19.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-ZA,en-GB;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://10.0.0.20/
Connection: keep-alive

В первом случае производится запрос (GET /) корневого файла (по умолчанию это index.htm).

Во втором случае производится запрос определённой страницы (GET /page2.htm).

Поскольку функционал Arduino веб-сервера определяем мы, то в скетче мы должны предусмотреть проверку HTTP запроса и определить запрашивается ли корневой или какой-то другой файл.

Скетч Arduino для работы с двумя веб-страницами


Скетч, представленный ниже, это модифицированная версия скетча из 2-й части этого руководства.

/*--------------------------------------------------------------
  Скетч:      eth_websrv_SD_link

  Описание:  Arduino веб-сервер с двумя страницами на SD карте памяти, имеющими ссылки друг на друга.
  
  Оборудование: контроллер Arduino Uno, плата Ethernet Shield, microSD карта памяти 2 Гб, отформатированная в FAT16
                
  Программное обеспечение: среда разработки Arduino IDE
  
  Ссылки:
    - WebServer example by David A. Mellis and modified by Tom Igoe
    - SD card examples by David A. Mellis and Tom Igoe
    - Ethernet library documentation: http://arduino.cc/en/Reference/Ethernet
    - SD Card library documentation: http://arduino.cc/en/Reference/SD

  Дата создания:         2 марта 2013
  Изменено:     14 июня 2013
 
  Автор:       W.A. Smith, http://startingelectronics.org
--------------------------------------------------------------*/

#include <SPI.h>
#include <Ethernet.h>
#include <SD.h>

// Размер буфера для HTTP запроса
#define REQ_BUF_SZ   20

byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress ip(192, 168, 0, 20); // IP-адрес (нужно изменить на актуальный для вашей сети)
EthernetServer server(80);

File webFile; // для работы с файлами на SD карте
char HTTP_req[REQ_BUF_SZ] = {0}; // HTTP запрос, сохраняемый как null terminated строка
char req_index = 0; // индех HTTP_req буфера

void setup() {
    // Выключение Ethernet чипа
    pinMode(10, OUTPUT);
    digitalWrite(10, HIGH);
    
    Serial.begin(115200);
    
    // Инициализация SD карты
    Serial.println("Initializing SD card...");
    if (!SD.begin(4)) {
        Serial.println("ERROR - SD card initialization failed!");
        return;
    }
    Serial.println("SUCCESS - SD card initialized.");
    // Поиск файла index.htm
    if (!SD.exists("index.htm")) {
        Serial.println("ERROR - Can't find index.htm file!");
        return;
    }
    Serial.println("SUCCESS - Found index.htm file.");

    Ethernet.begin(mac, ip);
    server.begin();
}

void loop() {
    EthernetClient client = server.available();  // try to get client

    if (client) {
        boolean currentLineIsBlank = true;
        while (client.connected()) {
            if (client.available()) {
                char c = client.read();
                // оставить последний элемент массива 0 (null terminate строка)
                if (req_index < (REQ_BUF_SZ - 1)) {
                    HTTP_req[req_index] = c; // сохраняем символ HTTP запроса
                    req_index++;
                }
                Serial.print(c);    // печатаем HTTP запрос в Serial
                if (c == '\n' && currentLineIsBlank) {
                    client.println("HTTP/1.1 200 OK");
                    client.println("Content-Type: text/html");
                    client.println("Connnection: close");
                    client.println();
                    // Открываем запрошенный файл
                    if (StrContains(HTTP_req, "GET / ") || StrContains(HTTP_req, "GET /index.htm")) {
                        webFile = SD.open("index.htm");
                    }
                    else if (StrContains(HTTP_req, "GET /page2.htm")) {
                        webFile = SD.open("page2.htm");
                    }
                    // Посылаем веб-страницу клиенту
                    if (webFile) {
                        while(webFile.available()) {
                            client.write(webFile.read());
                        }
                        webFile.close();
                    }
                    // Обнуляем буфер
                    req_index = 0;
                    StrClear(HTTP_req, REQ_BUF_SZ);
                    break;
                }
                if (c == '\n') {
                    currentLineIsBlank = true;
                } 
                else if (c != '\r') {
                    currentLineIsBlank = false;
                }
            } // end if (client.available())
        } // end while (client.connected())
        delay(1);
        client.stop();
    } // end if (client)
}

// Обнуляем массив
void StrClear(char *str, char length) {
    for (int i = 0; i < length; i++) {
        str[i] = 0;
    }
}

// Ищем совпадение в запросе
char StrContains(char *str, char *sfind) {
    char found = 0;
    char index = 0;
    char len;

    len = strlen(str);
    
    if (strlen(sfind) > len) {
        return 0;
    }
    while (index < len) {
        if (str[index] == sfind[found]) {
            found++;
            if (strlen(sfind) == found) {
                return 1;
            }
        }
        else {
            found = 0;
        }
        index++;
    }

    return 0;
}

Примечание. Не забудьте изменить IP-адрес, указанный в скетче, на актуальный для вашей сети.

Ниже описаны изменения по сравнению с исходной версией из 2-й части руководства.

HTTP запрос


Скетч был изменен для сохранения HTTP запроса в строке HTTP_req. Затем эту строку можно использовать для поиска нужных данных (имени запрашиваемого файла).

HTTP запрос выводится в Serial и эту информацию можно использовать для диагностики и отладки работы кода.

Посылка запрошенной веб-страницы


После того, как Arduino сервер получил HTTP запрос от браузера, он сначала отвечает стандартным HTTP заголовком, а затем отправляет саму запрошенную страницу.

Фрагмент кода, который осуществляет выбор веб-страницы для отправки:

// Открываем запрошенный файл
if (StrContains(HTTP_req, "GET / ") || StrContains(HTTP_req, "GET /index.htm")) {
    webFile = SD.open("index.htm");
}
else if (StrContains(HTTP_req, "GET /page2.htm")) {
    webFile = SD.open("page2.htm");
}

Здесь открывается файл index.htm или файл page2.htm с SD карты памяти. Сам код, который отправляет файл браузеру, такой же, как во 2-й части этого руководства.

Для выбора того или иного файла, функцией StrContains() просматривается строка HTTP_req, содержащая запрос. Если запрос содержит «GET /», то выбирается корневой файл index.htm.

Если строка HTTP запроса содержит «GET /page2.htm», то будет открыт и отправлен браузеру файл page2.htm.

Ссылка на странице page2.htm указывает на index.htm, а не на «/». По этой причине в коде необходимо проверить, содержит ли HTTP запрос текст «GET /» или текст «GET /index.htm». Реализацию этого вы можете увидеть в вышеприведенном фрагменте кода.

Улучшения скетча


Скетч из этого урока демонстрирует механизм открытия ссылок на страницы, поэтому он был сделан максимально простым. Конечно же этот код может быть улучшен, например, можно сразу извлекать из запроса имя файла (страницы) и предусмотреть обработку ошибок, когда страница, запрашиваемая браузером, отсутствует на SD карте и т. д.

От переводчика о 8-й части


Фактически, всё, что написано в в этом уроке, можно свести к следующему:

GET запрос сохраняется в буфер, далее в этом запросе находится информация о запрошенном файле, на основании найденной информации открывается тот или иной файл, который затем передаётся браузеру.

Отсюда следует, что при помощи небольшой модификации кода можно масштабировать этот скетч для работы с любым количеством веб-страниц на SD карте памяти.

Часть 1, часть 2, часть 3, часть 4, часть 5, часть 6, часть 7.


Комментарии (2)


  1. fk0
    00.00.0000 00:00

    В HTML ссылки создаются при помощи тега <a>. Текст между открывающим тегом <a> и закрывающим тегом </a> становится «кликабельной» ссылкой.

    Слишком громко сказано. У тэга "A" исторически сложилось два назначения

    1. ссылка на "якорь" содержащийся на данной странице или на другую страницу, документ, файл... в таком случае элемент обязательно должен иметь атрибут "href".

    2. собственно "якорь" на который может вести ссылка -- даже имя тэга "A" образовано от слова Anchor -- якорь. В этом случае элемент должен иметь атрибут "id" или "name" (а ссылка на якорь будет иметь вид "#somename").

    В HTML5 появилось утверждение, что A-элемент без атрибута "href" выполняет роль "ссылки-заполнителя", но без помощи Javascript такая ссылка бесполезна (её не нажать до присвоения атрибута "href").

    А использование A-тэга как "якоря" никуда не девалось. Хотя вместо него можно использовать любой элемент с атрибутом "id".

    Значение атрибута href тега <a> должно содержать имя файла веб-страницы на которую делается ссылка...

    Не обязательно. Атрибут href может ссылаться на якорь на текущей страницы с помощью атрибута href="#somename".

    Вообще непонятно, зачем современный веб-сайт строить из множества дискретных файлов. Любой сложности сайт, веб-приложение, может быть построено из одной страницы, в которую интегрированы все необходимые ресурсы (картинки в base64, отдельные "страницы" как изначально не отображаемые DIV и т.п.) Разделение на дискретные файлы имеет смысл для постепенной загрузки (страница уже есть, а картинки только грузятся, например) в условиях ограниченного по полосе пропускания канала связи в основном. Если сайт большой. Типичный сайт для ардуино (управление чем-либо) прекрасно влезет в одну страницу. А нажатия кнопок в интерфейсе могут транслироваться в XHR-запросы.

    Ещё дискретные файлы нужны если предполагается устаревшие варианты html, когда не доступны ни современные CSS-селекторы (они могут организовать "навигацию" по сайту состоящему из единой страницы), ни Javascript.

    PS: вообще содержимое единственной страницы сайта лучше закешировать в web storage, чтоб в следующие разы загружалось быстрей. Ну разумеется с чем-то вроде номера версии. Браузер впрочем и сам кеширует, но есть нюансы. И загрузку этой страницы реализовать через маленький начальный загрузчик (который принимает через XHR или script-тэг большой файл и вставляет его в свой DOM), маленкий начальный файл браузер точно закеширует. И добавить web application manifest, тогда можно добиться, что относительно "тяжёлый" сайт запросто будет работать даже в оффлайне. Кнопки через XHR конечно нажиматься перестанут, но остальной функционал полезный для пользователей может и остаться. Так можно построить полнофункциональное веб-приложение для управления электронными приборами или вроде того.


    1. smart_alex Автор
      00.00.0000 00:00
      +2

      Автор руководства решает непростую задачу - пытается объяснить начинающим сложные вещи простыми словами. Поэтому неизбежны упрощения и умолчания.

      И, надо сказать, в целом автор со своей задачей успешно справляется.