SPIFFS - Fragen und Antworten - [Teil 2] - AZ-Delivery

Teil 2 

Im ersten Teil dieser Blogreihe habe ich auf dem D1 Mini NodeMCU (ESP8266) die Nutzung des SPIFFS gezeigt und dabei versucht, Nutzerfragen zu beantworten. Die letzte Frage aus den Kommentaren war, wie es möglich ist, die gespeicherte Datei auf einen SFTP-Server hochzuladen. Damit werde ich mich zuerst befassen. Außerdem wechseln wir von SPIFFS zu LittleFS und laden Temperaturdaten, die in einer Textdatei gespeichert werden, auf einen Fileserver. Los geht's.

(S)FTP-Verbindung

Von dem Leser Steve kam die Frage, wie man seine Daten nun aus dem SPIFFS-Dateisystem direkt via SFTP auf einen File Server hochladen kann. Dieses Thema ist sehr komplex. Bei meiner Suche bin ich auf einen Thread bei stackoverflow.com gestoßen, wo jemand genau das Gleiche versucht.
Er bekommt dort auch die passende Antwort mit der Begründung, dass das mit der Recheneinheit des ESP8266 nicht so einfach umsetzbar ist.

Eine Lösung ist dort, dass der Server nicht per SFTP angesprochen, sondern eine TCP-Verbindung aufgebaut wird, über die AT-Kommandos gesendet werden. Allerdings findet damit kein Schlüsselaustausch statt, der für die Sicherheit zuständig ist. Hinter diesem
Link findet man (unabhängig vom ESP8266) Kommando-Beispiele zum Dateiaustausch mit einem SFTP. Den habe ich im Nutzerforum auf der Webseite von Espressif gefunden (dem Hersteller der ESP8266). Auch dort wird darauf hingewiesen, dass es zurzeit keine Implementierung einer SFTP-Verbindung von einem ESP8266 gibt.

Es wird empfohlen, einen ESP32 zu verwenden. Dafür gibt es eine Lösung auf Github, die cURL verwendet. Damit ist unter anderem auch HTTPS sowie SFTP möglich.

Mehr konnte ich zu dem Thema leider nicht finden. Was schade ist, da z.B. mein Webspace Provider nur noch SFTP anbietet. Eine eigene Bibliothek zu schreiben, würde den Rahmen dieses Beitrags sprengen. Wenn Sie eine Lösung gefunden haben, schreiben Sie es gern unten in die Kommentare.

Eventuell ist der Datenaustausch über einen (S)FTP auch nicht der richtige Ansatz, um Sensordaten eines Mikrocontrollers auf einer Webseite anzeigen zu lassen. Statt HTML sind Webseiten meist in PHP geschrieben und greifen auf SQL-Datenbanken zurück. Auf
dieser Webseite habe ich eine Anleitung gefunden, wie man Sensordaten von einem ESP32 bzw. ESP8266 direkt in eine MySQL-Datenbank hochlädt.

Ich habe mich nun trotzdem auf die Suche begeben, um zumindest eine FTP-Lösung zu finden. Dazu habe ich im Arduinoforum eine Diskussion gefunden. Darüber bin ich dann im Arduino Playground gelandet, wo es eine Anleitung für den Upload von Daten des Arduinos auf einen FTP-Server gibt.

Dafür wird allerdings ein Ethernet Shield verwendet und SPIFFS ist nicht implementiert. Nach weiterer Suche bin ich dann bei
Github auf der ESP8266-Seite im Bereich Issues auf das Thema FTP-Client gestoßen. Dort wollte der Nutzer rudi48 basierend auf dem Code aus dem Arduino Playground einen Client programmieren, der die Netzwerkverbindung des ESP8266 nutzt. Seine Resultate findet man auf seiner Webseite.

Von dort nehme ich mir zum Testen den Quellcode, der auf dem NodeMCU laufen soll:

// File: WiWi_FTP_Client.ino for ESP8266 NodeMCU
/*
   Original Arduino: FTP passive client 
   http://playground.arduino.cc/Code/FTP
   Modified 6 June 2015 by SurferTim

   You can pass flash-memory based strings to Serial.print() by wrapping them with F().

   2015-12-09 Rudolf Reuter, adapted to ESP8266 NodeMCU, with the help of Markus.
*/
#include <ESP8266WiFi.h>
#include <FS.h>

// comment out next line to write to SD from FTP server
#define FTPWRITE

// Set these to your desired softAP credentials. They are not configurable at runtime.
const char *ssid = "WLAN-NAME";
const char *password = "xxxxxxxx";

boolean debug = false;  // true = more messages
//boolean debug = true;

// LED is needed for failure signalling
const short int BUILTIN_LED2 = 16;  //GPIO16 on NodeMCU (ESP-12)

unsigned long startTime = millis();

// provide text for the WiFi status
const char *str_status[]= {
  "WL_IDLE_STATUS",
  "WL_NO_SSID_AVAIL",
  "WL_SCAN_COMPLETED",
  "WL_CONNECTED",
  "WL_CONNECT_FAILED",
  "WL_CONNECTION_LOST",
  "WL_DISCONNECTED"
};

// provide text for the WiFi mode
const char *str_mode[]= { "WIFI_OFF", "WIFI_STA", "WIFI_AP", "WIFI_AP_STA" };

// change to your server
IPAddress server( 192, 168, 17, 72 );

WiFiClient client;
WiFiClient dclient;

char outBuf[128];
char outCount;

// change fileName to your file (8.3 format!)
String fileName = "TimeTemp.txt";
String  path = "/TimeTemp.txt";

// SPIFFS file handle
File fh;

void signalError() {  // loop endless with LED blinking in case of error
  while(1) {
      digitalWrite(BUILTIN_LED2, LOW);
      delay(300); // ms
      digitalWrite(BUILTIN_LED2, HIGH);
      delay(300); // ms
  }
}

//format bytes
String formatBytes(size_t bytes) {
  if (bytes < 1024) {
    return String(bytes) + "B";
  } else if (bytes < (1024 * 1024)) {
    return String(bytes / 1024.0) + "KB";
  } else if (bytes < (1024 * 1024 * 1024)) {
    return String(bytes / 1024.0 / 1024.0) + "MB";
  } else {
    return String(bytes / 1024.0 / 1024.0 / 1024.0) + "GB";
  }
}

//----------------------- WiFi handling
void connectWifi() {
  Serial.print("Connecting as wifi client to SSID: ");
  Serial.println(ssid);

  // use in case of mode problem
  WiFi.disconnect();
  // switch to Station mode
  if (WiFi.getMode() != WIFI_STA) {
    WiFi.mode(WIFI_STA);
  }

  WiFi.begin ( ssid, password );

  if (debug ) WiFi.printDiag(Serial);

  // ... Give ESP 10 seconds to connect to station.
  unsigned long startTime = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - startTime < 10000) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  // Check connection
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("WiFi connected; IP address: ");
    Serial.println(WiFi.localIP());
  } else {
    Serial.print("WiFi connect failed to ssid: ");
    Serial.println(ssid);
    Serial.print("WiFi password <");
    Serial.print(password);
    Serial.println(">");
    Serial.println("Check for wrong typing!");
  }
}  // connectWiFi()

//----------------- FTP fail
void efail() {
  byte thisByte = 0;

  client.println(F("QUIT"));

  while (!client.available()) delay(1);

  while (client.available()) {
    thisByte = client.read();
    Serial.write(thisByte);
  }

  client.stop();
  Serial.println(F("Command disconnected"));
  fh.close();
  Serial.println(F("SD closed"));
}  // efail

//-------------- FTP receive
byte eRcv() {
  byte respCode;
  byte thisByte;

  while (!client.available()) delay(1);

  respCode = client.peek();

  outCount = 0;

  while (client.available()) {
    thisByte = client.read();
    Serial.write(thisByte);

    if (outCount < 127) {
      outBuf[outCount] = thisByte;
      outCount++;
      outBuf[outCount] = 0;
    }
  }

  if (respCode >= '4') {
    efail();
    return 0;
  }
  return 1;
}  // eRcv()

//--------------- FTP handling
byte doFTP(boolean upload) {
  
  if (upload) {
    fh = SPIFFS.open(path, "r");
  } else {
    SPIFFS.remove(path);
    fh = SPIFFS.open(path, "w");
  }

  if (!fh) {
    Serial.println(F("SPIFFS open fail"));
    return 0;
  }

  if (upload) {
    if (!fh.seek((uint32_t)0, SeekSet)) {
      Serial.println(F("Rewind fail"));
      fh.close();
      return 0;
    }
  }

  if (debug) Serial.println(F("SPIFFS opened"));

  if (client.connect(server, 21)) {  // 21 = FTP server
    Serial.println(F("Command connected"));
  } else {
    fh.close();
    Serial.println(F("Command connection failed"));
    return 0;
  }

  if (!eRcv()) return 0;
  if (debug) Serial.println("Send USER");
  client.println(F("USER rudi"));

  if (!eRcv()) return 0;
  if (debug) Serial.println("Send PASSWORD");
  client.println(F("PASS <password>"));

  if (!eRcv()) return 0;
  if (debug) Serial.println("Send SYST");
  client.println(F("SYST"));

  if (!eRcv()) return 0;
  if (debug) Serial.println("Send Type I");
  client.println(F("Type I"));

  if (!eRcv()) return 0;
  if (debug) Serial.println("Send PASV");
  client.println(F("PASV"));

  if (!eRcv()) return 0;

  char *tStr = strtok(outBuf, "(,");
  int array_pasv[6];
  for ( int i = 0; i < 6; i++) {
    tStr = strtok(NULL, "(,");
    array_pasv[i] = atoi(tStr);
    if (tStr == NULL) {
      Serial.println(F("Bad PASV Answer"));
    }
  }
  unsigned int hiPort, loPort;
  hiPort = array_pasv[4] << 8;
  loPort = array_pasv[5] & 255;

  if (debug) Serial.print(F("Data port: "));
  hiPort = hiPort | loPort;
  if (debug) Serial.println(hiPort);

  if (dclient.connect(server, hiPort)) {
    Serial.println(F("Data connected"));
  }
  else {
    Serial.println(F("Data connection failed"));
    client.stop();
    fh.close();
    return 0;
  }

  if (upload) {
    if (debug) Serial.println("Send STOR filename");
    client.print(F("STOR "));
    client.println(fileName);
  } else {
    if (debug) Serial.println("Send RETR filename");
    client.print(F("RETR "));
    client.println(fileName);
  }

  if (!eRcv()) {
    dclient.stop();
    return 0;
  }

  if (upload) {
    if (debug) Serial.println(F("Writing"));
    // for faster upload increase buffer size to 1460
//#define bufSizeFTP 64
#define bufSizeFTP 1460
    uint8_t clientBuf[bufSizeFTP];
    //unsigned int clientCount = 0;
    size_t clientCount = 0;
  
    while (fh.available()) {
      clientBuf[clientCount] = fh.read();
      clientCount++;
      if (clientCount > (bufSizeFTP - 1)) {
        dclient.write((const uint8_t *) &clientBuf[0], bufSizeFTP);
        clientCount = 0;
        delay(1);
      }
    }
    if (clientCount > 0) dclient.write((const uint8_t *) &clientBuf[0], clientCount);

  } else {
    while (dclient.connected()) {
      while (dclient.available()) {
        char c = dclient.read();
        fh.write(c);
        if (debug) Serial.write(c);
      }
    }
  }

  dclient.stop();
  Serial.println(F("Data disconnected"));

  if (!eRcv()) return 0;

  client.println(F("QUIT"));

  if (!eRcv()) return 0;

  client.stop();
  Serial.println(F("Command disconnected"));

  fh.close();
  if (debug) Serial.println(F("SPIFS closed"));
  return 1;
}  // doFTP()


void readSPIFFS() {
  fh = SPIFFS.open(fileName, "r");

  if (!fh) {
    Serial.println(F("SPIFFS open fail"));
    return;
  }

  while (fh.available()) {
    Serial.write(fh.read());
  }

  fh.close();
}  // readSPIFFS()


void setup() {
  delay(1000);
  Serial.begin(115200);
  delay(1000);
  Serial.println("Sync,Sync,Sync,Sync,Sync");
  delay(500);
  Serial.println();
  // signal start
  pinMode(BUILTIN_LED2, OUTPUT);
  digitalWrite(BUILTIN_LED2, LOW);
  delay(100); // ms
  digitalWrite(BUILTIN_LED2, HIGH);
  delay(300); // ms

  Serial.print("Chip ID: 0x");
  Serial.println(ESP.getChipId(), HEX);

  Serial.println ( "Connect to Router requested" );
  connectWifi();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("WiFi mode: ");
    Serial.println(str_mode[WiFi.getMode()]);
    Serial.print ( "Status: " );
    Serial.println (str_status[WiFi.status()]);
    // signal WiFi connect
    digitalWrite(BUILTIN_LED2, LOW);
    delay(300); // ms
    digitalWrite(BUILTIN_LED2, HIGH);      
  } else {
    Serial.println("");
    Serial.println("WiFi connect failed, push RESET button.");
    signalError();
  }

  if (!SPIFFS.begin()) {
     Serial.println("SPIFFS failed, needs formatting");
     signalError();
  }

  fh = SPIFFS.open(path, "r");
  if (!fh) {
    Serial.println(F("SPIFFS open fail"));
    signalError();
  } else fh.close();
  
  Serial.println(F("Ready. Press d, u or r"));
}  // setup()


void loop() {
  byte inChar;

  if (Serial.available() > 0) {
    inChar = Serial.read();
  }

boolean upload = true; // false = download

  if (inChar == 'd') {
    upload = false;
    if (doFTP(upload)) Serial.println(F("FTP OK"));
    else Serial.println(F("FTP FAIL"));
  }
  
  if (inChar == 'u') {
    upload = true;
    if (doFTP(upload)) Serial.println(F("FTP OK"));
    else Serial.println(F("FTP FAIL"));
  }  

  if (inChar == 'r') {
    String fileNameDir;
    Dir dir = SPIFFS.openDir("/");
    while (dir.next()) {
      fileNameDir = dir.fileName();
      size_t fileSize = dir.fileSize();
      Serial.printf("FS File: %s, size: %s\n", fileNameDir.c_str(), formatBytes(fileSize).c_str());
    }
  }
  delay(10);  // give time to the WiFi handling in the background
}

Folgende Zeilen müssen dann geändert werden:

18: SSID, also der Name des eigenen WLAN.

19: PASSWORD: der dazugehörige WLAN-Schlüssel.

44: IP-Adresse: des FTP-Servers (Achtung: nicht mit Punkten, sondern mit Kommata getrennt).

53 und 54: Dateiname. Ich nutze hier wieder meine test.txt.

201: Nutzername für den FTP-Server. Man ersetzt hier rudi durch seinen eigenen Nutzernamen.

205: Passwort für den FTP-Server. Hier ersetzt man  durch sein eigenes Passwort.

Das Programm kann nun kompiliert werden. Öffnet man den Seriellen Monitor, bekommt man Informationen zur WLAN-Verbindung. Ist diese erfolgreich und konnte die angegebene Datei im SPIFFS gefunden werden, kann man in die Eingabezeile d (download), u (upload) oder r (read filesystem) eingeben.
Damit ist es möglich, Dateien aus dem SPIFFS des (in meinem Fall) D1 Mini NodeMCU an einen FTP-Server im passive Mode zu übertragen.

LittleFS

Bereits auf der Webseite, die die Informationen zur Installation des SPIFFS in der Arduino IDE bereitstellt, wird darauf hingewiesen, dass SPIFFS "deprecated" (also veraltet) ist und nicht weiterentwickelt wird. Man soll auf die andere Variante namens LitteFS wechseln. Den gleichen Hinweis erhält man in der Arduino IDE im unteren Informationsblock, wenn man ein Programm mit SPIFFS kompiliert.

Auf meiner Suche nach einem FTP-Client für den ESP8266 bin ich über die vorher genannte Github-Seite auch noch auf eine weitere Lösung gestoßen, die LittleFS statt SPIFFS nutzt. Man kann das Paket als ZIP-Datei herunterladen und anschließend in der Arduino IDE über das Menü Sketch -> Bibliothek einbinden -> .ZIP-Bibliothek hinzufügen installieren. Es stehen dann auch Beispiele unter dem Namen FTPClientServer zur Verfügung. Ich nutze das Beispiel FTPClientSample:

/*
   This is an example sketch to show the use of the FTP Client.

   Please replace
     YOUR_SSID and YOUR_PASS
   with your WiFi's values and compile.

   If you want to see debugging output of the FTP Client, please
   select select an Serial Port in the Arduino IDE menu Tools->Debug Port

   Send L via Serial Monitor, to display the contents of the FS
   Send F via Serial Monitor, to fromat the FS
   Send G via Serial Monitor, to GET a file from a FTP Server

   This example is provided as Public Domain
   Daniel Plasa <dplasa@gmail.com>
   https://github.com/dplasa/FTPClientServer
*/
#include <ESP8266WiFi.h>
#include <LittleFS.h>
#include <FTPClient.h>

const char *ssid PROGMEM = "WLAN-NAME";
const char *password PROGMEM = "****************";

// tell the FTP Client to use LittleFS
FTPClient ftpClient(LittleFS);

// provide FTP servers credentials and servername
FTPClient::ServerInfo ftpServerInfo("ftp-authoring", "Z&4L/&n%!", "ftp.eurotape.de");

void setup(void)
{
  Serial.begin(115200);
  WiFi.begin(ssid, password);

  bool fsok = LittleFS.begin();
  Serial.printf_P(PSTR("FS init: %S\n"), fsok ? PSTR("ok") : PSTR("fail!"));

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.printf_P(PSTR("."));
  }
  Serial.printf_P(PSTR("\nConnected to %S, IP address is %s\n"), ssid, WiFi.localIP().toString().c_str());

  ftpClient.begin(ftpServerInfo);
}

enum consoleaction
{
  show,
  get,
  put,
  wait,
  format,
  list
};
consoleaction action = show;

String fileName;
bool transferStarted = false;
uint32_t startTime;

void loop()
{
  // this is all you need
  // make sure to call handleFTP() frequently
  ftpClient.handleFTP();

  //
  // Code below just to debug in Serial Monitor
  //
  if (action == show)
  {
    Serial.printf_P(PSTR("Enter 'F' to format, 'L' to list the contents of the FS, 'G'/'P' + File to GET/PUT from/to FTP Server\n"));
    action = wait;
  }
  else if (action == wait)
  {
    if (transferStarted )
    {
      const FTPClient::Status &r = ftpClient.check();
      if (r.result == FTPClient::OK)
      {
        Serial.printf_P(PSTR("Transfer complete, took %lu ms\n"), millis() - startTime);
        transferStarted = false;
      }
      else if (r.result == FTPClient::ERROR)
      {
        Serial.printf_P(PSTR("Transfer failed after %lu ms, code: %u, descr=%s\n"), millis() - startTime, ftpClient.check().code, ftpClient.check().desc.c_str());
        transferStarted = false;
      }
    }

    if (Serial.available())
    {
      char c = Serial.read();
      if (c == 'F')
        action = format;
      else if (c == 'L')
        action = list;
      else if (c == 'G')
      {
        action = get;
        fileName = Serial.readStringUntil('\n');
        fileName.trim();
      }
      else if (c == 'P')
      {
        action = put;
        fileName = Serial.readStringUntil('\n');
        fileName.trim();
      }
      else if (!(c == '\n' || c == '\r'))
        action = show;
    }
  }

  else if (action == format)
  {
    startTime = millis();
    LittleFS.format();
    Serial.printf_P(PSTR("FS format done, took %lu ms!\n"), millis() - startTime);
    action = show;
  }

  else if ((action == get) || (action == put))
  {
    startTime = millis();
    ftpClient.transfer(fileName, fileName, action == get ?
                       FTPClient::FTP_GET_NONBLOCKING : FTPClient::FTP_PUT_NONBLOCKING);
    Serial.printf_P(PSTR("transfer started, took %lu ms!\n"), millis() - startTime);
    transferStarted = true;
    action = show;
  }

  else if (action == list)
  {
    Serial.printf_P(PSTR("Listing contents...\n"));
    uint16_t fileCount = listDir("", "/");
    Serial.printf_P(PSTR("%d files/dirs total\n"), fileCount);
    action = show;
  }
}

uint16_t listDir(String indent, String path)
{
  uint16_t dirCount = 0;
  Dir dir = LittleFS.openDir(path);
  while (dir.next())
  {
    ++dirCount;
    if (dir.isDirectory())
    {
      Serial.printf_P(PSTR("%s%s [Dir]\n"), indent.c_str(), dir.fileName().c_str());
      dirCount += listDir(indent + "  ", path + dir.fileName() + "/");
    }
    else
      Serial.printf_P(PSTR("%s%-16s (%ld Bytes)\n"), indent.c_str(), dir.fileName().c_str(), (uint32_t)dir.fileSize());
  }
  return dirCount;
}

Auch hier müssen in den Zeilen 23, 24 und 30 die eigenen Zugangsdaten eingegeben werden, ähnlich wie im vorangegangenen Beispiel. Um das Programm zu kompilieren, ist nun aber das LittleFS-Plugin notwendig. Der Installationsablauf für LittleFS ist identisch zu dem von SPIFFS. Man muss lediglich eine andere Datei von Github herunterladen. Aktuell ist es Version 2.6.0.

Nachdem ich das installiert hatte und das Programm kompilieren wollte, wurde mir die Fehlermeldung "mklittlefs not found!" angezeigt. Wenn Sie auch diese Fehlermeldung erhalten: Die Lösung dafür ist lediglich ein Update ESP8266 Core, da es einen Bug in der Boardliste gab. Dafür muss man über das Menü Werkzeuge -> Board -> Boardverwalter nach esp8266 suchen und sollte esp8266 by ESP8266 Community finden und updaten. Aktuell ist die Version 2.7.1. Man muss dann noch das Dateisystem in den Flash-Speicher des ESP8266 übertragen. Über das Menü Sketch -> Sketch Ordner anzeigen gelangt man, wie bereits beschrieben, in den Ordner des Arduino Sketches.

Dort legt man den Ordner
data an und erstellt darin die gewünschten Dateien. Anschließend wählt man im Menü Werkzeuge den Punkt ESP8266 LittleFS Data Upload (wieder daran denken, den Seriellen Monitor zu schließen!):


Auf dem Bild sieht man auch meine Einstellungen. Die Ausgabe im unteren Teil zeigt bereits das Ergebnis des Uploads auf den D1 Mini. Öffnet man wieder den Seriellen Monitor, sieht man die Informationen zur Verbindung zum WLAN. Eventuell muss man nochmal die Resettaste am Mikrocontroller betätigen. Man hat nun die Möglichkeit, den Inhalt des Dateisystems durch Eingabe von
L anzuzeigen oder mit F zu formatieren. Außerdem kann man mit G (Get, herunterladen) oder P (Put, hochladen) gefolgt vom Dateinamen (ohne das '+' ) Daten mit dem FTP-Server austauschen.

 Ein individuelles Programm

Wenn die Ausführung des letzten Beispiels funktioniert hat, können wir das als Basis nutzen und ein eigenes Programm schreiben. Nehmen wir an, Sie besitzen ein Freibad und stellen Ihren Gästen Informationen zur Wassertemperatur auf Ihrer Webseite zur Verfügung.

Eine Möglichkeit wäre nun, dass Sie in Ihrem PHP-Code eine Textdatei auslesen, die sich auf dem gleichen Webspace befindet. Zu diesem Webspace existiert eine FTP-Verbindung. Es ist allerdings keine verschlüsselte Verbindung. Durch Nutzername und Passwort, sowie dem gekapselten Ordner haben wir zumindest noch für ein klein wenig Datensicherheit gesorgt. Die Temperaturdaten sind dann sowieso für die Öffentlichkeit gedacht.

Ich werde nun einen DHT11 Temperatursensor (hier erhältlich) an den D1 Mini NodeMCU anschließen, seine Daten in die Textdatei im Flashspeicher schreiben und in regelmäßigen Zyklen zum FTP hochladen. Das Ausgeben auf der Webseite werde ich nicht behandeln. Dort müsste die Textdatei geöffnet und zeilenweise ausgelesen werden. Der Temperatursensor misst hier natürlich nicht Wasser-, sondern Lufttemperatur. Er dient nur als Beispiel.

Eventuell möchten Sie ja auch Lufttemperatur auf Ihrer Webseite zeigen. Angeschlossen wird der Sensor folgendermaßen:


Ich nutze als Basiscode die Beispiele FTPClientSample, LittleFS
Timestamp und DHTESP8266 . Für die Verwendung des Temperatursensors muss die passende Bibliothek installiert werden. Dafür öffnet man wieder die Bibliotheksverwaltung und gibt in die Suche DHT11 ein. Ich habe die DHT sensor library for ESPx by beegee_tokyo gewählt.

Aus den genannten Beispielen baue ich ein Programm zusammen, dass eine FTP-Verbindung herstellt, das LittleFS Dateisystem mountet, die aktuelle Uhrzeit von einem Zeitserver holt, die Temperatur ermittelt und das alles in einer Textdatei auf den FTP-Server hochlädt.

Versuchen wir zuerst zu verstehen, wie das FTPClientSample funktioniert. Es ist ein bisschen verwirrend, da zwischen den verschiedenen Zuständen hin und her gesprungen wird. Da ich nur den Teil brauche, der die Datei hochlädt (also Put), entferne ich den Rest. Dann wird es etwas leichter. Ich schreibe den Code um und nutze eine switch-case-Anweisung, damit der Ablauf etwas übersichtlicher wird. Ich lasse die Tastatureingabe weg und übergebe dem Upload direkt meine test.txt.

In jedem Schleifendurchlauf muss
ftpClient.handleFTP() aufgerufen werden. Da ich statt auf Tastatureingabe auf ein Zeitintervall reagieren lassen möchte, entferne ich auch diese Tastatureingabe über die serielle Schnittstelle. Ist das Zeitintervall überschritten, wird in den nächsten Zustand gewechselt.

Dort wird die Textdatei mit dem Parameter "a" (für append, also am Ende anfügen) geöffnet. War das Öffnen erfolgreich, wird die aktuelle Uhrzeit und das Datum geholt, das wird in die Datei geschrieben, diese wird wieder geschlossen und in den nächsten Zustand gewechselt. Darin wird der Upload zum FTP-Server gestartet und erneut in den nächsten Zustand gewechselt.

An dieser Stelle checkt das Programm mit jedem Schleifendurchlauf auf die Antwort des FTP-Servers. War der Upload erfolgreich (oder auch nicht), wird wieder zum Anfangszustand zurück gewechselt und erneut auf das Zeitintervall geprüft. Die Abläufe funktionieren nicht-blockierend.

Das Programm wechselt zwar in einen anderen case der switch-case-Anweisung. Diese wird allerdings immer wieder durchlaufen. Das Programm bleibt also nicht stehen. Das ist wichtig, da sonst das Prüfen auf die FTP-Server-Antwort nicht funktioniert. Außerdem ist es so möglich, noch weitere Aufgaben durchführen zu lassen, beispielsweise um einen Taster zu betätigen.

Mein Quellcode sieht folgendermaßen aus:

/*
   Zyklischer FTP Upload einer Textdatei
   von Andreas Wolter
   
   Temperatur in Textdatei schreiben und auf einen
   FTP Server hochladen.
     
   Quellen der Vorlagen:
   
   FTPClientSample
   This is an example sketch to show the use of the FTP Client.

   Please replace
     YOUR_SSID and YOUR_PASS
   with your WiFi's values and compile.

   This example is provided as Public Domain
   Daniel Plasa <dplasa@gmail.com>
   https://github.com/dplasa/FTPClientServer

   ----------------------------

   LittleFS_timestamp
   Example showing timestamp support in LittleFS
   Released into the public domain.
   Earle F. Philhower, III <earlephilhower@yahoo.com>

   ----------------------------

   DHT_ESP8266
   by beegee_tokyo
   
*/
#include <ESP8266WiFi.h>
#include <LittleFS.h>
#include <FTPClient.h>
#include "DHTesp.h"

// Wifi
const char *ssid PROGMEM = "IHR WLAN-NAME";
const char *password PROGMEM = "IHR WLAN-KEY";

// FTP
String fileName = "test.txt";
FTPClient ftpClient(LittleFS);    // LittleFS für FTP nutzen
FTPClient::ServerInfo ftpServerInfo("IHR NAME", "IHR PASSWORT", "IHRE ADRESSE"); // FTP-Zugang

// State Machine
unsigned int state = 0;

// Zeitmessungen
uint32_t startTime;
uint32_t aktuelleZeit = 0;
uint32_t alteZeit = 0;
uint32_t uploadIntervall = 10000;

// Zeitserver
long timezone = -2;
byte daysavetime = 0;
struct tm tmstruct ;

// Temperatur
float temperatur = 0.0;
DHTesp dht;

bool getLocalTime(struct tm * info, uint32_t ms) {
  uint32_t count = ms / 10;
  time_t now;

  time(&now);
  localtime_r(&now, info);

  if (info->tm_year > (2016 - 1900)) {
    return true;
  }
  while (count--) {
    delay(10);
    time(&now);
    localtime_r(&now, info);
    if (info->tm_year > (2016 - 1900)) {
      return true;
    }
  }
  return false;
}

void setup(void)
{
  Serial.begin(115200);
  WiFi.begin(ssid, password);
  // LittleFS starten
  bool fsok = LittleFS.begin();
  Serial.printf_P(PSTR("FS init: %S\n"), fsok ? PSTR("ok") : PSTR("fail!"));

  // Warte auf WiFi-Verbindung
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.printf_P(PSTR("."));
  }
  Serial.printf_P(PSTR("\nConnected to %S, IP address is %s\n"), ssid, WiFi.localIP().toString().c_str());

  // FTP-Client starten und Zugansdaten uebergeben
  ftpClient.begin(ftpServerInfo);

  // Mit Zeitserver verbinden
  Serial.println("Contacting Time Server");
  configTime(3600 * timezone, daysavetime * 3600, "time.nist.gov", "0.pool.ntp.org", "1.pool.ntp.org");
  delay(2000);
  tmstruct.tm_year = 0;
  getLocalTime(&tmstruct, 5000);
  Serial.printf("\nNow is : %d-%02d-%02d %02d:%02d:%02d\n", (tmstruct.tm_year) + 1900, (tmstruct.tm_mon) + 1, tmstruct.tm_mday, tmstruct.tm_hour, tmstruct.tm_min, tmstruct.tm_sec);
  Serial.println("");

  // DHT11 Temperatursensor initialisieren
  dht.setup(2, DHTesp::DHT11);  // Pin D4 / 17 / GPIO2
}

void loop()
{
  // muss zyklisch ausgefuehrt werden
  ftpClient.handleFTP();

  // FTP Upload nach festgelegtem Intervall
  switch(state) {
    case 0: {   // Zeit pruefen
      aktuelleZeit = millis();
      if (aktuelleZeit - alteZeit >= uploadIntervall) {
        state = 1;
        alteZeit = aktuelleZeit;
      }
    } break;
    case 1: {  
      File file = LittleFS.open("/test.txt", "a");
      if (file) {
        getLocalTime(&tmstruct, 5000);
        temperatur = dht.getTemperature();
        Serial.printf("%02d.%02d.%d %02d:%02d:%02d,%.2f,Grad Celsius\n",  tmstruct.tm_mday, (tmstruct.tm_mon) + 1, (tmstruct.tm_year) + 1900, tmstruct.tm_hour, tmstruct.tm_min, tmstruct.tm_sec, temperatur);
        file.printf("%02d.%02d.%d %02d:%02d:%02d,%.2f,Grad Celsius\n",  tmstruct.tm_mday, (tmstruct.tm_mon) + 1, (tmstruct.tm_year) + 1900, tmstruct.tm_hour, tmstruct.tm_min, tmstruct.tm_sec, temperatur);
        file.close();
        state = 2;
      }
      else {
        Serial.println("Datei konnte nicht geoeffnet werden");
        state = 0;
      }
    } break;
    case 2: {   // Upload starten
      startTime = millis();
      ftpClient.transfer(fileName, fileName, FTPClient::FTP_PUT_NONBLOCKING);
      Serial.printf_P(PSTR("Upload gestartet, Dauer %lu ms!\n"), millis() - startTime);
      state = 3;
    } break;
    case 3: {   // Antwort des FTP-Servers checken
      const FTPClient::Status &r = ftpClient.check();
      if (r.result == FTPClient::OK)
      {
        Serial.printf_P(PSTR("Upload abgeschlossen, Dauer %lu ms\n"), millis() - startTime);
        state = 0;
      }
      else if (r.result == FTPClient::ERROR)
      {
        Serial.printf_P(PSTR("Upload fehlgeschlagen nach %lu ms, code: %u, descr=%s\n"), millis() - startTime, ftpClient.check().code, ftpClient.check().desc.c_str());
        state = 0;
      }      
    } break;
    default: state = 0; break;
  }
}

Meine Ausgabe im Seriellen Monitor sieht dann so aus:


Und der Inhalt meiner Textdatei im Flash-Speicher sowie auf dem FTP sieht so aus:


Ich habe Datum/Uhrzeit, Temperaturwert und Einheit der Temperatur durch Kommata getrennt. So könnte man statt TXT eine CSV-Datei (Comma separated values) speichern. Die Kommata dienen dann als Trennzeichen. Es ist dann relativ einfach, ein Diagramm aus einer Tabelle zu erzeugen und den Verlauf der Temperaturwerte grafisch darzustellen.

Fazit

Leider habe ich keine zufriedenstellende Antwort auf die Frage gefunden, wie man die Dateien, die im Flash-Speicher abgelegt werden, per SFTP-Verbindung auf einen Server hochladen kann.

Die Community um das Arduino-Projekt und der ESP-Reihe hat Unmengen an Programmen geschrieben und als Bibliotheken zur Verfügung gestellt. Dadurch sind wir in der komfortablen Lage, nicht jeden Code komplett selbst schreiben zu müssen. Ich bin sicher, es gibt eine Lösung für dieses Problem. Ich konnte sie nur leider nicht finden.

Trotzdem konnte ich durch die Fragen und die Recherchen zu den Antworten neue Dinge ausprobieren. Ich hoffe, ich konnte einige Fragen beantworten und mit meinen Lösungswegen etwas weiterhelfen.

Vielen Dank für die Kommentare und die daraus entstandenen Denkanstöße. 

Viel Spaß beim Ausprobieren.

Andreas Wolter

für AZ-Delivery Blog 

Grundlagen software

7 Kommentare

Djg

Djg

Danke, jetzt läuft der sketch.
Problem was die Version des Boards. Ich habe auf eine Version < 3 (2.74) umgestellt und das Compilieren lief durch. Die übertragung auf die Fritzbox 7490 läuft super.

l_fileName =“test.txt” // localer Filename im LittleFilesystem
r_fileName = “/FRITZ/t/test.txt” // Remote Filename, schreibt in den Ordner t auf den immer vorhandenen FRITZ-Ordner
ftpClient.transfer(l_fileName, r_fileName, FTPClient::FTP_PUT_NONBLOCKING);

Zu erwähnen wäre noch, das hier ArduinoIDE Version 1.. verwendet wird.
In der AduinoIDE Version 2.0 ist das LittleFS als Bibliothek vorhanden, es kann aber kein Tool zum Hochladen der Dateien ( Dateien im Folder Data) verwendet werden, da diese Tool-Funktion nicht implementiert wurde.

Andreas Wolter

Andreas Wolter

@Dig: es kann mehrere Ursachen haben.
Konnten Sie die mitgelieferten Beispiele kompilieren?
Eventuell ist die Bibliothek nicht vollständig installiert, oder eventuell passen die Versionen der Arduino IDE, dem ESP8266 Core und der Bibliothek nicht mehr zusammen. Dazu müsste man mehr Tests durchführen. Nochmal neu installieren. Die Versionen ändern. Das würde ich als erstes probieren.

Grüße,
Andreas Wolter
AZ-Delivery Blog

Djg

Djg

Ich bekomme immer eine Fehlermeldung :
In file included from C:\Users\Dietm\Documents\Arduino\libraries\FTPClientServer-master/FTPClient.h:23,
from D:\SW-Arduino\FTP006\FTP006.ino:21:
C:\Users\Dietm\Documents\Arduino\libraries\FTPClientServer-master/FTPCommon.h:11:7: error: ‘esp8266Pool’ has not been declared
11 | using esp8266Pool::polledTimeout::oneShotMs; // import the type to the local namespace
| ^~~~~~~~~~~
In file included from C:\Users\Dietm\Documents\Arduino\libraries\FTPClientServer-master/FTPClient.h:23,
from D:\SW-Arduino\FTP006\FTP006.ino:21:
C:\Users\Dietm\Documents\Arduino\libraries\FTPClientServer-master/FTPCommon.h:130:5: error: ‘oneShotMs’ does not name a type
130 | oneShotMs aTimeout; // timeout from esp8266 core library
| ^~~~~~~~~
exit status 1
Fehler beim Kompilieren für das Board NodeMCU 1.0 (ESP-12E Module).

Woran kann das liegen?

peko

peko

Hallo,

vielen dank für das tolle Script !
Ich habe es nach meinen Anforderungen fast komlett anpassen können.
Nur finde ich keinen Weg, das Directory zu wechseln, da es im Homeverzeichnis
des angegebenen Usernamen speichert.

Gruß peko

Raphael

Raphael

Wow, ein super Artikel! Genau das habe ich gesucht. – Die Kombination aus littleFS und FTP. Das Script funktioniert. Vielen Dank dafür!

HaJoWi

HaJoWi

Danke für die umfassende Recherche und die Beispiele. Tolle Grundlage für meine Experimente!

davi

davi

Hello for esp8266 based project, I’m using ESP8266FtpServer by david@paivahome.com. It works with SPIFFS and LittleFS . IT does only support FTP (but in my case it is not a problem as my domitic environment is on a separated lan . Using a fileZilla (or a basic python script) I can distribute my config files accross my whole network in basically “one-click”.

Kommentar hinterlassen

Alle Kommentare werden von einem Moderator vor der Veröffentlichung überprüft

Empfohlene Blogbeiträge

  1. ESP32 jetzt über den Boardverwalter installieren - AZ-Delivery
  2. Internet-Radio mit dem ESP32 - UPDATE - AZ-Delivery
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1 - AZ-Delivery
  4. ESP32 - das Multitalent - AZ-Delivery