Подключение микроконтроллера к локальной сети: HTTP и CGI

В предыдущей части соотношение объёмов моего корявого быдлокода и интересной/полезной информации превысило всякие разумные пределы. Так что в этой я постараюсь исправиться)

Речь пойдёт о реализации простенького веб-сервера на базе TCP/IP стека, запиленного в предыдущей статье. Скорее, даже не сервера, а веб-интерфейса, который можно прикрутить к какому-нибудь сетевому девайсику на микроконтроллере.

Простой пример
Для начала, сделаем втупую)

// Принимаем все подключения
uint8_t tcp_listen(uint8_t id, eth_frame_t *frame)
{
return 1;
}

// В ответ на любой пакет отправляем тестовую страничку
// и закрываем соединение
void tcp_data(uint8_t id, eth_frame_t *frame, uint16_t len, uint8_t closing)
{
const prog_char *str = PSTR(
"HTTP/1.0 200 OKrn"
"Content-Type: text/plain; charset=windows-1251rn"
"rn"
"Превед из микроконтроллера!");

ip_packet_t *ip = (void*)(frame->data);
tcp_packet_t *tcp = (void*)(ip->data);
uint16_t len_str;

// Отправляем данные
len_str = strlen_P(str);
memcpy_P(tcp->data, str, len_str);
tcp_send(id, frame, len_str, 1);
}

void tcp_opened(uint8_t id, eth_frame_t *frame) {}
void tcp_closed(uint8_t id, uint8_t reset) {}

Заходим браузером на, с позволения сказать, серевер и видим следующее:

Что радует, на отдачу странички уходят считанные милисекунды (смотрим в столбик Time):

Теперь рассмотрим как это работает поподробней.

HTTP
HTTP (Hypertext Transfer Protocol) — прикладной протокол, работающий поверх TCP, с помощью которого наш сервер и отдаёт странички браузеру. Протокол текстовый и достаточно простой. Возьмём сниффер и посмотрим как браузер обменивается данными с HTTP-сервером из предыдущего примера:

Как запрос, так и ответ содержат заголовки (каждая строка — отдельное поле заголовка). Заканичвается заголовок пустой строкой. После заголовка могут идти данные.

Заголовок запроса начинается со следующей строки:

GET / HTTP/1.1

  • GET — используемый метод. Данный метод используется для получения странички с веб-сервера в самом простом случае. Также применяются методы POST — для отправки данных на сервер и HEAD — для получения информации о страничке без скачивания самой странички. Минимально функциональный сервер должен поддерживать, по крайней мере, метод GET.
  • / — URL странички, которую запрашивает браузер.
  • HTTP/1.1 — тип и версия заголовка.

После этой строки идут дополнительные поля, содержащие различную информацию. Поля эти (кроме Host) необязательные. Для нас, в них, в принципе, нет ничего полезного. Разве что можно распарсить Host, если захотим сделать поддержку нескольких хостов.

Ответ нашего сервера начинается со строки:

HTTP/1.0 200 OK

  • HTTP/1.0 — тип и версия заголовка.
  • 200 — Код, обозначающий результат запроса. 200 — всё нормально, 403 — доступ запрещён, 404 — не найдена страничка по указанному URL, etc.
  • OK — текстовое описание статуса. Обычно нигде не используется и добавляется просто так.

Также в заголовке ответа мы указали поле Content-Type, содержащее MIME-тип отдаваемого документа и его кодировку. Оно нужно, чтобы браузер точно знал как отобразить полученную страничку.

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

Мы отправляли одну и ту же страничку, не разбираясь какой был указан URL, и вообще, не обращая внимание на запрос. Не хорошо, пора это исправить)

Пример посложнее
Напишем простенький веб-интерфейс для управления светодиодом. Алгоритм его работы будет следующим:

  • При получении пакета по TCP, смотрим, является ли он GET-запросом. Если является, выковыриваем из него URL.
  • Разбираем URL на путь и параметры.
  • Проверяем путь. Пока у нас будет только главная страничка. Если путь не равен "/", возрвращаем страничку с ошибкой 404.
  • Проверяем параметры. Если в параметрах есть команда, выполняем её (включаем/выключаем светодиод).
  • Отдаём страничку, с текущим состоянием светодиода и ссылкой для включения/выключения.

// Страничка с ошибкой 404
prog_char webif_404_reply[] =
"HTTP/1.0 404 Not Foundrn"
"Content-Type: text/html; charset=windows-1251rn"
"Server: ATmega16rn"
"rn"
"<pre>Page not foundrnrn"
"<a href=’/’>Home page</a></pre>rn";

// Заголовок странички
prog_char webif_200_header[] =
"HTTP/1.0 200 OKrn"
"Content-Type: text/html; charset=windows-1251rn"
"Server: ATmega16rn"
"rn";

// Здесь всё понятно
#define LED_DDR DDRD
#define LED_PORT PORTD
#define LED_BIT (1<<PD0)

#define led_on() { LED_PORT &= ~LED_BIT; led_state = 1; }
#define led_off() { LED_PORT |= LED_BIT; led_state = 0; }

// Переменные
uint8_t led_state; // Состояние светодиода
uint8_t lang_ru; // Язык странички

// Функция для копирования данных из флешки в буфер
void fill_buf_p(char **buf, const prog_char *pstr)
{
char c;
while((c = pgm_read_byte(pstr)))
{
*((*buf)++) = c;
pstr++;
}
}

// Вызывается при старте
void webif_init()
{
LED_DDR |= LED_BIT;
led_off();
}

// Вызывается при получении данных по TCP
void webif_data(uint8_t id, eth_frame_t *frame, uint16_t len)
{
ip_packet_t *ip = (void*)(frame->data);
tcp_packet_t *tcp = (void*)(ip->data);
char *req = (void*)tcp_get_data(tcp);
char *buf = (void*)(tcp->data), *buf_ptr = buf;
char *url, *p, *params, *name, *value;

// Нет данных? — выходим
if(!len) return;

// Парсим запрос — проверяем что метод — GET и вытаскиваем URL
if( (memcmp_P(req, PSTR("GET "), 4) == 0) &&
((p = strchr(req + 4, ‘ ‘)) != 0) )
{
url = req + 4;
*p = 0;

// Разбираем URL на путь и параметры
if((params = strchr(url, ‘?’)))
*(params++) = 0;

// Проверяем путь. Пока у нас есть
// только одна главная страничка "/"
if(strcmp_P(url, PSTR("/")) == 0)
{

// Разбираем параметры
while(params)
{
// Смотрим где заканчивается параметр
if((p = strchr(params, ‘&’)))
*(p++) = 0;

// Разбираем параметр на имя и значение
name = params;
if((value = strchr(name, ‘=’)))
*(value++) = 0;

// Параметр = led (on/off)?
if( (strcmp_P(name, PSTR("led")) == 0 ) && value )
{
if(strcmp_P(value, PSTR("on")) == 0)
led_on()
else if(strcmp_P(value, PSTR("off")) == 0)
led_off()
}

// Параметр = lang (en/ru)?
else if( (strcmp_P(name, PSTR("lang")) == 0) && value )
{
if(strcmp_P(value, PSTR("en")) == 0)
lang_ru = 0;
else if(strcmp_P(value, PSTR("ru")) == 0)
lang_ru = 1;
}

// Переходим к следующему параметру
params = p;
}

// заливаем в буфер страничку
fill_buf_p(&buf_ptr, webif_200_header);
fill_buf_p(&buf_ptr, PSTR("<pre>"));

if(!lang_ru)
{
fill_buf_p(&buf_ptr, PSTR("<p align=’right’>[<b>EN</b> | "
"<a href=’/?lang=ru’>RU</a>]</p>"));
}
else
{
fill_buf_p(&buf_ptr, PSTR("<p align=’right’>[<a href=’/?lang=en’>EN</a> | "
"<b>RU</b>]</p>"));
}

if((!led_state)&&(!lang_ru))
fill_buf_p(&buf_ptr, PSTR("Led is OFF. Turn <a href=’/?led=on’>on</a>."));
else if(led_state &&(!lang_ru))
fill_buf_p(&buf_ptr, PSTR("Led is ON. Turn <a href=’/?led=off’>off</a>."));
else if((!led_state)&&(lang_ru))
fill_buf_p(&buf_ptr, PSTR("Светодиод выключен. <a href=’/?led=on’>Включить</a>."));
else if(led_state &&(lang_ru))
fill_buf_p(&buf_ptr, PSTR("Светодиод включен. <a href=’/?led=off’>Выключить</a>."));

fill_buf_p(&buf_ptr, PSTR("</pre>"));
}

// Неправильный URL — пишем в буфер страничку с ошибкой 404
else
{
fill_buf_p(&buf_ptr, webif_404_reply);
}
}

// Отправляем данные из буфера
tcp_send(id, frame, buf_ptr-buf, 1);
}

// Принимаем подключения на порт 80
uint8_t tcp_listen(uint8_t id, eth_frame_t *frame)
{
ip_packet_t *ip = (void*)(frame->data);
tcp_packet_t *tcp = (void*)(ip->data);

if(tcp->to_port == htons(80))
return 1;
return 0;
}

// Передаём данные "серверу"
void tcp_data(uint8_t id, eth_frame_t *frame, uint16_t len, uint8_t closing)
{
webif_data(id, frame, len);
}

Заходим на девайс браузером и видим примерно вот такую страничку:

Страничка наша сделана максимально компактной, чтобы точно поместиться в один пакет. Как видим, ответ сервера занял всего 200 байт.

Можно теперь немного поиграться:

Формы
HTML позволяет создавать формы для отправки различных данных на сервер. Главное не забывать о том, что вся страничка должна поместиться в 1 пакет. Такое уж ограничение у сервера на микроконтроллере)

Так что, если нужно сделать много настроек, стоит разбить веб-интерфейс на несколько вкладок.

Вот простейшая форма, позволяющая юзеру ввести один байт:

<form action=’/’ method=’GET’>
Enter value (0..255):
<input type=’text’ name=’pwm’ size=’4′ value=’33’>
<input type=’submit’ value=’OK’>
</form>

В тэге form указывается адрес и способ отправки данных на сервер. В данном случае, это метод GET — поля формы будут отправлены в виде параметров в URL.

В браузере эта форма будет выглядеть примерно вот так:

При нажатии ОК, на сервер будет отправлен следующий запрос:

GET /?pwm=33 HTTP/1.1

С помощью такой формы мы можем, например, регулировать яркость светодиода

// Инициализация
void webif_init()
{
// Включаем ШИМ
DDRB |= (1<<PB2)|(1<<PB3);
TCCR0 = (1<<WGM01)|(1<<WGM00)|(1<<COM01)|(1<<COM00)|(1<<CS02);
}

// Обработка запросов браузера
void webif_data(uint8_t id, eth_frame_t *frame, uint16_t len)
{
ip_packet_t *ip = (void*)(frame->data);
tcp_packet_t *tcp = (void*)(ip->data);
char *req = (void*)tcp_get_data(tcp);
char *buf = (void*)(tcp->data), *buf_ptr = buf;
char *url, *p, *params, *name, *value;
char str[8];
int val;

if(!len) return;

if( (memcmp_P(req, PSTR("GET "), 4) == 0) &&
((p = strchr(req + 4, ‘ ‘)) != 0) )
{
url = req + 4;
*p = 0;

if((params = strchr(url, ‘?’)))
*(params++) = 0;

// Браузер запросил главную страничку?
if(strcmp_P(url, PSTR("/")) == 0)
{
// Разбираем параметры
while(params)
{
if((p = strchr(params, ‘&’)))
*(p++) = 0;

name = params;
if((value = strchr(name, ‘=’)))
*(value++) = 0;

// Параметр pwm
if( (strcmp_P(name, PSTR("pwm")) == 0 ) && value )
{
// Парсим значение
val = atoi(value);
if(val < 0) val = 0;
if(val > 255) val = 255;
led_brightness = val;

// Устанавливаем яркость
OCR0 = led_brightness;
}

params = p;
}

itoa(led_brightness, str, 10);

// Отправляем главную страничку
fill_buf_p(&buf_ptr, webif_200_header);
fill_buf_p(&buf_ptr, PSTR("<pre>"));
fill_buf_p(&buf_ptr, PSTR("Brightness: "));
fill_buf(&buf_ptr, str);
fill_buf_p(&buf_ptr, PSTR(". <a href=’/edit’>Change</a>"));
fill_buf_p(&buf_ptr, PSTR("</pre>"));
}

// Браузер запросиил страничку /edit?
else if(strcmp_P(url, PSTR("/edit")) == 0)
{
itoa(led_brightness, str, 10);

// Отправляем форму
fill_buf_p(&buf_ptr, webif_200_header);
fill_buf_p(&buf_ptr, PSTR("<pre>"));
fill_buf_p(&buf_ptr, PSTR("<form action=’/’ method=’GET’>rn"));
fill_buf_p(&buf_ptr, PSTR("Enter value (0..255):rn"));
fill_buf_p(&buf_ptr, PSTR("<input type=’text’ name=’pwm’ size=’4′ value=’"));
fill_buf(&buf_ptr, str);
fill_buf_p(&buf_ptr, PSTR("’> "));
fill_buf_p(&buf_ptr, PSTR("<input type=’submit’ value=’OK’>rn"));
fill_buf_p(&buf_ptr, PSTR("</form>"));
fill_buf_p(&buf_ptr, PSTR("</pre>"));
}

// Браузер запросил другую страничку, говорим 404
else
{
fill_buf_p(&buf_ptr, webif_404_reply);
}
}

tcp_send(id, frame, buf_ptr-buf, 1);
}

Работает)

Заключение
В этой части мы рассмотрели основы работы с протоколом HTTP и научились делать простенькие веб-интерфейсы для девайсиков.

Проекты можно взять здесь: первый, второй.

В следующей подчасти мы продолжим играться с HTTP.

To be continued…

Все статьи цикла

  • Подключение микроконтроллера к локальной сети: Теория
  • Подключение микроконтроллера к локальной сети: работаем с ENC28J60
  • Подключение микроконтроллера к локальной сети: UDP-сервер
  • Подключение микроконтроллера к локальной сети: UDP-клиент
  • Подключение микроконтроллера к локальной сети: Широковещательные сообщения и DHCP
  • Подключение микроконтроллера к локальной сети: TCP-клиент
  • Подключение микроконтроллера к локальной сети: HTTP и CGI
  • Подключение микроконтроллера к локальной сети: TCP и HTTP (продолжение)
  • Подключение микроконтроллера к локальной сети: HTTP и CGI (заключение)
  • Подключение микроконтроллера к локальной сети: тесты производительности и краткое описание API стека
  • Подключение микроконтроллера к локальной сети: Заключение
  • Веб сервер на Tiny2313. Чисто ради лулзов

Источник

Оставить комментарий

Вы можете использовать следующие теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>