ESP32 скачивает файлы с FTP сервера.

5N62V
Offline
Зарегистрирован: 25.02.2016

Ну это что хотелось бы. Пока нифига не скачивает. :)

Всем бобра!

Столкнулся я с задачей (нет, чтоб диодами мигать, что ближе к моему уровню! ): есть аудио плеер на ESP32 - он по кругу проигрывает плейлист с SD карты. По Вифи он подключен к модему. Где-то в просторах и-нета есть FTP сервер с плейлистом. И если на FTP добавить файл, то ЕСПшка должна его скачать к себе на SD.

Полез в и-неты, нашел FTP клиент:  https://github.com/ldab/ESP32_FTPClient   , но этот вариант больше заточен на сохранение файлов на ftp, а скачивать может файлы только до 90кб, как утверждает автор.  Ну, я , окрыленный недавними успехами по допиливанию другого прокта ( линк ) под свои нужды , думаю ща я допилю функцию скачивания, и все будет ок. Ага. Там подчинение и наследование, а один метод я вообще найти не могу, хотя пример компилится без проблем. Печаль.

Дай думаю спрошу , мож кто чего посоветует:

1) а может мне ftp клиент нафиг не нужен, и организовать скачивание файлов до 10М можно и иным способом? Сложность, на мой взгляд, в том, что перед скачиванием надо получить список файлов, сравнить его с тем, что на карте, и тогда уже скачивать. Т.е. какой-то скрипт должен этот список высылать по запросу ESP-шки...

2) Правильно ли я понимаю, что скорее всего лимит в 90кБ скорее всего связан с тем, что автор скачивал и хранил файл в оперативке? А по идее его надо скачивать в буфер, а буфер потом скидывать на карту?

Спасибо заранее!

negavoid
Offline
Зарегистрирован: 09.07.2016

1) может. можно и по http, да или хоть своим протоколом, но да, нужен будет скриптик со списком для сравнения. но это как бы не проблема ))

<?php echo implode( "\r\n", array_diff( scandir( "mp3" ), [ ".", ".." ] ) );

2) Правильно понимаете. Так и надо. Только буфер - он в той же самой оперативке. А sd-картридеры бывают разные. Вот автор и не стал заморачиваться, написал бы он сохранение файла для одного - так его бы начали долбать "а напиши для другого". А это не совсем правильно, ftp - отдельно, sd - отдельно. А соединяет их вместе пусть уже тот, кто делает проект с тем и другим.

Так что, всё везде правильно поняли.

5N62V
Offline
Зарегистрирован: 25.02.2016

negavoid пишет:

1) может. можно и по http, да или хоть своим протоколом, но да, нужен будет скриптик со списком для сравнения. но это как бы не проблема ))

<?php echo implode( "\r\n", array_diff( scandir( "mp3" ), [ ".", ".." ] ) );

Спасибо за ответ. Эта строчка - содержимое файла index.php ,  который должен лежать в папке с контентом?

negavoid
Offline
Зарегистрирован: 09.07.2016

Эта строчка - содержимое index.php, который должен лежать рядом с папкой "mp3" с контентом. Если хотите поместить его внутрь папки "mp3", добавьте его к массиву, чтобы себя не выводил:   "..", "index.php" ] и делайте scandir( "." )

<?php echo implode( "\r\n", array_diff( scandir( "." ), [ ".", "..", "index.php" ] ) );

 

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

negavoid пишет:

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

Для этого достаточно описать интерфейс, и всё. И реализовать первой имплементацией этого интерфейса - сохранение файла с FTP в оперативку. Чуть-чуть доки - и все, кто захотят, смогут реализовать другие имплементации этого интерфейса, и всё. Типа такого:

struct IDataSaver
{
  virtual bool BeginSave(const char* fileName) =0;
  virtual size_t WriteDataChunk(uint8_t* chunk, size_t sz) = 0;
  virtual void EndSave() = 0; 
};

 

5N62V
Offline
Зарегистрирован: 25.02.2016

DIYMan пишет:

Для этого достаточно описать интерфейс, и всё. И реализовать первой имплементацией этого интерфейса - сохранение файла с FTP в оперативку. 

Это, вроде бы, понятно из реализации метода скачивания:

void ESP32_FTPClient::DownloadFile(const char * filename, unsigned char * buf, size_t length, bool printUART ) {
  FTPdbgn("Send RETR");
  if (!isConnected()) return;
  client.print(F("RETR "));
  client.println(F(filename));

  char _resp[ sizeof(outBuf) ];   // в хедере определено char outBuf[128];
  GetFTPAnswer(_resp);

  char _buf[2];

  unsigned long _m = millis();
  while ( !dclient.available() && millis() < _m + timeout) delay(1);

  while (dclient.available())
  {
    if ( !printUART )
      dclient.readBytes(buf, length);

    else
    {
      for (size_t _b = 0; _b < length; _b++ )
      {
        dclient.readBytes(_buf, 1),
                          Serial.print(_buf[0], HEX);
      }
    }
  }
}

У меня вот в чем загвоздка:  я так понимаю, что строками

client.print(F("RETR ")); 

client.println(F(filename));

запрашивается у сервера файл на скачивание.  Мне же нужна только его часть, которая поместится в буфер, т.к. потом его надо слить на карту, а это - время. Может кто-нить из знающих знает как запросить у сервера   кусок файла нужного размера и с нужного адреса? И есть ли такая возможность вообще? 

UPD. Видимо я приплыл: среди комманд FTP ничего похожего нет :(((

https://en.wikipedia.org/wiki/List_of_FTP_commands

DIYMan
DIYMan аватар
Offline
Зарегистрирован: 23.11.2015

Читайте про команду REST - Restart transfer from the specified point.

Алексей.
Алексей. аватар
Offline
Зарегистрирован: 02.02.2018

5N62V пишет:
UPD. Видимо я приплыл: среди комманд FTP ничего похожего нет :(((

https://en.wikipedia.org/wiki/List_of_FTP_commands

Читаем то что изложено по приведенной вами ссылке:
REST - Restart transfer from the specified point.
Ещё ссылку на рфц 3659 дают, а там пример как файл начинают читать с позиции 802816
Кроме этого REST присутствовал в RFC 959 раздел 4.1.3. FTP SERVICE COMMANDS

5N62V
Offline
Зарегистрирован: 25.02.2016

Ну типа гляжу в книгу вижу фигу. :) Тогда по идее надо запускать с определенного адреса командой REST addr  и сразу RETR  filename, и когда буфер заполнился - слать комманду ABOR для остановки передачи.  Правильно понимаю?

  Assume that the transfer of a largish file has previously been   interrupted after 802816 octets had been received - Это че за октеты? 

UPD. ага - байты и октеты - это синонимы. 

Хм .  А у REST в аргументах только адрес, и боюсь это адрес не начала файла, иначе бы имя файла тоже было бы в аргументах. 

Алексей.
Алексей. аватар
Offline
Зарегистрирован: 02.02.2018

Октет - восемь двоичных единиц информации умещаются в один байт, который может принимать десятичном исчислении значения от 0 до 255

5N62V
Offline
Зарегистрирован: 25.02.2016

Протрахался я с ftp- клиентом пару дней. Реализацию функции скачивания файла с FTP  (изначальный код в посте №5) я переписал так, чтобы полученные байты сразу писались на карту памяти, без буферизации. 

void myESP32_FTPClient::DownloadFileSD(const char * filename) {

  uint32_t counter = 0;
      file = SD.open("/lastTest.mp3", FILE_WRITE);
    if(!file){
        Serial.println("Failed to open file for writing");
        return;
    }
  FTPdbgn("Send RETR");
  if(!isConnected()) return;
  client.print(F("RETR "));
  client.println(F(filename));
  
  char _resp[ sizeof(outBuf) ];    
  GetFTPAnswer(_resp);
  
  unsigned long _m = millis();
  while( !dclient.available() && millis() < _m + timeout) delay(1);

  while(dclient.available()) 
  {
      file.write((uint8_t)dclient.timedRead());//timedRead()-  метод класса Stream, сделал его public
      //dclient.timedRead();
      counter++;
  }
  file.close();
  Serial.print("Bytes read -");Serial.println(counter);
}

И - не работает. :( файлы короче, чем положено. Думал пропуск данных, задрал тактовую частоту SPI со штатных 4МГц до 15МГц - все равно короче. Я потом на карточке по файлу случайно тыцнул - а он играет как положено, просто обрезаный! Тут я и офигел! То есть такой метод стриминга байтов на карточку рабочий, просто обрывается связь.  dclient.available() становится нихера не эвэйлебл, и - привет семье!   //dclient.timedRead(); - это я проверял как будет без пересылания данных на карту работать. Разницы не заметил. Так что автор не зря жаловался, что ему больше 90кб не удалось скачивать.  У меня скачивалось до 3х Мб, но соединение рвалось, и причина тут не в карте памяти или оперативке, а в его библиотеке.  Вот такая печаль.

Алексей.
Алексей. аватар
Offline
Зарегистрирован: 02.02.2018

5N62V пишет:
И - не работает. :( файлы короче, чем положено.
Такое поведение видимо из за того, что приемная сторона не успевает обработать поток данных приходящих от сервера.
При передачи очередной порции данных, количество байтов ограничен размером окна, принимаемая сторона не отправляет подтверждение, отправитель повторяет отправку и вновь не получает подтверждения и разрывает соединение.

Простое решение - это использовать другой протокол, например http.
В запросе нужно добавить заголовок "Range" и указать какой диапазон байтов требуется получить.
Например:
Добавляю заголовок "Range: bytes=0-0" указываю что хочу получить байты с нулевого по нулевой т.е. хочу один байт, на самом деле хочу узнать лишь длину файла.
В ответе получаю 206-й код (Partial Content) и заголовок "Content-Range: bytes 0-0/42091" и контент в один байт, теперь я знаю что файл длиной 42091 байт.
Затем в цикле выполняю запросы указывая диапазоны байтов, которые я гарантированно получу и смогу их обрабатывать без ограничения по времени.
Запрашиваю очередной кусок байтов длиной 4096, в запросе "Range: bytes=8192-12287", в ответе получаю "Content-Range: bytes 8192-12287/42091" и "Content-Length: 4096" и собственно контент.

 

5N62V
Offline
Зарегистрирован: 25.02.2016

Алексей. пишет:

Простое решение - это использовать другой протокол, например http.
 

Да, я это понимаю, но тут проблема иная - я в http полный ноль, как, впрочем, и во всех других сетевых протоколах. Найти ресурс где лаконично представлено все, что мне надо, я не смог. Учить с нуля весь http - было бы здорово, но времени нет. Или с духом не соберусь.  Вот и допиливаю что найду. Не всегда оно работает, ессно.  Вот товарищ negavoid мне даже содержимое php файла дал, а как его применить - не знаю :( В сети сплошные серврера расписаны, а по клиентам очень мало. Поэтому "Добавляю заголовок "Range: bytes=0-0" для меня пока сродни "берешь лямбда-функцию, и юзаешь ее". :)

Вообще я уже запустил свой девайс по http применив вот это решение. Но там автор не удосужился (вернее ему не надо было) опрос контента организовать. Так я поженил ужа с носорогом: для получения списка файлов подымаю ftp клиент, получаю контент, убиваю фтп клиента, для скачивания подымаю http клиент, скачиваю, убиваю его. :)  Через жопу? Однозначно да! :)  Но это пока все, на что я способен.  У меня ответ на вопрос "вам шашечки , или ехать?" однозначный - ехать! Но как правило уже в дороге я очередной раз понимаю, что с шашечками ехать лучше! :)

 

Алексей.
Алексей. аватар
Offline
Зарегистрирован: 02.02.2018

Вы на одном ядре выполняете получение байтов с потока и на том-же ядре выполняете запись на карту.
Не пробовали сравнить время получения файла с ftp не выполняя пока запись, и во втором опыте время сохранения такого же количества байт в файл на карту.
Если время сохранения на карту сравнимо со временем получения файла, то получите как раз такой неудовлетворительный результат.
Например: загрузка файла 100 секунд и сохранение тоже 100 секунд, значит на одном ядре получим 200 секунд и сервер передавая очередную порцию данных не дожидается подтверждения.
Можно попробовать выполнять загрузку на нулевом ядре (на нем живут драйверы wifi в том числе), а сохранение на первом и обмениваться через очередь (один таск добавляет в очередь, а другой получает данные из очереди).
Если очередь не будет успевать опустошаться вы сразу это заметите, получив ошибку при добавлении в очередь очередной порции данных.

5N62V
Offline
Зарегистрирован: 25.02.2016

Алексей. пишет:

Не пробовали сравнить время получения файла с ftp не выполняя пока запись, и во втором опыте время сохранения такого же количества байт в файл на карту.

Я отключал запись на карту:  строку file.write((uint8_t)dclient.timedRead());я менял на dclient.timedRead();.  В таком виде функция работала быстрее чем у автора - просто считывался байт без заполнения буфера и связанных с этим проверок. Результат был одинаковый : скачивание прерывалось после скачанных пары Мб.  Поэтому я пришел к выводу, что проблема кроется не в том, что проц не успевает записать данные на карту. С этим как раз все ок, раз  иногда больше 3х метров записать удавалось, и кусок файла получался не битый. Тут  свет на ситуацию пролил бы лог с сервера, но у нас в поддержке сидят манагеры, а не разработчики, увы.

Про разделение потоков я тоже думал, но так как и первое ядро успевает считывать и скидывать на карту, то смысла перекидывать задачу на нулевое ядро не увидел. 

В любом случает спасибо за участие! Может ткнете в ссылку на стоящий ресурс, где достойно описан порядок составления запросов и обработки ответов html?