25.06.2009

Ethernet Shield по UDP

К сожалению, библиотека Ethernet для Arduino заточена под работу в режиме TCP. Иногда этого явно недостаточно, особенно при таких-то богатых возможностях чипа Wiznet W5100 как встроенная поддержка UDP, ICMP, IGMP, ARP и даже PPPoE.

Лично мне для работы с NTP понадобился именно UDP, поэтому пришлось быстро написать класс, аналогичный Client в библиотеке Ethernet, но работающий по UDP. Недолго думая, я нарек его ClientUDP и поместил в каталог Ethernet. Все, что мне потребовалось - переработать код существующего класса Client, о чем я и расскажу ниже.

Для тех, кого не интересуют тонкости: ClientUDP_1.0.zip (распаковать в каталог arduino-0016\hradware\libraries).

Итак, что меняется при переходе с TCP на UDP? Грубо говоря – почти всё.

Начнем с того, что получать данные из соединения TCP можно побайтно (или блоками - нововведение Arduino 0016), в то время как в UDP - только пакетами:



Когда сокет находится в режиме UDP ( Sn_MR_UDP ), в буфере Wiznet вместе с данными сохраняется и заголовок пакета, который тоже надо уметь читать и правильно обрабатывать.

Работа с Ethernet в Arduino целиком построена на библиотеках socket.c и w5100.c, являющихся уровнями работы с сокетами и чипом соответственно. И если при чтении сокета TCP надо вызывать recv, то при чтении UDP - уже recvfrom.

Тут кроется еще одна неприятность, о которой надо помнить: длина буфера len, передаваемая в recvfrom, с длиной принятого пакета (получаемого из заголовка, см. выше) не сравнивается (вносить изменения в socket.c я пока не решился).

Интерфейс стандартного Client выглядит так:

 
#include "Print.h"

class Client : public Print {
private:
static uint16_t _srcport;
uint8_t _sock;
uint8_t *_ip;
uint16_t _port;
public:
Client(uint8_t);
Client(uint8_t *, uint16_t);
uint8_t status();
uint8_t connect();
virtual void write(uint8_t);
virtual void write(const char *str);
virtual void write(const uint8_t *buf, size_t size);
int available();
int read();
void flush();
void stop();
uint8_t connected();
uint8_t operator==(int);
uint8_t operator!=(int);
operator bool();
friend class Server;
};




UDP - протокол без фазы установления соединения, поэтому часть функциональности connect выкидывается, а имя заменяется на open. Опциональный аргумент будет указывать нашему классу, как выбирать source-порт - на основе глобальной автоинкрементирующейся переменной (_srcport, объявлена в библиотеке), либо явно. В последнем случае, приложение полностью ответственно за то, чтобы у разных сокетов не случилось двух одинаковых source-портов.

Функция connected тоже смысла не имеет, но можно заменить opened.

Функция stop, закрывающая соединение, остается, но тоже упрощается.

Уходят в небытие операторы ==, !=, bool() - потому что в языке Processing аналогичного класса нет, равно как и нет объекта-сервера, который возвращает этот объект при подключении очередного Client-а (см. примеры в стандартной библиотеке Ethernet, чтобы понять, о чем речь).

Прототипы функций-членов записи write являются виртуальными и наследуются от класса Print. Соответственно, все операторы Print работают в конечном итоге через них, абстрагируясь таким образом от типа устройства вывода. В Arduino 0016 определили целых три варианта write:

1. вывод байта: virtual void write(uint8_t) = 0;
2. вывод строки: virtual void write(const char *str);
3. вывод буфера: virtual void write(const uint8_t *buffer, size_t size);

Первый метод надо перекрывать обязательно - он абстрактный; два остальных работают через него.

Выше я уже упоминал, что в UDP бесполезны байтовые операции ввода-вывода, но для совместимости, оставим на месте однобайтовый write - это будет посылка пакета из одного байта. Это надо обязательно учитывать при использовании print: печать byte и char будет порождать однобайтовые пакеты, println будет генерировать минимум два пакета, в каждом по одному символу '\r' и '\n'; та же ахинея будет происходить с распечатыванием целых и вещественных чисел - каждый символ будет передан отдельным пакетом.

Короче, если хотите сохранять ясность, используйте методы write напрямую, тем более, что они вполне публичные (public).

Процедуры чтения в классе Print нет, так что смело пишем свой собственный read.

Неизменной, пожалуй, останутся available, сообщающая размер приемного буфера, и status, читающая регистр состояния сокета (число состояний сокращается до двух: SOCK_CLOSED и SOCK_UDP).

Самая противная из всех - flush, которая очищает приемный буфер. Раньше она дергала однобайтовый read, пока available возвращала ненулевое значение. Теперь при вызове read надо забрать весь пакет целиком, не зная даже заранее его размер. Мое мнение – операцию сброса входного буфера надо выносить прямиком в socket.c, именно там ей и место. Поэтому, также исключаем ее из интерфейса.

Вот результат:

 
#include "Print.h"

class ClientUDP : public Print {
private:
static uint16_t _srcport;
uint8_t _sock;
uint8_t *_ip;
uint16_t _port;
public:
ClientUDP(uint8_t);
ClientUDP(uint8_t *, uint16_t);
uint8_t open(uint16_t);

uint8_t status();
virtual void write(uint8_t);
virtual void write(const uint8_t *, size_t);
uint16_t read(uint8_t *, uint16_t *, uint8_t *, uint16_t);
int available();
void stop();
uint8_t opened();
};



Если придираться, то, строго говоря, это не совсем Client, потому что сервер ему не нужен. Да и сам смысл этого класса нивелируется, если бы библиотека socket.c была доступна скетчам напрямую. Однако, на этом примере я наглядно демонстрирую, что при наличи исходного кода, здравой логики и желания, довольно просто можно получать необходимые вещи. В следующих статьях я обязательно покажу, как воспользоваться этой библиотекой.

Комментариев нет:

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