FTPino - A FTP Client and Server for the Particle

Dear all,

I want to share with you a project I’ve been working on the last week.
It’s called FTPino and it’s a FTPServer and FTPClient. I’m using it to log data from my Particle constellation to a SDCard.

I’m currently in the process of creating a library and integrating it with the Particle cloud.

Feedback is much appreciated.

Cheers,
Mihai

Sources:

3 Likes

Hello mihaigalos,

Thanks for posting the ftp server and client library. I gave your library a try with two photons: one running FTPServer and the other running FTPClient. I receiver different errors in the initial handshake of the client and server:

“Error when sending Type I.”


“Payload: 502 Command not implemented: .”


Error when sening PASS (credential password).


Have you tried using two photons previously?

Hello shm45,

I’ve only tested it with Client->PC or PC<–>Server, so this usecase was never validated.

The “TYPE” Command should be implemented already.

Can you please post the code in full so as I can reproduce it?

M

application.cpp for Client:

[CODE]

// This #include statement was automatically added by the Particle IDE.
#include “application.h”
#include “FTPClient.h”

#define CLIENT_ACTIVE
#define FTPWRITE

int setIP(String command);

char str[96];

String ftpAddress = “192.168.0.102”;
String user = “User_Rre4”;
String pass = “atari”;
String remoteFile = “test.txt”;

char fileName[] = “test.txt”;
String stringToWrite= "Lorem Ipsum is simply dummy text of the printing and typesetting industry. "
"Lorem Ipsum has been the industry’s standard dummy text ever since the 1500s, when an unknown printer took a "
"galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, "
"but also the leap into electronic typesetting, remaining essentially unchanged. "
"It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently "
“with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.”;

SYSTEM_MODE(AUTOMATIC);

FTPClient *ftp = NULL;

void setup()
{
Particle.function(“setip”, setIP);
}

void loop(){

if(Serial.available()){
char command = (char)Serial.read();
switch(command){
case ‘1’:
Serial.println(WiFi.localIP());
break;
case ‘2’:
ftp = new FTPClient(ftpAddress, user, pass);
Serial.println(“created ftp client”);
break;
case ‘3’:
Particle.publish (“IcarusReport”,ftp->send(stringToWrite, remoteFile));
Serial.println(“sent ftp->send”);
break;
case ‘d’:
sprintf(str, “Going to DFU”);
Serial.println(str);
System.dfu();
break;
}
}

}

int setIP(String command){
ftpAddress = command;
sprintf(str,“IP: %s”,command.c_str());
Serial.println(str);
return 47;
}

[/CODE]

FTPClient.cpp (I hacked it a little to use the class constructor):

[CODE]
/*****************************************************************************
*

  • This file is part of FTPino.
  • FTPino is free software: you can redistribute it and/or modify
  • it under the terms of the GNU General Public License as published by
  • the Free Software Foundation, either version 3 of the License, or
  • (at your option) any later version.
  • FTPino is distributed in the hope that it will be useful,
  • but WITHOUT ANY WARRANTY; without even the implied warranty of
  • MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  • GNU General Public License for more details.
  • You should have received a copy of the GNU General Public License
  • along with FTPino. If not, see http://www.gnu.org/licenses/.

******************************************************************************/
#include “Config.h”
//#ifdef CLIENT_ACTIVE
#include “FTPClient.h”

FTPClient::FTPClient(String &_server, String &_username, String &_password)
{
server = _server;
username = _username;
password = _password;
}

char FTPClient::readServerResponse()
{
char respCode;
char thischar;

while(!client.available()) Particle.process(); // run main loop while waiting for client
respCode = client.peek();
outCount = 0;

while(client.available())
{
    thischar = client.read();

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

Particle.publish("Response: "+String(respCode),"Payload: "+String(outBuf));
delay(1000);
if(respCode >= '4')
{
    onFail();
    return 0;
}

return 1;

}

String FTPClient::send(String &stringToWrite, String &remoteFile, TE_FTPClient_WriteMode writeMode){

    client.connect(server,21);
    if (!client.connected())  return "Error, cannot connect to FTP.";


    if(!readServerResponse()) return "Error when connecting to FTP Server.";


    client.println("USER "+username);       if(!readServerResponse()) return "Error when sending USER (credential username).";
    client.println("PASS "+password);       if(!readServerResponse()) return "Error when sening PASS (credential password).";
    client.println("SYST");                 if(!readServerResponse()) return "Error when sending SYST.";
    client.println("Type I");               if(!readServerResponse()) return "Error when sending Type I.";

    client.println("PASV");                 if(!readServerResponse()) return "Error when sending PASV.";

    char *tStr = strtok(outBuf,"(,"); // tokenizing response of server, getting ports for data transfer
    int array_pasv[6];
    for ( int i = 0; i < 6; i++) {
        tStr = strtok(NULL,"(,");
        array_pasv[i] = atoi(tStr);
        if(tStr == NULL)
        {
            return "Error when tokenizing server response for passive data ports.";
        }
    }

    if (!dclient.connect(server,(array_pasv[4] << 8) | (array_pasv[5] & 255))) { // opening new datastream with the ports from the tokenized server response
        client.stop();
        return "Connection Error when creating second FTP Socket.";
    }

    String writeModeCommand;
    switch (writeMode){
        case TE_FTPClient_WriteMode_Append:                     writeModeCommand= "APPE";       break;
        case TE_FTPClient_WriteMode_Overwrite:                  writeModeCommand= "STOR";       break;
        TE_FTPClient_WriteMode_Unknown:
        default:             return "Error, unknown write mode for passive data socket.";       break;

    }

    client.print(writeModeCommand+" ");
    client.println(remoteFile);
    if(!readServerResponse())  { dclient.stop(); return "Error when sending "+writeModeCommand+"."; }




    char clientBuf[64]; uint32_t clientCount = 0, posInOutString =0;
    do
    {
        clientBuf[clientCount++] = stringToWrite[posInOutString++];

        if(clientCount > 63)
        {
          dclient.write(reinterpret_cast<const uint8_t*>(clientBuf),64);
          clientCount = 0;
        }
    }while(posInOutString < stringToWrite.length());

    if(clientCount > 0) dclient.write(reinterpret_cast<const uint8_t*>(clientBuf),clientCount); // finish off wriring what's left in the buffer

    dclient.stop();         if(!readServerResponse()) return "Error when stopping client.";
    client.println("QUIT"); if(!readServerResponse()) return "Error when sending QUIT to server.";
    client.stop();

    return "FTP Success.";

}

void FTPClient::onFail(){

client.println(“QUIT”);
client.stop();
}
//#endif //CLIENT_ACTIVE

[/CODE]

application.cpp for Server:

[CODE]
/*****************************************************************************
*

  • This file is part of FTPino.
  • FTPino is free software: you can redistribute it and/or modify
  • it under the terms of the GNU General Public License as published by
  • the Free Software Foundation, either version 3 of the License, or
  • (at your option) any later version.
  • FTPino is distributed in the hope that it will be useful,
  • but WITHOUT ANY WARRANTY; without even the implied warranty of
  • MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  • GNU General Public License for more details.
  • You should have received a copy of the GNU General Public License
  • along with FTPino. If not, see http://www.gnu.org/licenses/.

******************************************************************************/

// This #include statement was automatically added by the Particle IDE.
//#include “SdFat/SdFat.h”
#include “sd-card-library/firmware/sd-card-library-photon-compat.h”
//#include “FTPino/Buzz.h”
// This #include statement was automatically added by the Particle IDE.
#include “FileHandler.h”

// This #include statement was automatically added by the Particle IDE.
#include “FTPServer.h”
//#include “FTPClient.h”
#define TIMEZONE (-7)

char str[96];

String user = “User_Rre4”;
String pass = “atari”;

FTPServer *ftpServer = NULL;

String status;

void setup()
{
Time.zone(TIMEZONE);
ftpServer = new FTPServer(user, pass, 21, 63);

Particle.publish("FTPinoIP", String(WiFi.localIP()));
Serial.println(WiFi.localIP());
pinMode(D7, OUTPUT);
digitalWrite(D7,LOW);

}

void loop()
{

status = ftpServer->run();
if(status.length()>0) Particle.publish(“FTPServer”), delay(1000);;

if(Serial.available()){

 char command = (char)Serial.read();
  switch(command){
    case '1':
      Serial.println(WiFi.localIP());
      break;
    case '2':
      // nothing
      break;
    case '3':
      // nothing
      break;
    case 'd':
       sprintf(str, "Going to DFU");
       Serial.println(str);
       System.dfu();
       break;
    }
}

}

[/CODE]

FTPServer.cpp: (hacking to use Photon-compatible SD card library)

[CODE]
/*****************************************************************************
*

  • This file is part of FTPino.
  • FTPino is free software: you can redistribute it and/or modify
  • it under the terms of the GNU General Public License as published by
  • the Free Software Foundation, either version 3 of the License, or
  • (at your option) any later version.
  • FTPino is distributed in the hope that it will be useful,
  • but WITHOUT ANY WARRANTY; without even the implied warranty of
  • MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  • GNU General Public License for more details.
  • You should have received a copy of the GNU General Public License
  • along with FTPino. If not, see http://www.gnu.org/licenses/.

******************************************************************************/
#include “Config.h”

#ifdef SERVER_ACTIVE

//#include “Buzz.h”
//#include “SdFat/SdFat.h”
#include “sd-card-library/firmware/sd-card-library-photon-compat.h”
#include “application.h”
#include “FTPServer.h”

uint32_t FTPServer::totalConnections;

#ifdef DEBUG
//ArduinoOutStream cout(Serial);

class OutHelper{
public:
static unsigned char buffer [1024];

static unsigned char* c_str(String s){
    s.getBytes(buffer, sizeof(buffer));
    return &buffer[0];
}

};

unsigned char OutHelper::buffer [1024];

void dbg(String param1, String param2){
    #if (DEBUG == 1) || (DEBUG == 9)
    Particle.publish(param1, param2); delay(1000);
    #elif (DEBUG == 2)|| (DEBUG == 9)
    cout<<"[ "<<OutHelper::c_str(Time.timeStr())<<" ]     "<<OutHelper::c_str(param1)<<" -> "<<OutHelper::c_str(param2)<<endl; delay(30); //throttle
    #endif
}

else

inline void dbg(String param1, String param2) {}
#endif

FTPServer::FTPServer(String & user, String &pass, uint16_t _port, int16_t _timeoutSec){
#if DEBUG > 0
Serial.begin(9600);
#endif
credentials.username = user; credentials.password = pass; port = _port;
server = new TCPServer(port); server->begin();
dserver = new TCPServer((passiveDataPortHi<<8) | (passiveDataPortLo & 255)); dserver->begin();

pinMode(D7, OUTPUT);

digitalWrite(D7,LOW);
totalConnections = 0;
#if -1 != BUZZER_PIN
//Buzzer::init(D0);
#endif
fh = FileHandlerFactory::newFileHandler(static_cast(FILESYSTEM));
if(_timeoutSec > 0) timeoutSec = _timeoutSec;

}

FTPServer::~FTPServer(){
#if DEBUG > 0
Serial.end();
#endif
}

TEftpState FTPServer::parseCommand(String response, String &info){
TEftpState newState = TEftpState_Unknown; auto posEnd = response.indexOf(’\r’);
if(response.startsWith(“USER”)){
receivedCredentials.username = response.substring(String(“USER “).length(), posEnd); info = receivedCredentials.username; newState = TEftpState_User;
}else if(response.startsWith(“PASS”)){
receivedCredentials.password = response.substring(String(“PASS “).length(), posEnd);
if( (credentials.username == receivedCredentials.username) && (credentials.password == receivedCredentials.password)) newState = TEftpState_AuthOk;
else newState = TEftpState_AuthFail;
}else if(response.startsWith(“PORT”)){
auto lastComma= response.lastIndexOf(”,”); auto secondLastComma = response.lastIndexOf(”,”,lastComma-1);
auto hiPort = response.substring(secondLastComma+1, lastComma); auto loPort = response.substring(lastComma+1, posEnd);
activeDataPortHi= hiPort.toInt(); activeDataPortLo = loPort.toInt();
newState = TEftpState_Port;
}

else if(response.startsWith("PWD"))		newState = TEftpState_CurrentDir;		else if(response.startsWith("QUIT"))		    newState = TEftpState_Quit;
else if(response.startsWith("FEAT"))	newState = TEftpState_Features;			else if(response.startsWith("SYST"))		    newState = TEftpState_System;
else if(response.startsWith("PASV"))	newState = TEftpState_Passive;			else if(response.startsWith("LIST"))		    newState = TEftpState_List;
else if(response.startsWith("TYPE"))	newState = TEftpState_Type;         	else if(response.startsWith("CDUP"))            newState = TEftpState_ParentDir;

else if(response.startsWith("REST"))    newState = TEftpState_RestartAt,    info = response.substring(String("REST ").length(), posEnd);
else if(response.startsWith("RETR"))	newState = TEftpState_RetrieveFile, info = response.substring(String("RETR ").length(), posEnd);
else if(response.startsWith("DELE"))	newState = TEftpState_DeleteFile,   info = response.substring(String("DELE ").length(), posEnd);
else if(response.startsWith("STOR"))	newState = TEftpState_Store,        info = response.substring(String("STOR ").length(), posEnd);
else if(response.startsWith("MKD"))	    newState = TEftpState_MakeDir,      info = response.substring(String("MKD ") .length(), posEnd);

else if(response.startsWith("APPE"))	newState = TEftpState_Append,       info = response.substring(String("APPE ").length(), posEnd);
else if(response.startsWith("CWD"))		newState = TEftpState_ChangeDir,    info = response.substring(String("CWD ") .length(), posEnd);
else if(response.startsWith("RNFR"))    newState = TEftpState_RenameFrom,   info = response.substring(String("RNFR ").length(), posEnd);
else if(response.startsWith("RNTO"))    newState = TEftpState_RenameTo,     info = response.substring(String("RNTO ").length(), posEnd);

else if(response.startsWith("RMD"))     newState = TEftpState_DeleteDir,    info = response.substring(String("RMD ") .length(), posEnd);

else info = response.substring(0,posEnd);

if((-1 != aliveTimer) &&(response.length()>0)) aliveTimer = millis() + timeoutSec*1000;
return newState;

}

void FTPServer::waitForClientDataConnection(){
do{ dclient = dserver->available(); Particle.process();} while(!dclient.connected());
}

String FTPServer::dataRead(){
char inBuffer[1024]; memset(inBuffer, 0, sizeof(inBuffer)); uint8_t pos =0 ;
while (client.available()) inBuffer[pos++]= client.read();
return String(inBuffer);
}

void FTPServer::dataWrite(String data){
server->println(“150 Opening ASCII mode data connection for transfer.”);
dserver->write(data);
dclient.stop(); dclient.flush();
server->println(“226 Transfer complete.”);
}

void FTPServer::readFile(String file){
auto bytesRead =0; auto totalBytes = 0;
server->println(“150 Opening ASCII mode data connection for transfer.”);
uint8_t buf[256]; auto fileSize = fh->fileSize(file);
do{
bytesRead = fh->readFile(file, buf, sizeof(buf));
auto writtenBytes = 0; auto lengthToWrite = sizeof(buf);
if(totalBytes + sizeof(buf) >=fileSize) lengthToWrite = fileSize-totalBytes; // account for very last iteration, do not print buffer twice
do{
if (transferMode==TEftpTransferMode_Active) writtenBytes = dclient.write(buf, lengthToWrite);
else if (transferMode==TEftpTransferMode_Passive) writtenBytes = dserver->write(buf, lengthToWrite); /* use passive store at your own risk. It doesn’t write the last couple of kB of a file.
This is probably because the client side closes and flushes the output buffer before the FTPino
server has had a chance to read it all. Use the active mode for tranferring files to and from FRPino.*/

    }while (ERR_BUFFER_FULL == writtenBytes);
    totalBytes+= writtenBytes;
}while( (bytesRead == sizeof(buf)) && bytesRead > 0); // continue while still reading chunks of sizeof(buffer)from card..
dclient.stop(); fh->flush();
server->println("226 Transfer complete. "+String(totalBytes)+" read.");

}

void FTPServer::writeFile(String file, IFileHandler* fh){
server->println(“150 Opening BINARY mode data connection for file transfer.”);

uint32_t pos =0; auto totalBytes = 0; uint8_t readBuffer[256];
if(!dclient.connected()) {server->println("425 No data connection"); return; }
else{
     auto bytesRead =0;
     do{
        bytesRead = dclient.read(readBuffer, sizeof(readBuffer));
        if(bytesRead>0){
            fh->writeFile(file, reinterpret_cast<char*>(readBuffer), bytesRead);
            totalBytes+= bytesRead, bytesRead = 0;
        }
    }while( dclient.connected() || dclient.available());
}
dclient.stop();dclient.flush();
fh->flush();
server->println("226 Transfer complete. "+String(totalBytes)+" bytes written.");

}

String FTPServer::run(){

if (client.connected()) {
	String clientIP = String(remoteIp = client.remoteIP());
	dbg("-------------------------------------------------------------","");
	dbg("FTPino", "Client connected: "+clientIP);
	#if -1 != BUZZER_PIN
	//Buzzer::beepTwice();
	#endif
	digitalWrite(D7,HIGH);
	String clientResponse, parseInfo;

	if(timeoutSec>0)    aliveTimer = millis() + timeoutSec*1000;
    else                aliveTimer = -1;

		while(client.connected()){

			switch(state){
				case TEftpState_Init:           server->println("220 Welcome to FTPino FTP Server.");							                break;
				case TEftpState_User:           server->println("331 Password required for user "+parseInfo+".");				                break;
				case TEftpState_AuthOk:         server->println("230 User successfuly logged in.");							                    break;
				case TEftpState_AuthFail:       client.stop();																	                break;

				case TEftpState_CurrentDir:     server->println("257 \"/\" is current directory.");							                    break;
				case TEftpState_System:         server->println("215 UNIX emulated by FTPino.");								                break;
				case TEftpState_Features:       server->println("211 Extensions supported SIZE MDTM XCRC.");					                break;
				case TEftpState_Type:           server->println("200 Type set to BINARY.");    	                                                break;

				case TEftpState_RestartAt:      server->println("350 Restarting at "+parseInfo+". !!! Not implemented");                        break;
				case TEftpState_Store:          writeFile(parseInfo, fh);                                                                       break;
				case TEftpState_DeleteDir:      if(fh->deleteTarget(parseInfo,true)<0) server->println("550 Can't delete Directory.");
				                                else server->println("250 Directory "+parseInfo+" was deleted.");                               break;
				case TEftpState_DeleteFile:     if(fh->deleteTarget(parseInfo,false)<0) server->println("550 Can't delete File.");
				                                else server->println("250 File "+parseInfo+" was deleted.");                                    break;
				case TEftpState_MakeDir:        if(fh->makeDir(parseInfo)<0) server->println("550 Can't create directory.");
				                                else server->println("257 Directory created : "+parseInfo+".");                                 break;

				case TEftpState_RenameFrom:     fh->renameFrom(parseInfo); server->println("350 Directory exists, ready for destination name"); break;
				case TEftpState_RenameTo:       fh->renameTo(parseInfo);   server->println("250 Directory renamed successfully");               break;
				case TEftpState_List:           dataWrite(fh->getDirList());										                            break; // implementing LIST verb as described at : https://files.stairways.com/other/ftp-list-specs-info.txt
				case TEftpState_RetrieveFile:   readFile(parseInfo);                                                                            break;
				case TEftpState_Quit:	        server->println("221 Bye.");	client.stop(); dclient.stop();                                  break;
				case TEftpState_ParentDir:      fh->changeToParentDir(); server->println("250 Going one level up.");                            break;
				case TEftpState_Port: {
				    transferMode = TEftpTransferMode_Active;
				    dclient.connect(remoteIp, activeDataPortHi<<8 | activeDataPortLo);server->println("200 Port command successful.");
				    break;
				}
				case TEftpState_Passive: {
					auto ip = WiFi.localIP(); char buffer [1024];
					sprintf(buffer,"(%d,%d,%d,%d,"+String(passiveDataPortHi)+ ","+String(passiveDataPortLo)+")",ip[0],ip[1],ip[2],ip[3]);
					server->println("227 Entering Passive Mode "+String(buffer));
					waitForClientDataConnection();
					transferMode = TEftpTransferMode_Passive;
					break;
				}
				case TEftpState_ChangeDir: {
				    if(fh->changeDir(parseInfo)<0)  server->println("550 Can't change directory to "+parseInfo+".");
				    else                            server->println("250 \"/"+parseInfo+"\" is current directory.");
				    break;
				}

				case TEftpState_Append:{
					break;
				}

			}



			if( client.connected()){// if client not disconnected by the logic above
				do{
					delay(100); // allow the client to read response

					clientResponse = dataRead();
					auto newState = parseCommand(clientResponse, parseInfo);

					if(newState != state) state=newState;
					if((clientResponse.length() > 0) && (TEftpState_Unknown == state)) server->println("502 Command not implemented: "+parseInfo+".");
					if((-1 != aliveTimer) && (static_cast<int64_t>(millis()) -static_cast<int64_t>(aliveTimer)) > 0)  server->println("530 Timeout, disconnecting control socket."), state = TEftpState_Quit; // timeout

				}while(TEftpState_Unknown == state) ;
			}



		}

		state = TEftpState_Init;
		dbg("FTPino", "Client disconnected: "+clientIP);
		fh->flush();
		#if -1 != BUZZER_PIN
		//Buzzer::beepOnce();
		#endif
		totalConnections++;
} else {

	client = server->available();

	digitalWrite(D7,LOW);
	state = TEftpState_Init;
}



return "";

}
#endif // SERVER_ACTIVE

[/CODE]

Great, thanks!

Here are my initial comments:

  • You don’t need include the SDCard library in the FTPino server. I forgot the SDFat.h header there, you can just remove it entirely. The magic has to happen in the FileHandler. There you can create your own handler and interact with your desired sd library. Just create a new class which handles IO to the SD and create a new instance in the factory.

  • No need to comment out the Buzzer part, as it already has #define guards. Just specify if you are using a buzzer or not in the Config.h and it should work.

I’ll look into the code and try to debug it, I’ll be back when I find something!

Hello Mihai,

Thank you for the clarification on the FileHandler class and the #define for buzzer in Config.h.

The issues seems to be the Client and Server never getting past the lines in FTPClient::send( ) below:

[CODE]
client.println("USER "+username); if(!readServerResponse()) return “Error when sending USER (credential username).”;
client.println("PASS "+password); if(!readServerResponse()) return “Error when sening PASS (credential password).”;
client.println(“SYST”); if(!readServerResponse()) return “Error when sending SYST.”;
client.println(“Type I”); if(!readServerResponse()) return “Error when sending Type I.”;

    client.println("PASV");                 if(!readServerResponse()) return "Error when sending PASV.";

[/CODE]

Cheers,
shm45

Hi there,

Got it running !

v1.0.1 is ready. You can check out the GitHub link above. When we have a stable Photon2Photon communication on your side I can release it in the libraries.

Thanks for the support.

Hello Mihai,

thanks for the update. I pulled the latest FTPClient.h/.cpp and FTPServer.h/.cpp and kept my application.cpp the same for both client and server, but still see issues when attempting to send IcarusReport. see image below of output from Particle dashboard:

Thanks,
shm45

Hello shm45,

The server should already be online when the client connects, in your screenshot the server come online after the client.
Anyway, this is the code I used to validate it. Maybe you could try it as it’s really simple? Comment out the Server_Active on the client side and the Client_Active on the server side…

M

	//#define SERVER_ACTIVE
#define CLIENT_ACTIVE
#include "Config.h"
// This #include statement was automatically added by the Particle IDE.
#include "SdFat/SdFat.h"

#include "Buzz.h"

// This #include statement was automatically added by the Particle IDE.
#include "FileHandler.h"

// This #include statement was automatically added by the Particle IDE.
#include "FTPServer.h"

#include "FTPClient.h"


#define TIMEZONE (+2)

String user         = "Mihai";
String pass         = "MfTPpas$";

#ifdef SERVER_ACTIVE
FTPServer *ftpServer = NULL;
#endif

void setup()
{
	#ifdef SERVER_ACTIVE
	Time.zone(TIMEZONE);
	ftpServer = new FTPServer(user, pass, 21, 63);
	
	Particle.publish("FTPinoIP", String(WiFi.localIP()));
	
	pinMode(D7, OUTPUT);
	digitalWrite(D7,LOW);
	#endif
	
	#ifdef CLIENT_ACTIVE
	String ftpAddress   = "192.168.0.151";
	String user         = "Mihai";
	String pass         = "MfTPpas$";
	
	String fileName = "test.txt";
	String stringToWrite= "Lorem Ipsum is simply dummy text of the printing and typesetting industry. "
	"Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a "
	"galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, "
	"but also the leap into electronic typesetting, remaining essentially unchanged. "
	"It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently "
	"with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
	
	FTPClient ftp (ftpAddress, user, pass);
	String result = ftp.send(stringToWrite, fileName);
	delay(1000); Particle.publish("Client", result);
	#endif
	
}

void loop()
{
   #ifdef SERVER_ACTIVE   
   auto status = ftpServer->run();
   if(status.length()>0) Particle.publish("FTPServer"), delay(1000);
   #endif
}

Hello Mihai,

ok, the TCP client/server looks to be working, but I am having SD card issues. I attempted to use the photon compatible sd card library available through Particle Build, but i get the red blinks of death followed by a reset when attempting to write to the SD.

Could you post the version of the SdFat library you used for your example demo?

Thanks,
shm45

1 Like

is client “passive mode” where it logs in then connects to the server for the data transfer (server assigns port number) ? This is needed to pass through most enterprise scale firewalls.

The old FTP scheme was for the client to login then request a file then the server connected (TCP) to the client. That inbound connection attempt is blocked by most firewall policies.

@shm45
cool that you got the TCP Communication running! Concerning SDFatlib, I’m using the latest version from particle cloud. Did you remember to set the Chip Select on the pin you’re using ? Red flashes of death usually indicate a memory leak or a HW problem (least likely).

Check your wiring, and remember the SDCard needs 100-150mA @ 3.3V to function. Connect the VDD to the 3.3V pin, a normal I/O won’t do.

For debugging purposes, I reccommend using the USB Serial connection. Just get a client like Termite (http://www.compuphase.com/software_termite.htm), and listen to the Photon’s COM port. SDFat outputs stderr directly to Serial, so at least you’ll be able to see if the card can be initialized.

@stevech

The communication mode is Active, the reasond behind using Acrive vs Passive is described in the library’s doc.

Thanks for FTPino @mihaigalos. I got the example code working in 1st shot - transferring String text from Photon to a regular FTP server.

I need to transfer files (about 10MB) from an SD card though. In that case how can entire files be sent?

Hello Mihai–I got it working!

The trick was using the SdFat-Particle library (I spent way too much time trying to adapt the Arduino SdFat library).

I transferred a 466 kByte .wav file using a python client to upload the file to a Photon running FTPServer–worked like a charm:

[CODE]

simple ftp client to store file to ftp server

import ftplib

username = "User1"
passw = “Pass2”

filename = “music.wav”

specify 20 second timeout

ftp = ftplib.FTP(“192.168.1.2”, username, passw, “account”, 20)

ftp.storbinary("STOR " + filename, open(filename))
[/CODE]

Would you consider exposing chipSelect to the FTPServer constructor if people want a different pin besides A2?

-shm45

@pteja Great to hear you got it running with one shot! I am unsure as to when I’ll have time to continue wiith the missing features such as the one you pointed out. I’m commiting to minor bugfixes and maintaninance, however. If you would want to continue the development, I can give you admin access to the git repository.

@shm45 superb! I implemented the chip select as a constructor parameter.

Cheers,
Mihai

1 Like

First, thanks for this!

I just want to let you know that I was able to use your code with sd card and I also adapted it to use an SPI flash memory with SPIFFS.

But I have an issue with both file systems. As soon as I send a file bigger than 128 bytes, the last part of the file is always missing. I tried with FTP Rush. Read is ok but with write command last part of the file is always missing. An 8.5kb file will end with a size of around 5.5kb…

I tried to increase the maxIterations in the FTPServer::writeFile function and I also added a Particle.process() in the loop but it doesnt help.

And one strange thing I noticed and I’m not able to explain why is the loop is leaving because dclient.connected == 0 and dclient.active == 0 and isFTPinoClient == 0. Max iteration is always at 1000 so it means that we just did a write and everything fail after this…

How isFTPinoClient == 0… the only place where it’s set to false is in the run loop and there is a debug event just before that so I would see it…

@Suprazz Did you look at the docs? (yes, just scroll down the page, after click – maybe your browser zoomed in too much and you didn’t see them after the filelisting?) :smile:

In respect to FTPRush, you must configure your client to use a single socket for data. In v2.1.8 this can be found under Options -> Transfer -> Single connection mode (must be checked).

I’ve had a bittersweet experience trying to validate FTPino with FileZilla. Most operations work if one respects the prerequisite of setting a single data socket. All except the Store command. This is responsible for sending data to FTPino for it to be written to the SD.

FTP Active mode just doesn’t work. In Passive mode, the data is sent, received and written to the SD as you would expect. Except the last coouple of kB.

My only explanation as to what is happening is that FileZilla writes to FTPino’s buffer without checking if the buffer is full ? and closes the connection, before the client has had a chance to read the whole file… ?

yes I configured FTPRush to use single socket mode and I did all my tests with FTPRush and I’m not using filezilla.
If I understand well, this issue is only with filezilla right?

In my case it happens with FTPRush too and what I dont understand why and how isFTPinoClient is set to 0…

auto bytesRead =0;
 do
 {
	bytesRead = dclient.read(readBuffer, sizeof(readBuffer));
	if(bytesRead>0)
	{
		dbg("Bytes read:", String(bytesRead));
		maxIterations = 2000;
		fh->writeFile(file, reinterpret_cast<char*>(readBuffer), bytesRead, isAppend);
		totalBytes+= bytesRead, bytesRead = 0; 
	}
	Particle.process();
	maxIterations--;
}while(( dclient.connected() || dclient.available()) ||
		(isFTPinoClient && (maxIterations > 0)));

dbg("Client connected:", String(dclient.connected()));
dbg("Client available:", String(dclient.available()));
int intclient = isFTPinoClient ? 1 : 0;
dbg("IsFTPinoClient:", String(intclient));
dbg("Iterations:", String(maxIterations));

I did a wireshark and we can see that FTPRush is sending alll the data correctly.
My file length is 374 bytes and FTPIno quit after the first write of 128 bytes…