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

В этой части мы поговорим про TCP и напишем небольшое приложение, использующее этот протокол.

Краткое содержание:

  • Введение в TCP
  • Программный интерфейс и простенький пример
  • Реализация
  • Пример побольше
  • Заключение

Введение в TCP


Вот формат TCP-пакета:

TCP-пакет

Указатель потока — некое значение, увеличивающееся при отправке узлом данных. Если я отправляю пакет, содержащий 5 байт данных, указатель потока в следующем пакете увеличится на 5. Если в пакете установлен флаг SYN или FIN, указатель потока дополнительно увеличивается на 1.

Указатель подтверждения — в этом поле узел указывает до какой позиции он получил поток, отправляемый другим узлом. Если узел получает пакет с указателем потока равным 20, содержащий 100 байт данных, в следующем отправляемом пакете он запишет указатель подтверждения 120.

Флаги. Нам интересны флаги SYN, FIN, ACK и RST.

Флаг SYN служит для установки соединения и синхронизации указателей потоков. Этот флаг установлен в первом пакете текущего соединения, отправляемом другому узлу.

Флаг ACK — подтверждение. Означает, что в пакете установлен указатель подтверждения. Этот флаг обычно установлен во всех пакетах, кроме самого первого на соединение.

Флаг FIN служит для закрытия соединения и означает, что узел отправил последние данные.

Флаг RST — сброс соединения. Он означает, что что-то пошло не так и соединение было прибито узлом.

Размер окна служит для управления потоком. В этом поле узел указывает, сколько данных он готов получить.

Контрольная сумма рассчитывается по обычному для IP-стека алгоритму и, также как и в UDP, не от самого пакета, а от вот такой штуки:

TCP-пакет с псевдозаголовком

Лучше всего рассмотреть работу TCP на примере)

Установка соединения:

Установка соединения

Когда я хочу установить соединение с удалённым сервером, отправляю ему пакет с установленным флагом SYN. Флаг SYN означает синхронизацию начальных значений указателей потока. Своё начальное значение указателя я выбираю случайным образом. Флаг ACK не установлен, так что поле указателя подтверждения не юзается.

Сервер отвечает мне пакетом с установленными флагами SYN и ACK. Значение указателя потока он тоже выбирает случайно. Флаг SYN увеличивает указатель потока на 1, так что сервер подтверждает это увеличение и записывает в указатель подтверждения моё значение указателя потока + 1.

Я отправляю серверу пакет с установленным флагом ACK. Это подтверждает установку соединения.

Обмен данными:

Обмен данными

Когда соединение установлено, можно обмениваться пакетами. Но, в отличие от UDP, тут на каждый пакет нужно отправлять подтверждение, дабы удалённый узел знал, что отправленные им данные были успешно приняты.

Если я хочу закрыть соединение, посылаю удалённому узлу пакет с установленным флагом FIN.

Закрытие соединения

Удалённый узел отвечает мне таким же пакетом. Закрытие соединения подтверждается пакетом с установленным флагом ACK.

Впринципе, этого нам будет достаточно для реализации нашего простенького TCP-узла.

Программный интерфейс


Пришла пора добавить поддержку TCP в наш стек. Начать стоит с разработки программного интерфейса. Определим несколько коллбэков и вызовов.

Вызовы:

// Инициирует открытие нового соединения
// При ошибке возвращает 0xff.
// При успехе, возвращает идентификатор нового соединения
// Когда соединение будет создано (удалённый узел ответит),
//    будет вызван коллбэк tcp_opened
uint8_t tcp_open(uint32_t addr, uint16_t port, uint16_t local_port);

// Отправляет данные. Данные можно отправлять только в ответ на запрос
//    (т.е. из коллбэков tcp_opened и tcp_data )
// close - инициирует закрытие соединения. Когда соединение будет закрыто,
//    будет вызван коллбэк tcp_closed
void tcp_send(uint8_t id, eth_frame_t *frame, uint16_t len, uint8_t close);


Коллбэки:

//Вызывается при получении запроса на соединение
// Приложение возвращает ненулевое значение, если подтверждает соединение
uint8_t tcp_listen(uint8_t id, eth_frame_t *frame);

// Вызывается при успешном открытии нового соединения
void tcp_opened(uint8_t id, eth_frame_t *frame);

// Вызывается при закрытии соединения
//    reset - признак сброса соединения ("некорректного" закрытия)
void tcp_closed(uint8_t id, uint8_t reset);

// Вызывается, когда приложение должно забрать и/или отправить пакет
//    len - сколько получено данных (может быть 0)
//    closing - признак того, что соединение закрывается
//        (и удалённый узел отдаёт последние данные из буффера)
void tcp_data(uint8_t id, eth_frame_t *frame, uint16_t len, uint8_t closing);


Вот простенький пример того, как это может работать:

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);

    // Если получили запрос на подключение на порт 1212,
    //    подтверждаем соединение
    if(tcp->to_port == htons(1212))
        return 1;

    return 0;
}

void tcp_opened(uint8_t id, eth_frame_t *frame)
{
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);

    // Соединение открыто - отправляем "Hello!"
    memcpy(tcp->data, (void*)"Hello!\n", 7);
    tcp_send(id, frame, 7, 0);
}

void tcp_data(uint8_t id, eth_frame_t *frame, uint16_t len, uint8_t closing)
{
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    char *in_data = (void*)tcp_get_data(tcp);

    // Получены данные
    if( (!closing) && (len) )
    {
        // получили команду "quit" -
        //    отправляем "Bye" и закрываем соединение
        if(memcmp(in_data, "quit", 4) == 0)
        {
            memcpy(tcp->data, "Bye!\n", 5);
            tcp_send(id, frame, 5, 1);
            return;
        }

        // Отправляем "OK"
        memcpy(tcp->data, "OK\n", 3);
        tcp_send(id, frame, 3, 0);
    }
}

void tcp_closed(uint8_t id, uint8_t reset)
{
}


Первый тест)

Реализация


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

// Заголовок TCP-пакета
#define TCP_FLAG_URG        0x20
#define TCP_FLAG_ACK        0x10
#define TCP_FLAG_PSH        0x08
#define TCP_FLAG_RST        0x04
#define TCP_FLAG_SYN        0x02
#define TCP_FLAG_FIN        0x01

typedef struct tcp_packet {
    uint16_t from_port;        // порт отправителя
    uint16_t to_port;        // порт получателя
    uint32_t seq_num;        // указатель потока
    uint32_t ack_num;        // указатель подтверждения
    uint8_t data_offset;    // размер заголовка
    uint8_t flags;            // флаги
    uint16_t window;        // размер окна
    uint16_t cksum;            // контрольная сумма
    uint16_t urgent_ptr;    // указатель срочных данных
    uint8_t data[];
} tcp_packet_t;

// рассчитывает размер TCP-заголовка
#define tcp_head_size(tcp)    (((tcp)->data_offset & 0xf0) >> 2)

// вычисляет указатель на данные
#define tcp_get_data(tcp)    ((uint8_t*)(tcp) + tcp_head_size(tcp))


// Коды состояния TCP
typedef enum tcp_status_code {
    TCP_CLOSED,
    TCP_SYN_SENT,
    TCP_SYN_RECEIVED,
    TCP_ESTABLISHED,
    TCP_FIN_WAIT
} tcp_status_code_t;

// Состояние TCP
typedef struct tcp_state {
    tcp_status_code_t status;    // стейт
    uint16_t rx_time;            // время когда был получен последний пакет
    uint16_t local_port;        // локальный порт
    uint16_t remote_port;        // порт удалённого узла
    uint32_t remote_addr;        // IP-адрес удалённого узла
} tcp_state_t;


// Максимальное количество одновременно открытых соединений
#define TCP_MAX_CONNECTIONS        2

// Размер окна, который будем указывать в отправляемых пакетах
#define TCP_WINDOW_SIZE            65535

// Таймаут, после которого неактивные соединения будут прибиваться
#define TCP_TIMEOUT                10 //s

// Максимальный размер пакета, передаваемый при открытии соединения
#define TCP_SYN_MSS                448


// Пул TCP-соединений
tcp_state_t tcp_pool[TCP_MAX_CONNECTIONS];

// Режим отправки пакетов
// Устанавливается в 1 после отправки пакета
//  (следующие пакеты по умолчанию
//    будут отправляться на тот же адрес)
uint8_t tcp_use_resend;

// Создаёт TCP-соединение
uint8_t tcp_open(uint32_t addr, uint16_t port, uint16_t local_port)
{
    eth_frame_t *frame = (void*)net_buf;
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    tcp_state_t *st = 0, *pst;
    uint8_t id;
    uint16_t len;

    // Ищем свободный слот для соединения
    for(id = 0; id < TCP_MAX_CONNECTIONS; ++id)
    {
        pst = tcp_pool + id;

        if(pst->status == TCP_CLOSED)
        {
            st = pst;
            break;
        }
    }

    if(st)
    {
        // Отправляем SYN (активное открытие, шаг 1)
        ip->to_addr = addr;
        ip->from_addr = ip_addr;
        ip->protocol = IP_PROTOCOL_TCP;

        tcp->to_port = port;
        tcp->from_port = local_port;
        tcp->seq_num = 0;
        tcp->ack_num = 0;
        tcp->data_offset = (sizeof(tcp_packet_t) + 4) << 2;
        tcp->flags = TCP_FLAG_SYN;
        tcp->window = htons(TCP_WINDOW_SIZE);
        tcp->cksum = 0;
        tcp->urgent_ptr = 0;
        tcp->data[0] = 2; // MSS option
        tcp->data[1] = 4; // MSS option length = 4 bytes
        tcp->data[2] = TCP_SYN_MSS>>8;
        tcp->data[3] = TCP_SYN_MSS&0xff;

        len = sizeof(tcp_packet_t) + 4;

        tcp->cksum = ip_cksum(len + IP_PROTOCOL_TCP,
            (uint8_t*)tcp - 8, len + 8);

        if(ip_send(frame, len))
        {
            // Пакет отправлен, добавляем соединение в пул
            st->status = TCP_SYN_SENT;
            st->rx_time = rtime();
            st->remote_port = port;
            st->local_port = local_port;
            st->remote_addr = addr;

            return id;
        }
    }

    return 0xff;
}

// Отправляет ответ(ы) на полученный пакет
void tcp_reply(eth_frame_t *frame, uint16_t len)
{
    uint16_t temp;

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

    // Если в буфере лежит полученый пакет, меняем местами
    //   порты отправителя/получателя
    // Если в буфере предыдущий отправленый нами пакет,
    //   оставляем порты как есть
    if(!tcp_use_resend)
    {
        temp = tcp->from_port;
        tcp->from_port = tcp->to_port;
        tcp->to_port = temp;
        tcp->window = htons(TCP_WINDOW_SIZE);
        tcp->urgent_ptr = 0;
    }

    // Если отправляем SYN, добавляем опцию MSS
    //    (максимальный размер пакета)
    //    чтобы пакеты не фрагментировались
    if(tcp->flags & TCP_FLAG_SYN)
    {
        tcp->data_offset = (sizeof(tcp_packet_t) + 4) << 2;
        tcp->data[0] = 2;//option: MSS
        tcp->data[1] = 4;//option len
        tcp->data[2] = TCP_SYN_MSS>>8;
        tcp->data[3] = TCP_SYN_MSS&0xff;
        len = 4;
    }

    else
    {
        tcp->data_offset = sizeof(tcp_packet_t) << 2;
    }

    // Рассчитываем контрольную сумму
    len += sizeof(tcp_packet_t);
    tcp->cksum = 0;
    tcp->cksum = ip_cksum(len + IP_PROTOCOL_TCP,
        (uint8_t*)tcp - 8, len + 8);

    // В буфере лежит принятый пакет?
    if(!tcp_use_resend)
    {
        // Меняем адрес отправителя/получателя
        //    и отправляем
        ip_reply(frame, len);
        tcp_use_resend = 1;
    }

    // Или предыдущий отправленый пакет?
    else
    {
        // Отправляем туда же
        ip_resend(frame, len);
    }
}

// Отправка TCP-данных
void tcp_send(uint8_t id, eth_frame_t *frame, uint16_t len, uint8_t close)
{
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    tcp_state_t *st = tcp_pool + id;

    // Соединение установлена и нам есть что отправить (данные или FIN)
    if( (st->status == TCP_ESTABLISHED) && (len || close) )
    {
        // Отправляем FIN?
        if(close)
        {
            // Ждём пока удалённый узел тоже отправит FIN
            st->status = TCP_FIN_WAIT;
            tcp->flags = TCP_FLAG_FIN|TCP_FLAG_ACK;
        }

        // Обычные данные
        else
        {
            tcp->flags = TCP_FLAG_ACK;
        }

        // Отправляем пакет
        tcp_reply(frame, len);
    }
}

// Подтверждает принятые данные и устанавливает указатель потока
//    (обменивает seq_num и ack_num, затем прибавляет к ack_num
//    количество принятых данных)
void tcp_step(tcp_packet_t *tcp, uint16_t num)
{
    uint32_t ack_num;
    ack_num = ntohl(tcp->seq_num) + num;
    tcp->seq_num = tcp->ack_num;
    tcp->ack_num = htonl(ack_num);
}

// Обработка TCP-пакета
void tcp_filter(eth_frame_t *frame, uint16_t len)
{
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    tcp_state_t *st = 0, *pst;
    uint8_t id, tcpflags;

    // Пакет не на наш адрес - выбрасываем
    if(ip->to_addr != ip_addr)
        return;

    // Получен пакет, отправлять будем с обменом адресов
    tcp_use_resend = 0;

    // Рассчитываем количество полученных данных
    len -= tcp_head_size(tcp);

    // Выбираем интересные нам флаги (SYN/ACK/FIN/RST)
    tcpflags = tcp->flags & (TCP_FLAG_SYN|TCP_FLAG_ACK|
        TCP_FLAG_RST|TCP_FLAG_FIN);

    // Смотрим к какому соединению относится данный пакет
    for(id = 0; id < TCP_MAX_CONNECTIONS; ++id)
    {
        pst = tcp_pool + id;

        if( (pst->status != TCP_CLOSED) &&
            (ip->from_addr == pst->remote_addr) &&
            (tcp->from_port == pst->remote_port) &&
            (tcp->to_port == pst->local_port) )
        {
            st = pst;
            break;
        }
    }

    // Нет такого соединения
    if(!st)
    {
        // Получили SYN - запрос открытия соединения
        //  (пассивное открытие, шаг 1)
        if( tcpflags == TCP_FLAG_SYN )
        {
            // Ищем свободный слот для соединения
            for(id = 0; id < TCP_MAX_CONNECTIONS; ++id)
            {
                pst = tcp_pool + id;

                if(pst->status == TCP_CLOSED)
                {
                    st = pst;
                    break;
                }
            }

            // Слот найден и приложение подтверждает открытие соединения?
            if(st && tcp_listen(id, frame))
            {
                // Добавляем новое соединение
                st->status = TCP_SYN_RECEIVED;
                st->rx_time = rtime();
                st->remote_port = tcp->from_port;
                st->local_port = tcp->to_port;
                st->remote_addr = ip->from_addr;

                // Отправляем SYN/ACK
                //    (пассивное открытие, шаг 2)
                tcp->flags = TCP_FLAG_SYN|TCP_FLAG_ACK;
                tcp_step(tcp, 1);
                tcp->seq_num = gettc() + (gettc() << 16);
                tcp_reply(frame, 0);
            }
        }
    }

    else
    {
        st->rx_time = rtime();

        switch(st->status)
        {

        // Был отправлен SYN (активное открытие, пройден шаг 1)
        case TCP_SYN_SENT:

            // Если полученный пакет не SYN/ACK (RST, etc.)
            // прибиваем соединение
            if(tcpflags != (TCP_FLAG_SYN|TCP_FLAG_ACK))
            {
                st->status = TCP_CLOSED;
                break;
            }

            // Подтверждаем принятые данные,
            //    устанавливаем указатель потока
            tcp_step(tcp, len+1);

            // Получили SYN/ACK (шаг 2), отправляем ACK (шаг 3)
            tcp->flags = TCP_FLAG_ACK;
            tcp_reply(frame, 0);

            // Всё, соединение установлено
            st->status = TCP_ESTABLISHED;

            // Оповещаем приложение что открыто соединение
            tcp_opened(id, frame);

            break;

        // Был получен SYN (пассивное открытие, шаг 1),
        //    отправлен SYN/ACK (шаг 2)
        case TCP_SYN_RECEIVED:

            // Если полученный пакет - не ACK,
            //    прибиваем соединение
            if(tcpflags != TCP_FLAG_ACK)
            {
                st->status = TCP_CLOSED;
                break;
            }

            // Устанавливаем указатель потока и
            //    указатель подтверждения
            tcp_step(tcp, 0);

            // Получен ACK (шаг 3), соединение открыто
            st->status = TCP_ESTABLISHED;

            // Оповещаем приложение
            tcp_opened(id, frame);

            break;

        // Соединение установлено
        case TCP_ESTABLISHED:

            // Получили не ACK и не FIN/ACK
            if( (tcpflags & ~TCP_FLAG_FIN) != TCP_FLAG_ACK )
            {
                // Прибиваем соединение
                st->status = TCP_CLOSED;

                // Оповещаем приложение
                tcp_closed(id, 1);

                break;
            }

            // Получили FIN/ACK (пассивное закрытие, шаг 1)
            if(tcpflags == (TCP_FLAG_FIN|TCP_FLAG_ACK))
            {
                // Устанавливаем указатель потока и
                //    указатель подтверждения
                tcp_step(tcp, len+1);

                // Отвечаем FIN/ACK (пассивное закрытие, шаг 2)
                tcp->flags = TCP_FLAG_FIN|TCP_FLAG_ACK;
                tcp_reply(frame, 0);

                // Соединение закрыто
                st->status = TCP_CLOSED;

                if(len)
                {
                    // Если вместе с FIN/ACK приходили данные
                    //    (такое может быть), отдаём их приложению
                    tcp_data(id, frame, len, 1);
                }

                // Оповещаем приложение, что соединение закрыто
                tcp_closed(id, 0);

                break;
            }

            // Устанавливаем указатель потока и
            //    указатель подтверждения
            tcp_step(tcp, len);

            if(len)
            {
                // Отправляем подтверждение
                tcp->flags = TCP_FLAG_ACK;
                tcp_reply(frame, 0);
            }

            // Отдаём данные приложению,
            //    забираем данные у приложения
            tcp_data(id, frame, len, 0);

            break;

        // Был отправлен FIN/ACK (активное закрытие, шаг 1)
        case TCP_FIN_WAIT:

            // Получено что-то, отличающееся от ACK или FIN/ACK?
            if( (tcpflags & ~TCP_FLAG_FIN) != TCP_FLAG_ACK )
            {
                // Прибиваем соединение
                st->status = TCP_CLOSED;

                // Оповещаем приложение
                tcp_closed(id, 1);
                break;
            }

            // Получен FIN/ACK (активное закрытие, шаг 2 или 3)
            if(tcpflags == (TCP_FLAG_FIN|TCP_FLAG_ACK))
            {
                // Устанавливаем указатели потока и подтверждения
                tcp_step(tcp, len+1);

                // Отправлям ACK (активное закрытие, шаг 3 или 4)
                tcp->flags = TCP_FLAG_ACK;
                tcp_reply(frame, 0);

                // Соединение закрыто
                st->status = TCP_CLOSED;

                if(len)
                {
                    // Отдаём данные приложению (если были)
                    tcp_data(id, frame, len, 1);
                }

                // Оповещаем приложение
                tcp_closed(id, 0);

                break;
            }

            // Соединение ещё не закрыто,
            //    у удалённого узла остались данные в буфере
            if(len)
            {
                // Устанавливаем указатель подтверждения
                tcp_step(tcp, len);

                // Отправляем подтверждение
                tcp->flags = TCP_FLAG_ACK;
                tcp_reply(frame, 0);

                // Отдаём данные приложению
                tcp_data(id, frame, len, 1);
            }

            break;

        default:
            break;
        }
    }
}

// Вызывается периодически
void tcp_poll()
{
    uint8_t id;
    tcp_state_t *st;

    tcp_use_resend = 0;

    // Пробегаемся по соединениям
    for(id = 0; id < TCP_MAX_CONNECTIONS; ++id)
    {
        st = tcp_pool + id;

        // Если у соединения долго не было активности
        if( (st->status != TCP_CLOSED) &&
            (rtime() - st->rx_time > TCP_TIMEOUT) )
        {
            // Прибиваем его
            st->status = TCP_CLOSED;

            // И оповещаем приложение
            tcp_closed(id, 1);
        }
    }
}


Пишем приложение


Чтобы испытать наш TCP/IP стек на практике, напишем какое-нибудь несложное приложение. Например, которое будет получать данные с термодатчика и при превышении температуры слать гневное письмо на определённое мыло.

Для начала, нам понадобится реализовать клиент SMTP.

// Состояние клиента SMTP
typedef enum smtp_status_code {
    SMTP_INIT,
    SMTP_HELLO_SENT,
    SMTP_AUTH_SENT,
    SMTP_FROM_SENT,
    SMTP_TO_SENT,
    SMTP_DATA_SENT,
    SMTP_MESSAGE_SENT,
    SMTP_QUIT_SENT
} smtp_status_code_t;

// Всякие константы
typedef enum smtp_info_class {
    SMTP_USERNAME,
    SMTP_PASSWORD,
    SMTP_FROM_EMAIL,
    SMTP_TO_EMAIL,
    SMTP_SUBJECT,
    SMTP_MESSAGE,
} smtp_info_class_t;



uint8_t smtp_success;
smtp_status_code_t smtp_state;


/*
 * Вспомогательные функции
 */

// Запись данных в буфер
void smtp_fill_packet(char **ptr, char *str)
{
    while(*str) *((*ptr)++) = *(str++);
}

// Запись данных из флешки в буфер
void smtp_fill_packet_p(char **ptr, prog_char *str)
{
    char c;

    while((c = pgm_read_byte(str))) {
        *((*ptr)++) = c;
        str++;
    }
}

// Отправка команды QUIT
void smtp_quit(uint8_t id, eth_frame_t *frame)
{
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    char *outdata = (void*)(tcp->data);
    char *p;

    p = outdata;
    smtp_fill_packet_p(&p, PSTR("QUIT\r\n"));
    tcp_send(id, frame, p-outdata, 0);
    smtp_state = SMTP_QUIT_SENT;
}

/*
 * SMTP-клиент
 */

// Сброс состояния SMTP
void smtp_reset()
{
    smtp_success = 0;
    smtp_state = SMTP_INIT;
}

// Обработчик данных, приходящих с сервера
void smtp_data(uint8_t id, eth_frame_t *frame, uint16_t len)
{
    char buf[64];
    ip_packet_t *ip = (void*)(frame->data);
    tcp_packet_t *tcp = (void*)(ip->data);
    char *datain = (void*)tcp_get_data(tcp);
    char *dataout = (void*)(tcp->data), *p;
    int statuscode;
    uint8_t num;

    // Ничего не пришло
    if(!len) return;

    // Нам должен прийти код и текстовое сообщение.
    //    Берём только код
    datain[len] = 0;
    statuscode = atoi(datain);

    switch(smtp_state)
    {

    // Только начали. Нам должна прийти строка вида
    // 220 Превед, йа типо сервер бла бла бла
    case SMTP_INIT:

        // Не 220 - выходим
        if(statuscode != 220)
        {
            smtp_quit(id, frame);
            break;
        }

        // Отправляем HELO (имя хоста)
        p = dataout;
        smtp_fill_packet_p(&p, PSTR("HELO localhost\r\n"));
        tcp_send(id, frame, p-dataout, 0);

        smtp_state = SMTP_HELLO_SENT;
        break;

    // Отправили приветствие.
	// Нам должно прийти 250 бла бла бла
    case SMTP_HELLO_SENT:

        // Не 250? Выходим.
        if(statuscode != 250)
        {
            smtp_quit(id, frame);
            break;
        }

        // Юзаем простую авторизацию
        // Готовим строку вида
        // "\0(юзернейм)\0(пароль)"
        p = buf;
        *(p++) = 0;
        smtp_info(SMTP_USERNAME, &p);
        *(p++) = 0;
        smtp_info(SMTP_PASSWORD, &p);
        num = (uint8_t)(p - buf);

        // Отправляем серверу строку вида "AUTH PLAIN base64(buf)"
        p = dataout;
        smtp_fill_packet_p(&p, PSTR("AUTH PLAIN "));
        p += base64_encode(p, (uint8_t*)buf, num);
        *(p++) = '\r'; *(p++) = '\n';
        tcp_send(id, frame, p-dataout, 0);

        smtp_state = SMTP_AUTH_SENT;
        break;

    // Отправили AUTH, теперь говорим от кого письмо
    case SMTP_AUTH_SENT:

        // На AUTH должен прийти код 235 (или изредка, 250)
        // если что-то другое, скорее всего, простая авторизация
        //    не поддерживается, либо юзернейм/пароль не верны.
        // Тогда выходим
        if( (statuscode != 235) &&
            (statuscode != 250) )
        {
            smtp_quit(id, frame);
            break;
        }

        // Отправляем MAIL FROM:(мыло)
        p = dataout;
        smtp_fill_packet_p(&p, PSTR("MAIL FROM:"));
        smtp_info(SMTP_FROM_EMAIL, &p);
        *(p++) = '\r'; *(p++) = '\n';
        tcp_send(id, frame, p-dataout, 0);

        smtp_state = SMTP_FROM_SENT;
        break;

    // От кого сказали, теперь говорим кому
    case SMTP_FROM_SENT:

        // Должен был прийти код 250 (MAIL FROM успешно принято)
        if(statuscode != 250)
        {
            smtp_quit(id, frame);
            break;
        }

        // Отправляем RCPT TO:(мыло)
        p = dataout;
        smtp_fill_packet_p(&p, PSTR("RCPT TO:"));
        smtp_info(SMTP_TO_EMAIL, &p);
        *(p++) = '\r'; *(p++) = '\n';
        tcp_send(id, frame, p-dataout, 0);

        smtp_state = SMTP_TO_SENT;
        break;

    // Сказали кому, теперь отправляем строку "DATA"
    case SMTP_TO_SENT:

        // Должен был прийти ответ 250 (RCPT TO успешно принято)
        if(statuscode != 250)
        {
            smtp_quit(id, frame);
            break;
        }

        // Отправляем "DATA"
        p = dataout;
        smtp_fill_packet_p(&p, PSTR("DATA\r\n"));
        tcp_send(id, frame, p-dataout, 0);

        smtp_state = SMTP_DATA_SENT;
        break;

    // DATA отправили, теперь отправляем саму дату -
    //    письмо в формате IMF (Internet Message Format)
    case SMTP_DATA_SENT:

        // На "DATA" должен был прийти ответ вида
        // 354 Давайте своё сообщение
        if(statuscode != 354)
        {
            smtp_quit(id, frame);
            break;
        }

        // Формируем сообщение в формате IMF
        p = dataout;
        smtp_fill_packet_p(&p, PSTR("From: "));
        smtp_info(SMTP_FROM_EMAIL, &p);
        smtp_fill_packet_p(&p, PSTR("\r\nTo: "));
        smtp_info(SMTP_TO_EMAIL, &p);
        smtp_fill_packet_p(&p, PSTR("\r\nSubject: "));
        smtp_info(SMTP_SUBJECT, &p);
        smtp_fill_packet_p(&p, PSTR("\r\n"
            "MIME-Version: 1.0\r\n"
            "Content-Type: text/plain; charset=ISO-8859-1; format=flowed\r\n"
            "Content-Transfer-Encoding: 7bit\r\n"
            "\r\n"));
        smtp_info(SMTP_MESSAGE, &p);
        smtp_fill_packet_p(&p, PSTR("\r\n.\r\n"));
        tcp_send(id, frame, p-dataout, 0);

        smtp_state = SMTP_MESSAGE_SENT;
        break;

    // Всё, сообщение отправили, говорим QUIT
    case SMTP_MESSAGE_SENT:

        // Если сервер успешно сожрал сообщение,
        //    должен прийти ответ вида
        //    250 Спасибо
        if(statuscode == 250)
        {
            // Тогда выставляем флаг успешной отправки мыла
            smtp_success = 1;
        }

        // Говорим серверу QUIT
        smtp_quit(id, frame);
        break;

    // QUIT отправлен, сервак должен закрыть соединение
    case SMTP_QUIT_SENT:
        // Оповещаем приложение о статусе завершения отправки
        smtp_done(smtp_success);
        break;
    }
}


Здесь мы использовали простую авторизацию. Прежде чем пытаться использовать определённый сервер с нашим девайсом, стоит убедиться, что сервер поддерживает такой вид авторизации. Узнать это можно, отправив команду EHLO:

Отправляем EHLO

Если в списке возможностей сервера будет что-то наподобие AUTH PLAIN, значит простая авторзация поддерживается.

Теперь само приложение:

// Цепляем библиотеку для DS1820
#include "ds1820.h"

// Адрес и порт сервера
#define EMAIL_SMTP_SERVER        inet_addr(127,0,0,1)    // адрес сервера
#define EMAIL_SMTP_PORT            htons(25)                // порт сервера

// Мыло, с которого будем отправлять сообщения
#define EMAIL_SMTP_USERNAME        "email@example.com"
#define EMAIL_SMTP_PASSWORD        "password"

// Мыло, на которое будем отправлять сообщения
#define EMAIL_TO                "email@example2.com"

// Минимальный интервал для отправки писем
#define EMAIL_MIN_INTERVAL        10*60 // 10 Минут

// Интервал опроса датчика
#define TEMP_POLL_INTERVAL        1500 // 1,5 сек.

// Предел, при превышении которого будем слать мыло
#define TEMP_THRESOLD            40 // 40 градусов


// Идентификатор соединения с сервером
uint8_t email_sending_id = 0xff;

// Количество неудачных попыток отправить мыло
uint8_t email_num_attempts_failed;

// Время следующей попытки
uint32_t email_next_attempt_time;

// Текущая температура
int current_temp;

// Последнее значение, отправленное по мылу
int last_reported_temp;

// UDP пакеты нам пока не нужны
void udp_packet(eth_frame_t *frame, uint16_t len) {}

// TCP соединения тоже не принимаем
uint8_t tcp_listen(uint8_t id, eth_frame_t *frame)
{
    return 0;
}

// Соединение открыто
void tcp_opened(uint8_t id, eth_frame_t *frame)
{
    if(id == email_sending_id)
    {
        // Сбрасываем SMTP клиент
        smtp_reset();
    }
}

// Пришли данные по TCP
void tcp_data(uint8_t id, eth_frame_t *frame, uint16_t len, uint8_t closing)
{
    if(id == email_sending_id)
    {
        // Отдаём их SMTP-клиенту
        smtp_data(id, frame, len);
    }
}

// Соединение закрыто
void tcp_closed(uint8_t id, uint8_t reset)
{
    if(id == email_sending_id)
    {
        // Можно открывать новое
        email_sending_id = 0xff;
    }
}

// Коллбэк SMTP-клиента, отдаём ему разные данные при необходимости
void smtp_info(smtp_info_class_t type, char **buf)
{
    char str[8];

    switch(type)
    {
    // Юзернейм
    case SMTP_USERNAME:
        smtp_fill_packet_p(buf, PSTR(EMAIL_SMTP_USERNAME));
        break;

    // Пароль
    case SMTP_PASSWORD:
        smtp_fill_packet_p(buf, PSTR(EMAIL_SMTP_PASSWORD));
        break;

    // От кого
    case SMTP_FROM_EMAIL:
        smtp_fill_packet_p(buf, PSTR(EMAIL_SMTP_USERNAME));
        break;

    // Кому
    case SMTP_TO_EMAIL:
        smtp_fill_packet_p(buf, PSTR(EMAIL_TO));
        break;

    // Тема письма
    case SMTP_SUBJECT:
        smtp_fill_packet_p(buf, PSTR("Thermo sensor"));
        break;

    // Текст письма
    case SMTP_MESSAGE:
        itoa(current_temp, str, 10);
        smtp_fill_packet_p(buf, PSTR("Temperature reached "));
        smtp_fill_packet(buf, str);
        smtp_fill_packet_p(buf, PSTR("'C !"));
        break;
    }
}

// Сеанс работы с SMTP-сервером завершён
void smtp_done(uint8_t success)
{
    if(!success)
    {
        // Если была ошибка, увеличиваем счётчик ошибок
        email_num_attempts_failed++;
        return;
    }

    // Следующее письмо не раньше чем через указанный интервал
    email_next_attempt_time = rtime() + EMAIL_MIN_INTERVAL;

    // Запоминаем последнюю отправленную температуру
    last_reported_temp = current_temp;

    // Сбрасываем счётчик ошибок
    email_num_attempts_failed = 0;
}

void email_send()
{
    uint16_t local_port;

    // Сеть не готова - выходим
    if(!lan_up())
        return;

    // Если было 5 неудачных попыток,
    //    больше не пытаемся - чтобы не забанили на серваке
    if(email_num_attempts_failed >= 5)
        return;

    // Если в данный момент соединение не открыто
    if(email_sending_id == 0xff)
    {
        // Выбираем случайный локальный порт
        local_port = 1025 + (gettc() & 0x7fff);

        // Пробуем подключиться
        email_sending_id = tcp_open(
            EMAIL_SMTP_SERVER,
            EMAIL_SMTP_PORT,
            htons(local_port));


        if(email_sending_id != 0xff)
        {
            // Следующая попытка не раньше, чем через 30 секунд
            email_next_attempt_time = rtime() + 1*30;
        }
    }
}

// Опрос термодатчика
void temp_poll()
{
    static uint8_t converted = 0;

    if(converted)
    {
        // Показания успешно прочитаны?
        if( (current_temp = ds1820_read()) != 0x7fff )
        {
            // Если значение превышено, и значительно изменилось с
            //    момента предыдущей отправки мыла, и
            //    предыдущее мыло было отправлено достаточно давно,
            //    посылаем новое мыло
            if( (current_temp > TEMP_THRESOLD) &&
                (abs(current_temp - last_reported_temp) >= 5) &&
                (rtime() > email_next_attempt_time) )
            {
                email_send();
            }
        }
    }

    // Запускаем процесс измерения
    converted = ds1820_start();
}

int main()
{
    uint32_t temp_last_poll = 0;

    _delay_ms(20);

    // Тут у нас светодиодик)
    DDRA |= (1<<PA0);
    PORTA |= (1<<PA0);

    // Инициализируем сеть
    lan_init();
    counter_init();
    sei();

    while(1)
    {
        // Ловим пакеты
        lan_poll();

        // Показываем состояние сети светодиодом
        if(lan_up())
            PORTA &= ~(1<<PA0);
        else
            PORTA |= (1<<PA0);

        // Опрашиваем датчик с заданным интервалом
        if(gettc() - temp_last_poll >= TEMP_POLL_INTERVAL)
        {
            temp_last_poll = gettc();
            temp_poll();
        }
    }

    return 0;
}


Греем датчик чем-нибудь горячим и получаем мыло)

Мыло)

Заключение


Скачать проект можно здесь.

В этой части мы вкратце познакомились с TCP и посмотрели мою корявую реализацию этого протокола)

В следующей части попробуем на её основе соорудить небольшой веб-сервер.



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

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

RSS свернуть / развернуть
А сколько памяти поддержка TCP заняла?
0
+1904 байта
0
классно.
0
Гениально
жгите дальше сударь
очень интересно читать
самое крутое это то что все это теперь можно поизучать в одном месте
в сообществе Дихальта
0
Прикольно если понравилось)
По-моему, на этот раз как-то уж очень сумбурно вышло.
Постараюсь исправится)
0
Не могли бы алгоритмы для программ оформить, что бы нужно было сильно программы рассматривать, просто бегло легче алгоритмы рассмотреть, а когда уже реализовывать например начинаешь, то можно и к программам обратится.
0
Дело вкуса. Я например предпочитаю глянуть код, а не диаграмму. Благо, С не асм, на порядок короче и понятней.
0
Вообще должно быть то и то. Если есть алгоритм, то код по нему легко читается, все равно алгоритм написан в виде коментов в коде. Для меня например из всех статей главное идею вынести, а уж запрограммировать сам как нибудь смогу.
0
Ну вроде в начале немножко описал :/
0
как бы plane — плоский, а plain — простой, хотя и произносятся одинаково.
0
Чорт, а ведь правда ))
Поправил)
0
Блин, хоть бы говорили что не нравится :((
0
Пока это для меня голая теория и без железа сильно не получится во все вникнуть. Поэтому сейчас трудно сказать что не понятно и что не нравиться. Но я заказ уже сделал. Жду когда пришлют ENC28J60 доп. с детальками. Сделаю плату и тогда уже и пойдут: непонятки с вопросами. А пока большое спасибо и дальнейших творческих успехов:)))
0
Ну из железа у мя только ENC28J60 (про подключение которой я писал во второй части) и ATmega16, распаяная на платке с минимальной обвязкой. Не показываю потому что у меня там страшный колхоз:) Всё хочу пинбоард купить чтобы наконец-то перестать путаться в этих проводочках:)
0
Пинбоард у меня уже давно есть, но я хочу попытаться замутить что-то подобное на STM32. Пора переходить на ethetnet\internet, а то последовательный rs232+переходники usb-com меня скоро в гроб загонят своей непредсказуемостью, когда необходимо устройство для работы 24/7/365.
0
Ну как бы все нравится :)
0
Lifelover,
Огромное вам спасибо за всю подборку статей. Это просто шедевральная вещь(весь цикл)
Его бы еще как-нибудь в CHM,pdf или еще вочтонибудь завернуть и можно хоть книгу издавать.
ЗЫ маленький вопрос(я пока не все читал) но такой вопрос. Работали ли вы вот с таким контроллером ldd6410.googlecode.com/files/DM9000.pdf
Просто на моей отладочной плате присутствует именно он.
0
Неа, не юзал. (
Но думаю к моему стеку можно любой модуль прицепить. Только написать функции для отправки и приёма пакетов.
0
а как в стеке высчитывается следующий ACK номер?
имею вот такую вот проблему. http://imageshack.us/photo/my-images/62/tcpgx.jpg/
Seq и Ack следующего tcp пакета (ака фрагментация данных) берутся откуда то из неизвестности (seq=4291563789, ack=28000..)
0
  • avatar
  • letni
  • 08 октября 2011, 12:18
Добрый вечер всем. Подскажите пожалуйста, кто-нибудь сталкивался с проблемой в этих исходниках при опросе датчика температуры-, если температура находится в пределе от 0 до +10(вне этого предела температура выводится верно) выводится температура *10, т.е. при вывод на экране 4градуса =40. Если не затруднит, не могли бы помочь в верном решении проблемы, не хочется городить ежика из костылей.
0
Прошу прощения все нормально, сам виноват, оказалось, не очищал экран и оставался разрядов от прошлого значения…сказывается недосып… Вопрос в другом — не могли бы дать объяснения к библиотеке ds1820(а то в ней никаких комментариев, одни магические цифры)
0
Привет!
Уже дня 4 пытаюсь перенести стек на STM32, но возникли проблемы. Сначало, пытался использовать стек с предпоследней статьи (Заключение), UDP пакеты вроде нормально работали, а вот с TCP много проблемы — странички, которые весили больше размера пакета браузер не смог получить а снифер показывал кучу ошибок. Сейчас решил использовать файлы с этой статьи, чтобы более подробно исследовать передачу по TCP и определить источник глюка. Конфигурация:
#define ARP_CACHE_SIZE			3

#define IP_PACKET_TTL			64

#define TCP_MAX_CONNECTIONS		2
#define TCP_WINDOW_SIZE			65535
#define TCP_TIMEOUT				10 //s
#define TCP_SYN_MSS				448

в tcp_data тупо отвечаю на запрос:
void tcp_data(uint8_t id, eth_frame_t *frame, uint16_t len, uint8_t closing)
{
    const char *str = "Stm32F10*";

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

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

при запросе с браузера в нем отображается «Stm32F10*», тоесть, втоде бы все ОК. Если делаю запрос через NC, то ответ тоже приходит (и утилитка закрывается). А вот что в это время видит снифер(192.168.1.200 — комп):

почему посреди пакета Malformated Pacect и в конце вместо FIN|ACK (завершение соединение) снифер видит RST|ACK?
Изначально, при переносе кода на стм32, девайс не хотел даже пинговаться, но после отключения выравнивания структур пинговаться начал нормально. Почему с TCP такая хрень, я не знаю. Очень надеюсь на помощь! Да и думаю, в связи с таким распространением STM32, это будет не только мне полезно)))
Заранее спасибо!
0
Может, просто взять не столь урезанный стек? STM32 позволяет.
0
мне нужно только отдавать web странички, ну и просто передавать данные по TCP. Этот стек понравился тем, что здесь нет ничего лишнего. Сначала, я пробовал uIP, промучался 4 дня, пока сделал передачу файлов с карты, но у меня получилось выжать только 0,5 кб/с, чего оч мало. А что вы можете еще посоветовать?
0
Не сильно в стеках разбираюсь. В Кейле что-то есть, lexanet выкладывал посты с примерами по этой теме.
0
блин, как то не хочется к кейлу возвращается, но наверно придется
0
офф: Народ, а подскажите какой-нить PHY подешовше и доставабельный — к SAM7X прикрутить… Лежить пару штук без дела, может пристрою куда?.. :)
0
Realtek RTL82xx. Конкретный партнамбер не помню. По моему, в каком-то из относящихся к отладкам на жирных кортексах (LPC1768, STM32F2/4) топиков я его упоминал, там же evsi еще какой-то из LANxxxx рекомендовал.
0
Пасиб!
0
RTL8201BL-LF и LAN8720A. К первому должны быть примеры (правда под STM32) в комплекте к TE-STM32F107 который делает Терраэлектроника.
0
Нарыл еще один: KSZ8051MLL. Вот тут есть подробности.
0
Объясните непонятливому — допустим я подцепился к smtp.mail.ru. Он мне в ответ выдал свое приветствие… дальше я хочу убить соединение… как я должен это сделать? Чтонить в духе
tcp_send(id, frame, 0, 1);
?
0
Ога. только лучше не нулевой длины данные, а передать строку QUIT — культурно выйти так сказать.
По сути это делает функция smtp_quit.
0
спасибо
0
Здравствуйте. Собрал устройство. Использовал готовый гейт от ардуины в качестве модуля. Прошил МК, устройство не завелось. Пробовал напрямую от компа к устройству — не отвечает 192.168.0.222. Скажите, может нужно какие-либо фьюзы выставлять и т.д.? Просто хотел попробовать сразу получится сайт или нет (может модуль нерабочий, за 2 недели можно вернуть), а потом уже по порядку идти.
0
Кстати использую Мегу16, кварц на 16МГц. Фьюзы не прошивал.
0
В фузах посмотри выключен ли JTAG и настроено ли на работу с внешним кварцем. А то кварц может и стоит, да работа идет от внутренного генератора.
0
У меня USBasp — по JTAG подсоединиться нечем.
0
Блин… в АТМега16 есть JTAG и по умолчанию он включен — занимает часть порта С. Влиять не должно. но…
Внимательно мое пред сообщение прочитайте…
Также рекомендую проверить соединение линий SPI.
0
В общем фьюзы прошил AVRDUDE -U lfuse:w:0xE0:m -U hfuse:w:0x99:m
Что бы оно от внешнего кварца работало. Соединения тестером проверил все соединено. Выставил компу IP 192.168.0.200 \ 255.255.255.0 \ 192.168.0.1 Комп соединяется, но пинг не проходит, сайт не отвечает. Есть идеи?
0
дежукинг — посмотри на каком моменте все тормозится, проходит ли инциализация стэка и прочее… а так — гадание будет.
0
На пине NSS атмеги что-то висит?
0
Ну только этот модуль для связи с езернетом. Больше ничего. В общем решил по порядку все пробовать. Буду надеяться что модуль рабочий.
0
В проекте Lifelover'а на NSS пине атмеги висит SD-карточка, а SS енки — на соседнем. Если просто выкинуть SD и оставить NSS (PB3 или PB4, не помню точно) висящим и неинициализированным — то SPI будет глючить. Нужно пересадить SS енки на него, либо инициализировать этот пин на выход.
+1
Может подскажите. Написал avrdude -c usbasp -p m16 -U lfuse:w:0xE0:m -U hfuse:w:0x99:m

На www.frank-zhao.com/fusecalc/fusecalc.php?chip=atmega16 посчитал. В итоге теперь мега не определяется программатором. Кварц стоит на 16 МГц. ATMega16-PU (DIP). Не скажите что может быть и как это дело разрулить?
0
Ты переключил на External Clock. Отключай резонатор и подавай тактовый сигнал на CLKIN.
0
Добрый день уважаемые коллеги.
Пробую перевести данную статью на STM32. Компилятор CooCox.
Возникли трудности из-за специфики архитектур.
В библиотеке smpt.c в функции «void smtp_fill_packet_p(char **ptr, prog_char *str)» есть prog_char *str которую я встречаю в первый раз, как и компилятор в CooCox'е. Я так понимаю что это адресация во flash контроллера. Можно ли тогда заменить ее на const char *str.
Так же, в коде полно вот таких функций «smtp_fill_packet_p(&p, PSTR(»QUIT\r\n"));". PSTR — я тоже ни когда не встречал. Это аналог prinf, sprint?
Подскажите, не проходите мимо.
0
PSTR() — строка расположенная во флэш памяти.
0
Здоровья вам GYR22. Спасибо за ссылку, и большой труд.
Из вашего проекта я подчеркнул:
Вы переписали функцию
void fill_buf_P(char **buf, const char * str)
{
uint16_t len = 0;
char *p = *buf;
while(*str)
{ p[len++] = *(str++); }
*buf += len;
}
и грузите в нее массивы типа «const char http_200[] = „HTTP/1.0 200 OK\r\n“;» const — означает что этот массив «железно» прописывается в памяти контроллера. Мои познания в программировании весьма скудны, и я не могу понять как работает ваша функция.
0
да да const здесь флэш, она просто копирует строку из флэша в буфер
зы спасибо за пожелание здоровья ща как раз чето приболел :)
0
Добрый день, использую давно стек этот, весьма неплох. Но использовал обычно как сервер, т.е устройство только отвечало на запрос. Понадобилось открывать самому соединение с устройства и столкнулся с такой проблемой: устройство шлет пакет по TCP с флагом SYN но хост ему не отвечает, хотя на хосте сервер по нужному порту запущен и «слушает». Может подскажете куда копать примерно.
0
фаервол на серваке выключен?
0
Похоже дело не в фаерволле, сейчас сервер отвечает с флагами [SYN, ACK], но устройство ему не отвечает [ACK] и сервер после нескольких повторных [SYN, ACK] дропает соединение.

0
В стэке есть косячок — посмотри, при установке соединения первый пакет имеет Win=65535, а вот ответный 8192, потом опять 8192, а потом 0. А должны идти инкрементом.
0
Немного не понял, что именно должно инкрементироваться? Win это же объем буфера на приемной стороне, т.е сколько байт сервер может за раз скормить клиенту, почему оно должно инкрементироваться
0
Вообщем чего-то там не то было… в подробностях могу ошибаться. Крутилось там все вокруг этих параметров: SEQ, WIN и т.д.
надо вспоминать — давно было.
0
Да, в общем разобрался. Дело в Ack и Seq указателях. В последней версии стека баг, его тов.manitou в комментариях описывает на стр. где описание API

Собственно чтобы устройство открывало соединение к серверу необходимо добавить пару строчек в lan.c:
Этот кусок кода проверяет КАЖДЫЙ пакет и соответственно выдаст ошибку
(lan.c функция tcp_filter() строка приблизительно 602):


if( (ntohl(tcp->seq_num) != st->ack_num) ||
    (ntohl(tcp->ack_num) != st->seq_num) ||
    (!(tcpflags & TCP_FLAG_ACK)) )
        return;

0
решил добавлением условия перед ним:

if(st->status == TCP_ESTABLISHED)
</code


</blockquote>
0
как то странно форум реагирует на ctrl+v когда пытаюсь вставить текст, отсылает сообщение.
0
В общем вот что хотел написать:

Да, в общем разобрался. Дело в Ack и Seq указателях. В последней версии стека баг, его тов. manitou в комментариях описывает на стр.где описание API
Собственно чтобы устройство открывало соединение к серверу необходимо добавить пару строчек в lan.c:
Этот кусок кода проверяет КАЖДЫЙ пакет и соответственно выдаст ошибку
(lan.c функция tcp_filter() строка приблизительно 602). Решается добавлением условия:

if(st->status == TCP_ESTABLISHED){

    if( (ntohl(tcp->seq_num) != st->ack_num) ||
        (ntohl(tcp->ack_num) != st->seq_num) ||
        (!(tcpflags & TCP_FLAG_ACK)) )
        return;
}

Но это не все, если даже мы и пропустим эту проверку, функция все равно
не сохранит правильное значение, полученное от сервера нужно сохранить его (тотже файл, строка приблизительно 638):


    //manitou: set target syn number (not allways 0)
        st->ack_num = htonl(tcp->seq_num)+1;
        //end manitou


После этого все заработало
0
В общем пока остановился на том что когда от сервера приходит [SYN, ACK] вот в этом месте в функции tcp_filter указатель st всегда равен нулю, и принятый пакет не попадает куда положено а считается что соединение не найдено.

// Нет такого соединения
    if(!st)
    {
        // Получили SYN - запрос открытия соединения
        //  (пассивное открытие, шаг 1)
        if( tcpflags == TCP_FLAG_SYN )
        {
            // Ищем свободный слот для соединения
            for(id = 0; id < TCP_MAX_CONNECTIONS; ++id)
            {
                pst = tcp_pool + id;

                if(pst->status == TCP_CLOSED)
                {
                    st = pst;
                    break;
                }
            }

            // Слот найден и приложение подтверждает открытие соединения?
            if(st && tcp_listen(id, frame))
            {
                // Добавляем новое соединение
                st->status = TCP_SYN_RECEIVED;
                st->rx_time = rtime();
                st->remote_port = tcp->from_port;
                st->local_port = tcp->to_port;
                st->remote_addr = ip->from_addr;

                // Отправляем SYN/ACK
                //    (пассивное открытие, шаг 2)
                tcp->flags = TCP_FLAG_SYN|TCP_FLAG_ACK;
                tcp_step(tcp, 1);
                tcp->seq_num = gettc() + (gettc() << 16);
                tcp_reply(frame, 0);
            }
        }
    }

    else
    {
        st->rx_time = rtime();

        switch(st->status)
        {

        // Был отправлен SYN (активное открытие, пройден шаг 1)
        case TCP_SYN_SENT:

            // Если полученный пакет не SYN/ACK (RST, etc.)
            // прибиваем соединение

0
Не подскажете что делает данная строка:
pst = tcp_pool + id;


tcp_pool это массив, id это переменная uint8_t
0
Это pst = tcp_pool[id]. И вообще, если ты не знаешь столь базового синтаксиса С, тебе следует не стек ковырять, а K&R читать.
-1
Надож когда то начинать чтото ковырять, почему не стек. Тем более если мне нужно чтоб он у меня заработал. А насчет синтаксиса я просто не встречал такой конструкции, когда к массиву что то прибавляется. Компилятор windows считает такую конструкцию бессмысленной и выбрасывает предупреждение вроде: «данная операция ничего не делает» хотя и не считает ошибкой.
0
Если ты результат прибавления никуда не присвоишь — бесполезная. А вообще-то это базовый синтаксис С, указательная арифметика (имя массива является указателем на его начало, и вообще, разница между типизированным указателем и массивом в С практически отсутствует).
P.S. Что за «компилятор Windows»?
0
а ну не так выразился, я в дебаггере с visual studio прогнал пример этот
0
Хинт «операция ничего не делает» ничего не говорит о самой операции. Он говорит о том, что в данном коде ее безболезненно можно выкинуть — например, если ты усердно что-то считаешь, но посчитанное значение нигде не используешь — оптимизатор выкинет все эти расчеты и напишет «данные операции ничего не делали».
Но в коде стека оно явно используется, так что там компилятор ругаться не должен.
0
Понял ))
0
Это pst = tcp_pool[id]
Нет, ведь в исходном выражении нет разадресации, чистая арифметика с указателями (если я правильно понял смысл из фрагмента кода). Это можно записать как pst = &tcp_pool[id]

Другими словами, pst это указатель на элемент массива tcp_pool с индексом id
+1
*постучался головой о стену и ушел перечитывать K&R*
0
Верно, здесь только добавление смещения к указателю:
pst = tcp_pool + id;
а дальше по коду идёт разыменование указателя:
if(pst->status == TCP_CLOSED)
а именно доступ к полю структуры по указателю
0
Почему-то после сброса/включения питания при срабатывании условия, когда требуется отправлять письмо оно не отправляется, а отправление возможно только со второго раза.
0
Нашел ошибку в библиотеке TCP, приложенной к этой статье. В расширенной версии стека (в заключительных статьях цикла) эту ошибку исправили, но видимо добавили лишнего и заставить работать ее не удалось.
Но в моем проекте хватит и урезанной версии TCP, которая заработала сразу, но стабильно отваливалась через сутки работы.
Проблема оказалась в lan.c (строка 725) в функции void tcp_poll():
if( (st->status != TCP_CLOSED) &&
    (rtime() - st->rx_time > TCP_TIMEOUT) )
При этом rtime() — uint32_t, а st->rx_time — uint16_t, соответственно после 65535с/18ч работы условие становилось истинным всегда и tcp_poll()сходу прибивал все соединения, не давая установить связь.
для лечения надо исправить uint16_t rx_time на uint32_t rx_time в строке 183 в файле lan.h:
typedef struct tcp_state {
	tcp_status_code_t status;
	uint32_t rx_time;
	uint16_t remote_port;
	uint16_t local_port;
	uint32_t remote_addr;
} tcp_state_t;

Эта же проблема, я так понимаю, была и у azimut4nt в сообщениях выше.
И, спасибо Lifelover за полезную либу и шикарное описание TCP/IP!
0
При желании сэкономить пару байт можно просто обрезать возвращаемое rtime() время до 16 бит.
0
Тоже вариант, но решил не обрезать, ибо во-первых автор в финальной версии как раз uint32_t сделал, а во-вторых вдруг приспичит соединение сутками поддерживать :)
0
В смысле, таймаут более 64К секунд? Как-то больно сурово, как по мне.
0
Вообще да, оно же при каждом обмене обновляется.
По идее и байта бы хватило, а то что это за клиент который по 5 минут молчит.
0
ОС Windows и ОС Linux могут держать таймаут 1-3 минуты (в зависимости от текущей фазы обмена и линий связи). При этом 1 минута это вполне адекватное (и чуть ли не минимальное) для них время для 100Мбит/с подключения, ведь удалённая сторона может быть и по GPRS подключена.
С другой стороны большой таймаут будет проблемой при обрывах и попытках повторной установки связи (исчерпание свободных слотов для подключения).
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.