STM Урок 95. LAN. W5500. FTP Server. Часть 3



В предыдущей части занятия мы написали функцию разбора строки команды FTP, а такаже обработали несколько команд для адкеватного ответа на них клиенту.

Давайте теперь ответим клиенту на команду PASV. Вернее пока начнём отвечать, потому что это не совсем просто и проделаем эту процедуру постепенно. Для этого сначала подключим глобальную переменную

extern uint8_t ipaddr[4];

extern uint16_t local_port_ftp_data_passiv;

Затем перейдём в файл w5500.c и напишем функцию закрытия сокета после функции SocketClosedWait

//-----------------------------------------------

void CloseSocket(uint8_t sock_num)

{

  uint8_t opcode;

  opcode = (((sock_num<<2)|BSB_S0)<<3)|OM_FDM1;

  w5500_writeReg(opcode, Sn_CR, 0x10); //CLOSE SOCKET

}

//-----------------------------------------------

Добавим прототип на данную функцию, а заодно ещё на некоторые в заголовочном файле w5500.h

void SetReadPointer(uint8_t sock_num, uint16_t point);

void CloseSocket(uint8_t sock_num);

uint8_t GetSocketStatus(uint8_t sock_num);

void SetSockPort(uint8_t sock_num, uint16_t port);

Вернёмся в наш switch в функцию ftp_cmd_parse файла ftpd.c и начнём писать реакцию сервера на команду клиента PASV

  break;

case PASV_CMD:

  local_port_ftp_data_passiv++;

  if(local_port_ftp_data_passiv > 50000) local_port_ftp_data_passiv=35000;

  break;

default: // Invalid

Назначим адрес порта сервера для соединения передачи данных. Мы данное значение будем инкрементировать при каждом следующем соединении, которое будет происходить по той же команде клиена, чтобы адреса порта были разные. Если адрес порта достигнет 50000, то процесс возобновится.

Далее мы ответим клиенту сообщением с адресом IP и адресом порта. сначала мы передаём старший байт адреса порта, а затем младший

if(local_port_ftp_data_passiv > 50000) local_port_ftp_data_passiv=35000;

len = sprintf((char*)datasect->data, "227 Entering Passive Mode (%d,%d,%d,%d,%d,%d)rn",

  ipaddr[0], ipaddr[1], ipaddr[2], ipaddr[3], local_port_ftp_data_passiv >> 8, local_port_ftp_data_passiv & 0x00ff);

tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

break;

Но недостаточно нам просто передать команду. Мы должны обеспечить работу соединения для передачи данных.

Поэтому этим мы и займёмся

tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

//Разъединим сокет для данных и заново его подключим

if(GetSocketStatus(FTP_SOCKET_DATA)==SOCK_ESTABLISHED)

  DisconnectSocket(FTP_SOCKET_DATA); //Разъединяемся

CloseSocket(FTP_SOCKET_DATA);

SocketClosedWait(FTP_SOCKET_DATA);

HAL_UART_Transmit(&huart2,(uint8_t*)"654321rn",8,0x1000);

sprintf(str1,"S%d closedrn",FTP_SOCKET_DATA);

HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);

SetSockPort(FTP_SOCKET_DATA, local_port_ftp_data_passiv);

OpenSocket(FTP_SOCKET_DATA,Mode_TCP);

//Ждём инициализации сокета (статус SOCK_INIT)

SocketInitWait(FTP_SOCKET_DATA);

//Продолжаем слушать сокет

ListenSocket(FTP_SOCKET_DATA);

SocketListenWait(FTP_SOCKET_DATA);

ftpprop.datasock_mode = PASSIVE_MODE;

ftpprop.datasock_state = DATASOCK_READY;

sprintf(str1,"PASV port: %drn", local_port_ftp_data_passiv);

HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);

break;

Тут думаю всё ясно. Если соединение то разрываем его и соединяемся заново, используя уже новый адрес порта.

С обработкой ответа вроде всё.

Но теперь нам нужно как-то обрабатывать пакеты данных в соединении для передачи данных. Но, в принципе, мы этим заняться ещё успеем.

Соберём код, прошьём контроллер и ещё раз попробуем соединиться

Image16

Здесь уже картина следующаяя. Ответ наш клент получил, соединение у нас создалось, клиент нам передаёт следующую команду LIST, которая означает просьбу клиента передать подробную информацию о текущем каталоге. Мы такую информацию передавать пока не умеем, а передаётся она по соединению уже для передачи данных. Поэтому сервер отправил клиенту сообщение, что такую команду он не знает, на что клиент попросил закрыть соединение.

Поэтому следующая задача — обработать команду от клиента LIST.

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

  break;

case QUIT_CMD :

  len = sprintf((char*)datasect->data, "221 Goodbye!rn");

  tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

  DisconnectSocket(sn); //Разъединяемся

  SocketClosedWait(sn);

  sprintf(str1,"S%d closedrn",sn);

  HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);

  OpenSocket(sn,Mode_TCP);

  //Ждём инициализации сокета (статус SOCK_INIT)

  SocketInitWait(sn);

  //Продолжаем слушать сокет

  ListenSocket(sn);

  SocketListenWait(sn);

  ftpprop.connect_stat = FTP_DISCONNECT;

  break;

default: // Invalid

Мы шлём нужное сообщение клиенту и разъединяемся с ним, затем заново открываем сокет и слушаем его, чтобы быть всегда в готовности создать новое соединение по желанию клиента, не обязательно того же самого.

Теперь начнём работать с командой LIST, также наряду с ней есть подобная команда MLSD, которую тоже может запросить сервер, поэтому её также обработаем

  break;

case MLSD_CMD:

  len = sprintf((char*)datasect->data, "150 Opening data channel for directory listing of "%s"rn", ftpprop.work_dir);

  tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

  ftpprop.current_cmd = MLSD_CMD;

  break;

case LIST_CMD:

  sprintf(str1,"LIST_CMDrn");

  HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);

  len = sprintf((char*)datasect->data, "150 Opening data channel for directory listing of "%s"rn", ftpprop.work_dir);

  tcp_send_ftp_one(sn, (uint8_t *)buf_send, len);

  ftpprop.current_cmd = LIST_CMD;

  break;

case QUIT_CMD :

Здесь мы шлём соответствующие сообщения клиенту. Они абсолютно одинаковые. Также в соответствующее поле структуры мы занесём индекс текущей команды.

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

Но хотим мы или не хотим этого, данную информацию мы должны как-то предоставить. В данный момент у нас текущий каталог корневой. Да оно и не важно какой у нас текущий каталог. Мы же могли запросить с клиента любой, написав путь к нему в свойствах соединения.

Начнём потихоньку работать с информацией, которую мы будем передавать.

Пока что перейдём в функцию ftp_receive в тело условия наличия пакета в сокете данных и в случае, если у нас не установлен флаг наличия соединения в поле структуры, то установим его

else if(sn == FTP_SOCKET_DATA)

{

  if(!ftpprop.connect_stat_data)

  {

    sprintf(str1,"FTP Data socket Connectedrn");

    HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);

    ftpprop.connect_stat_data = 1;

  }

}

Затем мы добавим определитель типа текущей команды, выйдя только из тела одного условия

      ftpprop.connect_stat_data = 1;

    }

    switch(ftpprop.current_cmd)

    {

      case LIST_CMD:

      case MLSD_CMD:

      case RETR_CMD:

        if(ftpprop.data_stat==DATA_COMPLETED)

        {

          ftpprop.data_stat=DATA_FIRST;

        }

        break;

      default:

        break;

    }

  }

}

Мы обработали сразу несколько команд, так как обработчик одинаковый. В обработчике мы узнали статус передачи, и затем, если у нас нет непереданных данных, установили статус передачи данных в статус передачи первого пакета. В отличие от HTTP, здесь у нас не будет отличаться, первый ли он или единственный. Это мы уже будем определять по мере обработки пакета. Также в этом условии у нас будет команда вызова определённой функции, которой у нас пока ещё нет. Поэтому давайте ей добавим. Это будет функция передачи данных клиенту по сокету для передачи данных. Добавим мы её после функции ftp_cmd_parse

//-----------------------------------------------

void ftp_data_send(uint8_t sn)

{

}

//-----------------------------------------------

 

 

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

  ftpprop.data_stat=DATA_FIRST;

  ftp_data_send(sn);

}

break;

В заголовочном файле w5500.h добавим макрос размера окна

#define SS_DESELECT() HAL_GPIO_WritePin(CS_GPIO_PORT, CS_PIN, GPIO_PIN_SET)

//--------------------------------------------------

#define SIZE_WND 2048

Вернёмся в нашу функцию передачи данных клиенту ftp_data_send в файл ftpd.c и начнём писать её тело

void ftp_data_send(uint8_t sn)

{

  static char buf_send[SIZE_WND+3];

  FRESULT res; //результат выполнения

  buf_send[0] = '';

  switch(ftpprop.current_cmd)

  {

    case LIST_CMD:

    case MLSD_CMD:

      break;

    default:

      break;

  }

}

 

Пока мы будем обрабатывать только текущие команды запроса информации а каталоге.

Добавим несколько глобальных переменных для работы с библиотекой FATFS

extern volatile uint16_t tcp_size_wnd;

static FILINFO fileInfo;

static DIR dir;

extern uint32_t bytesread;

Вернёмся в нашу функцию и проинициализируем некоторые указатели в обработчике команд передачи информации о текущем каталоге

case LIST_CMD:

case MLSD_CMD:

  fileInfo.lfname = (char*)sect;

  fileInfo.lfsize = sizeof(sect);

  break;

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

//-----------------------------------------------

//функция чтения и передачи позиций каталога

void ftp_send_dirinfo(uint8_t sn, char *buf)

{

  FRESULT res; //результат выполнения

  uint16_t len;

  char str_date[15];

  int str_date_ptr = 0, buf_ptr = 0;

  uint16_t buf1_ptr = 0;

  char *fn;

  fileInfo.lfname = (char*)sect;

  fileInfo.lfsize = sizeof(sect);

}

//-----------------------------------------------

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

extern uint8_t sect[515];

static char buf1[256];

Думаю, такого размера для информации о позиции нам хватит. Также для того, чтобы наш буфер не развалился к нашему следующему приходу в функцию, то добавим к нему аттрибут static.

Продолжим писать тело нашей функции

fileInfo.lfsize = sizeof(sect);

buf_ptr+=3;//оставим место под ардес и опкод

//если есть оставшаяся информация о позиции в каталоге, то передадим её

if(ftpprop.rem_bytes_dirinfo)

{

  memcpy((uint8_t*)(buf+buf_ptr),(uint8_t*)(buf1+strlen(buf1)-ftpprop.rem_bytes_dirinfo),ftpprop.rem_bytes_dirinfo);

  buf_ptr+=ftpprop.rem_bytes_dirinfo;

}

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

Далее цикл

  buf_ptr+=ftpprop.rem_bytes_dirinfo;

}

while(1)

{

  buf1_ptr = 0;

  res = f_readdir(&dir, &fileInfo);

}

В цикле мы устанавливаем указатель буфера для текущей позиции на ноль и читаем информацию о текущей позиции. С данной функцией мы работали и прекрасно знаем, что она сама переключает указатель на следующую позицию после повторного её вызова.

Работаем дальше с циклом

res = f_readdir(&dir, &fileInfo);

if (res==FR_OK && fileInfo.fname[0])

{

  fn = fileInfo.lfname;

  if(!strlen(fn)) fn=fileInfo.fname;

}

else

{

  break;

}

Мы сначала определяем имя файла (каталога). Если оно длинное, то оно уже определяется несколько по-другому, мы также знаем как это делается, поэтому это мы здесь также учли.

 

 

Не выходим из цикла и пишем код дальше

 

  break;

}

switch((fileInfo.fdate >> 5) & 0x0f)

{

  case 1:

    len = sprintf(str_date, "JAN ");

    break;

  case 2:

    len = sprintf(str_date, "FEB ");

    break;

  case 3:

    len = sprintf(str_date, "MAR ");

    break;

  case 4:

    len = sprintf(str_date, "APR ");

    break;

  case 5:

    len = sprintf(str_date, "MAY ");

    break;

  case 6:

    len = sprintf(str_date, "JUN ");

    break;

  case 7:

    len = sprintf(str_date, "JUL ");

    break;

  case 8:

    len = sprintf(str_date, "AUG ");

    break;

  case 9:

    len = sprintf(str_date, "SEP ");

    break;

  case 10:

    len = sprintf(str_date, "OCT ");

    break;

  case 11:

    len = sprintf(str_date, "NOV ");

    break;

  case 12:

    len = sprintf(str_date, "DEC ");

    break;

}

Данный участок кода, думаю, понятен. Мы берём информацию из структуры информации о файле (каталоге) о месяце создания файла (каталога) и заносим её в строку в формате, понятном клиенту.

Пишем дальше код в цикле, не выходя из него

  break;

}

str_date_ptr += len;

len = sprintf(str_date + str_date_ptr, "%d ", (fileInfo.fdate & 0x1f));

str_date_ptr += len;

len = sprintf(str_date + str_date_ptr, "%d", (((fileInfo.fdate >> 9) & 0x7f) + 1980));

str_date_ptr = 0;

if(fileInfo.fattrib & AM_DIR)

{

  sprintf(buf1, "d");

}

else

{

  sprintf(buf1, "-");

}

Здесь мы продвигаемся по буферу, предназначенному исключительно для строкового представления даты, затем заносим в данный буфер само число месяца. Далее мы обнуляем счётчик буфера, и затем уже начинаем заполнять буфер самой позиции. Начинаем мы его заполнять с информации о типе позиции — файл это или каталог.

Пишем дальше тело нашего цикла

  sprintf(buf1, "-");

}

buf1_ptr++;

len = sprintf(buf1 + buf1_ptr, "rwxr-xr-x 1 ftp ftp %lu %s %srn", fileInfo.fsize, str_date, fn);

buf1_ptr += len;

Мы продвигаемся по буферу на одну позицию и начинаем его заполнять соответственно стандарту, определяя права клиента на файл. Передадим стандартные права, затем передадим строку «ftp ftp», также являющуюся обязательной, а затем уже информацию о размере файла и заренее подготовленную нами ранее строку со строковым представлением даты. Ну и передвинем указатель на размер переданных в буфер данных.

Продолжаем тело цикла далее

buf1_ptr += len;

if((buf_ptr+buf1_ptr)>(SIZE_WND+3))

{

  memcpy((uint8_t*)(buf+buf_ptr),(uint8_t*)buf1,SIZE_WND+3-buf_ptr);

}

else

{

  memcpy((uint8_t*)(buf+buf_ptr),(uint8_t*)buf1,buf1_ptr);

}

buf_ptr += buf1_ptr;

Соответственно, здесь мы уже буфер со строкой для позиции копируем в большой буфер, который мы готовим для записи в буфер на отправку клиенту, равный размеру окна. И тут мы как раз и определяем, достигли ли мы предельного размера главного буфера сокета. Если достигли, то копируем в него только ту часть позиции, которая в него залезет как говорится под завязку. А если не достигли, то копируем весь. Затем опять передвигаемся по главному буферу.

Далее пишем наш цикл. Это будет завершающий код нашего цикла

  buf_ptr += buf1_ptr;

  if(buf_ptr>(SIZE_WND+3))

  {

    ftpprop.rem_bytes_dirinfo = buf_ptr-SIZE_WND-3;

    ftpprop.data_stat=DATA_MIDDLE;

    break;

  }

  else

  {

    ftpprop.rem_bytes_dirinfo = 0;

  }

}

Здесь при достижении окончания буфера мы определяем и устанавливаем значение поля оставшихся байтов для передачи текущей позиции, а также меняем статус передачи на статус передачи средней части всего объёма данных. Пока мы не знаем будет ли она действительно средней или же будет последней. Также не забываем про три информационных байта. Мы это определим потом. Ну а если не достигли окончания буфера, то тогда обнуляем счётчик.

Теперь выходим из бесконечного цикла и пишем код дальше. Нам осталось теперь наш заполненный буфер передать клиенту

      ftpprop.rem_bytes_dirinfo = 0;

    }

  }

  if(buf_ptr>(SIZE_WND+3))

  {

    buf_ptr=SIZE_WND+3;

  }

  else

  {

    ftpprop.data_stat=DATA_END;

    f_closedir(&dir);

  }

  tcp_send_ftp_one(sn, (uint8_t *)buf, buf_ptr-3);//отнимаем из общей длины служебные байты

}

Здесь мы передвигаем указатель в буфере на самое его окончание при достижении максимального размера. В противном случае мы его значение не трогаем, так как он не превышен и закрываем каталог. Также в противном случае мы статус передачи данных устанавливаем в окончание. И затем мы передаём буфер клиенту.

Все эти статусы мы обработаем позже, а пока вернёмся в нашу функцию передачи данных клиенту ftp_data_send и продолжим писать там обработчик команд клиента на запрос содержимого каталога

fileInfo.lfsize = sizeof(sect);

if(ftpprop.data_stat==DATA_FIRST)

{

  res = f_opendir(&dir, ftpprop.work_dir);

  if(res == FR_OK)

  {

    ftp_send_dirinfo(sn,buf_send);

    if(ftpprop.data_stat==DATA_END)

    {

      f_closedir(&dir);

    }

  }

}

else if(ftpprop.data_stat==DATA_MIDDLE)

{

  ftp_send_dirinfo(sn,buf_send);

}

break;

Здесь при условии, что у нас это первый пакет, мы открываем каталог. Если каталог успешно открылся, то мы вызываем нашу функцию передачи информации о содержимом каталога и затем, если у нас установлен статус окончания передачи данных, закрываем каталог. В противном случае, если у нас статус передачи данных среднего пакета, мы уже не открываем каталог, подразумевается, что у нас он уже открыт ранее и продолжаем передавать данные с информацией об очередных позициях каталога, для чего вызовем соответствующую функцию. Пока с этой функцией передачи данных всё.

Вернёмся теперь в функцию приёма и обработки пакетов FTP ftp_receive и в теле условия поступления пакета по соединению для передачи данных выше переключателя текущей команды добавим ещё один переключатель статусов передачи данных и обработаем в нём два статуса

  ftpprop.connect_stat_data = 1;

}

switch(ftpprop.data_stat)

{

  case DATA_END:

    ftpprop.current_cmd = NO_CMD;

    len = sprintf((char *)(sect+3), "226 Successfully transferred "%s"rn", ftpprop.work_dir);

    tcp_send_ftp_one(FTP_SOCKET_CTRL, sect, len);

    ftpprop.data_stat=DATA_COMPLETED;

    DisconnectSocket(FTP_SOCKET_DATA); //Разъединяемся

    SocketClosedWait(FTP_SOCKET_DATA);

    sprintf(str1,"S%d closedrn",FTP_SOCKET_DATA);

    HAL_UART_Transmit(&huart2,(uint8_t*)str1,strlen(str1),0x1000);

    OpenSocket(FTP_SOCKET_DATA,Mode_TCP);

    //Ждём инициализации сокета (статус SOCK_INIT)

    SocketInitWait(FTP_SOCKET_DATA);

    //Продолжаем слушать сокет

    ListenSocket(FTP_SOCKET_DATA);

    SocketListenWait(FTP_SOCKET_DATA);

    ftpprop.connect_stat_data = 0;

    break;

  case DATA_MIDDLE:

    ftp_data_send(sn);

    break;

}

switch(ftpprop.current_cmd)

В случае статуса окончания передачи мы закрываем сокет, переинициализируем определённые поля структуры, передаём определённое сообщение клиенту, что данные переданы, чтобы клиент знал, что данные мы ему все передали, открываем сокет и заново начинаем его слушать. А в случае статуса передачи средней части вызываем заново функцию передачи данных.

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

Соединимся с сервером и посмотрим результат

Image17

Вот и результат. Мы видим всю файловую систему флеш-карты.

А вот информация из WireShark

Image18

Видим, что всё передано.

Теперь наполним карту ещё информацией и посмотрим результат

Image20

Также вся информация отображается и приходит. Вот результат в WireShark, где мы видим отправку следующего пакета, который не уместился в буфер

Image21

Наполним ещё больше наш накопитель информацией.

А вот и результат

Image22

Работаем дальше с FTP. Попробуем открыть какой-нибудь каталого двойным щелчком мыши

Image23

Каталог нормально открылся. Посмотрим информацию в анализаторе трафика

Image24

Здесь также всё нормально. Клиент и сервер нормально общаются и адекватно реагируют на сообщения и команды.

Теперь пойдём дальше. Попробуем вернуться из нашего каталога на уровень выше, то есть выйти из него

Image25

В этом случае происходит потеря соединения. Причину такого разрыва мы найдём в следующей части нашего занятия, в которой мы также научимся создавать и удалять файлы и каталоги, а также передавать файлы на сервер и с сервера.

 

 

Предыдущая часть Программирование МК STM32 Следующая часть

 

 

Отладочную плату можно приобрести здесь Nucleo STM32F401RE

и здесь Nucleo STM32F401RE

Ethernet LAN Сетевой Модуль можно купить здесь W5500 Ethernet LAN

Переходник USB to TTL можно приобрести здесь USB to TTL ftdi ft232rl

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

STM LAN. W5500. FTP Server

2 комментария на “STM Урок 95. LAN. W5500. FTP Server. Часть 3
  1. Ашот:

    Добрый день! Прошёл данную часть урока и столкнулся с тем что TC видит корневой каталог, но открыть вложенные каталоги не может, в чем может быть проблема?

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*